Skip to content

Commit

Permalink
Add sphinx.util._files (sphinx-doc#12766)
Browse files Browse the repository at this point in the history
  • Loading branch information
AA-Turner authored Aug 11, 2024
1 parent c6163ff commit be73e64
Show file tree
Hide file tree
Showing 5 changed files with 94 additions and 76 deletions.
5 changes: 3 additions & 2 deletions CHANGES.rst
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,9 @@ Deprecated
* #12762: Deprecate ``sphinx.util.import_object``.
Use :py:func:`importlib.import_module` instead.
Patch by Adam Turner.
* #12766: Deprecate ``sphinx.util.FilenameUniqDict``
and ``sphinx.util.DownloadFiles``.
Patch by Adam Turner.

Features added
--------------
Expand All @@ -32,13 +35,11 @@ Bugs fixed
* #12514: intersphinx: fix the meaning of a negative value for
:confval:`intersphinx_cache_limit`.
Patch by Shengyu Zhang.

* #12730: The ``UnreferencedFootnotesDetector`` transform has been improved
to more consistently detect unreferenced footnotes.
Note, the priority of the transform has been changed from 200 to 622,
so that it now runs after the docutils ``Footnotes`` resolution transform.
Patch by Chris Sewell.

* #12587: Do not warn when potential ambiguity detected during Intersphinx
resolution occurs due to duplicate targets that differ case-insensitively.
Patch by James Addison.
Expand Down
10 changes: 10 additions & 0 deletions doc/extdev/deprecated.rst
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,16 @@ The following is a list of deprecated interfaces.
- Removed
- Alternatives

* - ``sphinx.util.FilenameUniqDict``
- 8.1
- 10.0
- N/A

* - ``sphinx.util.DownloadFiles``
- 8.1
- 10.0
- N/A

* - ``sphinx.util.import_object``
- 8.1
- 10.0
Expand Down
3 changes: 2 additions & 1 deletion sphinx/environment/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,8 @@
)
from sphinx.locale import __
from sphinx.transforms import SphinxTransformer
from sphinx.util import DownloadFiles, FilenameUniqDict, logging
from sphinx.util import logging
from sphinx.util._files import DownloadFiles, FilenameUniqDict
from sphinx.util._timestamps import _format_rfc3339_microseconds
from sphinx.util.docutils import LoggingReporter
from sphinx.util.i18n import CatalogRepository, docname_to_domain
Expand Down
76 changes: 3 additions & 73 deletions sphinx/util/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,13 +6,12 @@
import os
import posixpath
import re
from os import path
from typing import Any
from urllib.parse import parse_qsl, quote_plus, urlencode, urlsplit, urlunsplit

from sphinx.errors import FiletypeNotFoundError
from sphinx.locale import __
from sphinx.util import _importer, logging
from sphinx.util import _files, _importer, logging
from sphinx.util import index_entries as _index_entries
from sphinx.util.console import strip_colors # NoQA: F401
from sphinx.util.matching import patfilter # NoQA: F401
Expand Down Expand Up @@ -55,49 +54,6 @@ def get_filetype(source_suffix: dict[str, str], filename: str | os.PathLike) ->
raise FiletypeNotFoundError


class FilenameUniqDict(dict):
"""
A dictionary that automatically generates unique names for its keys,
interpreted as filenames, and keeps track of a set of docnames they
appear in. Used for images and downloadable files in the environment.
"""

def __init__(self) -> None:
self._existing: set[str] = set()

def add_file(self, docname: str, newfile: str) -> str:
if newfile in self:
self[newfile][0].add(docname)
return self[newfile][1]
uniquename = path.basename(newfile)
base, ext = path.splitext(uniquename)
i = 0
while uniquename in self._existing:
i += 1
uniquename = f'{base}{i}{ext}'
self[newfile] = ({docname}, uniquename)
self._existing.add(uniquename)
return uniquename

def purge_doc(self, docname: str) -> None:
for filename, (docs, unique) in list(self.items()):
docs.discard(docname)
if not docs:
del self[filename]
self._existing.discard(unique)

def merge_other(self, docnames: set[str], other: dict[str, tuple[set[str], Any]]) -> None:
for filename, (docs, _unique) in other.items():
for doc in docs & set(docnames):
self.add_file(doc, filename)

def __getstate__(self) -> set[str]:
return self._existing

def __setstate__(self, state: set[str]) -> None:
self._existing = state


def _md5(data: bytes = b'', **_kw: Any) -> hashlib._Hash:
"""Deprecated wrapper around hashlib.md5
Expand All @@ -114,34 +70,6 @@ def _sha1(data: bytes = b'', **_kw: Any) -> hashlib._Hash:
return hashlib.sha1(data, usedforsecurity=False)


class DownloadFiles(dict):
"""A special dictionary for download files.
.. important:: This class would be refactored in nearly future.
Hence don't hack this directly.
"""

def add_file(self, docname: str, filename: str) -> str:
if filename not in self:
digest = hashlib.md5(filename.encode(), usedforsecurity=False).hexdigest()
dest = f'{digest}/{os.path.basename(filename)}'
self[filename] = (set(), dest)

self[filename][0].add(docname)
return self[filename][1]

def purge_doc(self, docname: str) -> None:
for filename, (docs, _dest) in list(self.items()):
docs.discard(docname)
if not docs:
del self[filename]

def merge_other(self, docnames: set[str], other: dict[str, tuple[set[str], Any]]) -> None:
for filename, (docs, _dest) in other.items():
for docname in docs & set(docnames):
self.add_file(docname, filename)


class UnicodeDecodeErrorHandler:
"""Custom error handler for open() that warns and replaces."""

Expand Down Expand Up @@ -217,6 +145,8 @@ def isurl(url: str) -> bool:
'md5': (_md5, '', (9, 0)),
'sha1': (_sha1, '', (9, 0)),
'import_object': (_importer.import_object, '', (10, 0)),
'FilenameUniqDict': (_files.FilenameUniqDict, '', (10, 0)),
'DownloadFiles': (_files.DownloadFiles, '', (10, 0)),
}


Expand Down
76 changes: 76 additions & 0 deletions sphinx/util/_files.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
from __future__ import annotations

import hashlib
import os.path
from typing import Any


class FilenameUniqDict(dict[str, tuple[set[str], str]]):
"""
A dictionary that automatically generates unique names for its keys,
interpreted as filenames, and keeps track of a set of docnames they
appear in. Used for images and downloadable files in the environment.
"""

def __init__(self) -> None:
self._existing: set[str] = set()

def add_file(self, docname: str, newfile: str) -> str:
if newfile in self:
self[newfile][0].add(docname)
return self[newfile][1]
uniquename = os.path.basename(newfile)
base, ext = os.path.splitext(uniquename)
i = 0
while uniquename in self._existing:
i += 1
uniquename = f'{base}{i}{ext}'
self[newfile] = ({docname}, uniquename)
self._existing.add(uniquename)
return uniquename

def purge_doc(self, docname: str) -> None:
for filename, (docs, unique) in list(self.items()):
docs.discard(docname)
if not docs:
del self[filename]
self._existing.discard(unique)

def merge_other(self, docnames: set[str], other: dict[str, tuple[set[str], Any]]) -> None:
for filename, (docs, _unique) in other.items():
for doc in docs & set(docnames):
self.add_file(doc, filename)

def __getstate__(self) -> set[str]:
return self._existing

def __setstate__(self, state: set[str]) -> None:
self._existing = state


class DownloadFiles(dict[str, tuple[set[str], str]]):
"""A special dictionary for download files.
.. important:: This class would be refactored in nearly future.
Hence don't hack this directly.
"""

def add_file(self, docname: str, filename: str) -> str:
if filename not in self:
digest = hashlib.md5(filename.encode(), usedforsecurity=False).hexdigest()
dest = f'{digest}/{os.path.basename(filename)}'
self[filename] = (set(), dest)

self[filename][0].add(docname)
return self[filename][1]

def purge_doc(self, docname: str) -> None:
for filename, (docs, _dest) in list(self.items()):
docs.discard(docname)
if not docs:
del self[filename]

def merge_other(self, docnames: set[str], other: dict[str, tuple[set[str], Any]]) -> None:
for filename, (docs, _dest) in other.items():
for docname in docs & set(docnames):
self.add_file(docname, filename)

0 comments on commit be73e64

Please sign in to comment.