From cc8161a066a8abe0acdbe4befdf86f9504508a38 Mon Sep 17 00:00:00 2001 From: Adam Turner <9087854+aa-turner@users.noreply.github.com> Date: Tue, 14 Jan 2025 01:01:52 +0000 Subject: [PATCH] Move ``sphinx.util`` re-exports to module ``__getattr__`` --- sphinx/util/__init__.py | 161 +++++++++++------- tests/test_builders/test_build_html_assets.py | 12 +- tests/test_directives/test_directive_code.py | 6 +- tests/test_util/test_util.py | 79 ++++++++- 4 files changed, 194 insertions(+), 64 deletions(-) diff --git a/sphinx/util/__init__.py b/sphinx/util/__init__.py index 4cd055b29..35fbc2315 100644 --- a/sphinx/util/__init__.py +++ b/sphinx/util/__init__.py @@ -2,49 +2,23 @@ from __future__ import annotations -import hashlib import os import posixpath import re from typing import TYPE_CHECKING -from sphinx.errors import ExtensionError as _ExtensionError from sphinx.errors import FiletypeNotFoundError -from sphinx.util import _files, _importer, logging -from sphinx.util import index_entries as _index_entries -from sphinx.util._lines import parse_line_num_spec as parselinenos # NoQA: F401 -from sphinx.util._uri import encode_uri # NoQA: F401 -from sphinx.util._uri import is_url as isurl # NoQA: F401 -from sphinx.util.console import strip_colors # NoQA: F401 -from sphinx.util.matching import patfilter # NoQA: F401 -from sphinx.util.nodes import ( # NoQA: F401 - caption_ref_re, - explicit_title_re, - nested_parse_with_titles, - split_explicit_title, -) - -# import other utilities; partly for backwards compatibility, so don't -# prune unused ones indiscriminately -from sphinx.util.osutil import ( # NoQA: F401 - SEP, - copyfile, - ensuredir, - make_filename, - os_path, - relative_uri, -) if TYPE_CHECKING: + import hashlib + from collections.abc import Callable + from types import ModuleType from typing import Any -logger = logging.getLogger(__name__) - # Generally useful regular expressions. ws_re: re.Pattern[str] = re.compile(r'\s+') url_re: re.Pattern[str] = re.compile(r'(?P.+)://.*') - # High-level utility functions. @@ -67,6 +41,8 @@ def _md5(data: bytes = b'', **_kw: Any) -> hashlib._Hash: To be removed in Sphinx 9.0 """ + import hashlib + return hashlib.md5(data, usedforsecurity=False) @@ -75,37 +51,108 @@ def _sha1(data: bytes = b'', **_kw: Any) -> hashlib._Hash: To be removed in Sphinx 9.0 """ + import hashlib + return hashlib.sha1(data, usedforsecurity=False) -# deprecated name -> (object to return, canonical path or empty string) -_DEPRECATED_OBJECTS: dict[str, tuple[Any, str, tuple[int, int]]] = { - 'split_index_msg': ( - _index_entries.split_index_msg, - 'sphinx.util.index_entries.split_index_msg', - (9, 0), - ), - 'split_into': ( - _index_entries.split_index_msg, - 'sphinx.util.index_entries.split_into', - (9, 0), - ), - 'ExtensionError': (_ExtensionError, 'sphinx.errors.ExtensionError', (9, 0)), - '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)), -} - - def __getattr__(name: str) -> Any: - if name not in _DEPRECATED_OBJECTS: - msg = f'module {__name__!r} has no attribute {name!r}' - raise AttributeError(msg) - from sphinx.deprecation import _deprecation_warning - deprecated_object, canonical_name, remove = _DEPRECATED_OBJECTS[name] - _deprecation_warning(__name__, name, canonical_name, remove=remove) - return deprecated_object + obj: Callable[..., Any] + mod: ModuleType + + # RemovedInSphinx90Warning + if name == 'split_index_msg': + from sphinx.util.index_entries import split_index_msg as obj + + canonical_name = f'{obj.__module__}.{obj.__qualname__}' + _deprecation_warning(__name__, name, canonical_name, remove=(9, 0)) + return obj + + if name == 'split_into': + from sphinx.util.index_entries import _split_into as obj + + _deprecation_warning(__name__, name, '', remove=(9, 0)) + return obj + + if name == 'ExtensionError': + from sphinx.errors import ExtensionError as obj + + canonical_name = f'{obj.__module__}.{obj.__qualname__}' + _deprecation_warning(__name__, name, canonical_name, remove=(9, 0)) + return obj + + if name in {'md5', 'sha1'}: + obj = globals()[f'_{name}'] + canonical_name = f'hashlib.{name}' + _deprecation_warning(__name__, name, canonical_name, remove=(9, 0)) + return obj + + # RemovedInSphinx10Warning + + if name in {'DownloadFiles', 'FilenameUniqDict'}: + from sphinx.util import _files as mod + + obj = getattr(mod, name) + _deprecation_warning(__name__, name, '', remove=(10, 0)) + return obj + + if name == 'import_object': + from sphinx.util._importer import import_object + + _deprecation_warning(__name__, name, '', remove=(10, 0)) + return import_object + + # Re-exported for backwards compatibility, + # but not currently deprecated + + if name == 'encode_uri': + from sphinx.util._uri import encode_uri + + return encode_uri + + if name == 'isurl': + from sphinx.util._uri import is_url + + return is_url + + if name == 'parselinenos': + from sphinx.util._lines import parse_line_num_spec + + return parse_line_num_spec + + if name == 'patfilter': + from sphinx.util.matching import patfilter + + return patfilter + + if name == 'strip_colors': + from sphinx.util.console import strip_colors + + return strip_colors + + if name in { + 'caption_ref_re', + 'explicit_title_re', + 'nested_parse_with_titles', + 'split_explicit_title', + }: + from sphinx.util import nodes as mod + + return getattr(mod, name) + + if name in { + 'SEP', + 'copyfile', + 'ensuredir', + 'make_filename', + 'os_path', + 'relative_uri', + }: + from sphinx.util import osutil as mod + + return getattr(mod, name) + + msg = f'module {__name__!r} has no attribute {name!r}' + raise AttributeError(msg) diff --git a/tests/test_builders/test_build_html_assets.py b/tests/test_builders/test_build_html_assets.py index 064c8c85a..60ee923f7 100644 --- a/tests/test_builders/test_build_html_assets.py +++ b/tests/test_builders/test_build_html_assets.py @@ -140,22 +140,26 @@ def test_file_checksum(app): def test_file_checksum_query_string(): with pytest.raises( - ThemeError, match='Local asset file paths must not contain query strings' + ThemeError, + match='Local asset file paths must not contain query strings', ): _file_checksum(Path(), 'with_query_string.css?dead_parrots=1') with pytest.raises( - ThemeError, match='Local asset file paths must not contain query strings' + ThemeError, + match='Local asset file paths must not contain query strings', ): _file_checksum(Path(), 'with_query_string.js?dead_parrots=1') with pytest.raises( - ThemeError, match='Local asset file paths must not contain query strings' + ThemeError, + match='Local asset file paths must not contain query strings', ): _file_checksum(Path.cwd(), '_static/with_query_string.css?dead_parrots=1') with pytest.raises( - ThemeError, match='Local asset file paths must not contain query strings' + ThemeError, + match='Local asset file paths must not contain query strings', ): _file_checksum(Path.cwd(), '_static/with_query_string.js?dead_parrots=1') diff --git a/tests/test_directives/test_directive_code.py b/tests/test_directives/test_directive_code.py index d54b320ce..422de5ee4 100644 --- a/tests/test_directives/test_directive_code.py +++ b/tests/test_directives/test_directive_code.py @@ -107,7 +107,8 @@ def test_LiteralIncludeReader_lines_and_lineno_match2(literal_inc_path, app): options = {'lines': '0,3,5', 'lineno-match': True} reader = LiteralIncludeReader(literal_inc_path, options, DUMMY_CONFIG) with pytest.raises( - ValueError, match='Cannot use "lineno-match" with a disjoint set of "lines"' + ValueError, + match='Cannot use "lineno-match" with a disjoint set of "lines"', ): reader.read() @@ -117,7 +118,8 @@ def test_LiteralIncludeReader_lines_and_lineno_match3(literal_inc_path, app): options = {'lines': '100-', 'lineno-match': True} reader = LiteralIncludeReader(literal_inc_path, options, DUMMY_CONFIG) with pytest.raises( - ValueError, match="Line spec '100-': no lines pulled from include file" + ValueError, + match="Line spec '100-': no lines pulled from include file", ): reader.read() diff --git a/tests/test_util/test_util.py b/tests/test_util/test_util.py index 2584206eb..745b75c61 100644 --- a/tests/test_util/test_util.py +++ b/tests/test_util/test_util.py @@ -2,7 +2,32 @@ from __future__ import annotations -from sphinx.util.osutil import ensuredir +import pytest + +import sphinx.util +from sphinx.deprecation import RemovedInSphinx10Warning, RemovedInSphinx90Warning +from sphinx.errors import ExtensionError +from sphinx.util._files import DownloadFiles, FilenameUniqDict +from sphinx.util._importer import import_object +from sphinx.util._lines import parse_line_num_spec +from sphinx.util._uri import encode_uri, is_url +from sphinx.util.console import strip_colors +from sphinx.util.index_entries import _split_into, split_index_msg +from sphinx.util.matching import patfilter +from sphinx.util.nodes import ( + caption_ref_re, + explicit_title_re, + nested_parse_with_titles, + split_explicit_title, +) +from sphinx.util.osutil import ( + SEP, + copyfile, + ensuredir, + make_filename, + os_path, + relative_uri, +) def test_ensuredir(tmp_path): @@ -12,3 +37,55 @@ def test_ensuredir(tmp_path): path = tmp_path / 'a' / 'b' / 'c' ensuredir(path) assert path.is_dir() + + +def test_exported_attributes(): + # RemovedInSphinx90Warning + with pytest.warns( + RemovedInSphinx90Warning, + match=r"deprecated, use 'sphinx.util.index_entries.split_index_msg' instead.", + ): + assert sphinx.util.split_index_msg is split_index_msg + with pytest.warns(RemovedInSphinx90Warning, match=r'deprecated.'): + assert sphinx.util.split_into is _split_into + with pytest.warns( + RemovedInSphinx90Warning, + match=r"deprecated, use 'sphinx.errors.ExtensionError' instead.", + ): + assert sphinx.util.ExtensionError is ExtensionError + with pytest.warns( + RemovedInSphinx90Warning, + match=r"deprecated, use 'hashlib.md5' instead.", + ): + _ = sphinx.util.md5 + with pytest.warns( + RemovedInSphinx90Warning, + match=r"deprecated, use 'hashlib.sha1' instead.", + ): + _ = sphinx.util.sha1 + + # RemovedInSphinx10Warning + with pytest.warns(RemovedInSphinx10Warning, match=r'deprecated.'): + assert sphinx.util.FilenameUniqDict is FilenameUniqDict + with pytest.warns(RemovedInSphinx10Warning, match=r'deprecated.'): + assert sphinx.util.DownloadFiles is DownloadFiles + with pytest.warns(RemovedInSphinx10Warning, match=r'deprecated.'): + assert sphinx.util.import_object is import_object + + # Re-exported for backwards compatibility, + # but not currently deprecated + assert sphinx.util.encode_uri is encode_uri + assert sphinx.util.isurl is is_url + assert sphinx.util.parselinenos is parse_line_num_spec + assert sphinx.util.patfilter is patfilter + assert sphinx.util.strip_colors is strip_colors + assert sphinx.util.caption_ref_re is caption_ref_re + assert sphinx.util.explicit_title_re is explicit_title_re + assert sphinx.util.nested_parse_with_titles is nested_parse_with_titles + assert sphinx.util.split_explicit_title is split_explicit_title + assert sphinx.util.SEP is SEP + assert sphinx.util.copyfile is copyfile + assert sphinx.util.ensuredir is ensuredir + assert sphinx.util.make_filename is make_filename + assert sphinx.util.os_path is os_path + assert sphinx.util.relative_uri is relative_uri