Move `sphinx.util re-exports to module __getattr__`

This commit is contained in:
Adam Turner 2025-01-14 01:01:52 +00:00
parent b52ac5c71b
commit cc8161a066
4 changed files with 194 additions and 64 deletions

View File

@ -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<schema>.+)://.*')
# 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)

View File

@ -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')

View File

@ -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()

View File

@ -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