Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add sphinx.util._files #12766

Merged
merged 3 commits into from
Aug 11, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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)