diff --git a/CHANGES.rst b/CHANGES.rst index 596259e74db..238a63131e8 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -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 -------------- @@ -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. diff --git a/doc/extdev/deprecated.rst b/doc/extdev/deprecated.rst index f93485b7012..2a9cfb70601 100644 --- a/doc/extdev/deprecated.rst +++ b/doc/extdev/deprecated.rst @@ -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 diff --git a/sphinx/environment/__init__.py b/sphinx/environment/__init__.py index 7aef2ea0667..637fe185f7f 100644 --- a/sphinx/environment/__init__.py +++ b/sphinx/environment/__init__.py @@ -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 diff --git a/sphinx/util/__init__.py b/sphinx/util/__init__.py index ca0ac3139ed..6dd54cb5e76 100644 --- a/sphinx/util/__init__.py +++ b/sphinx/util/__init__.py @@ -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 @@ -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 @@ -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.""" @@ -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)), } diff --git a/sphinx/util/_files.py b/sphinx/util/_files.py new file mode 100644 index 00000000000..9252002411f --- /dev/null +++ b/sphinx/util/_files.py @@ -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)