diff --git a/CHANGES.rst b/CHANGES.rst index b3fb5042b2c..bf85a55d2ea 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -13,6 +13,10 @@ Incompatible changes Deprecated ---------- +* #12762: Deprecate ``sphinx.util.import_object``. + Use :py:func:`importlib.import_module` instead. + Patch by Adam Turner. + Features added -------------- diff --git a/doc/extdev/deprecated.rst b/doc/extdev/deprecated.rst index aa61768fb63..f93485b7012 100644 --- a/doc/extdev/deprecated.rst +++ b/doc/extdev/deprecated.rst @@ -22,6 +22,11 @@ The following is a list of deprecated interfaces. - Removed - Alternatives + * - ``sphinx.util.import_object`` + - 8.1 + - 10.0 + - ``importlib.import_module`` + * - ``sphinx.ext.intersphinx.normalize_intersphinx_mapping`` - 8.0 - 10.0 diff --git a/sphinx/builders/__init__.py b/sphinx/builders/__init__.py index 8a51cbb6717..3c5313afe41 100644 --- a/sphinx/builders/__init__.py +++ b/sphinx/builders/__init__.py @@ -19,10 +19,10 @@ from sphinx.util import ( UnicodeDecodeErrorHandler, get_filetype, - import_object, logging, rst, ) +from sphinx.util._importer import import_object from sphinx.util.build_phase import BuildPhase from sphinx.util.console import bold from sphinx.util.display import progress_message, status_iterator @@ -139,8 +139,11 @@ def init(self) -> None: def create_template_bridge(self) -> None: """Return the template bridge configured.""" if self.config.template_bridge: - self.templates = import_object(self.config.template_bridge, - 'template_bridge setting')() + template_bridge_cls = import_object( + self.config.template_bridge, + source='template_bridge setting' + ) + self.templates = template_bridge_cls() else: from sphinx.jinja2glue import BuiltinTemplateLoader self.templates = BuiltinTemplateLoader() diff --git a/sphinx/search/ja.py b/sphinx/search/ja.py index 5669155cb04..7ff663292df 100644 --- a/sphinx/search/ja.py +++ b/sphinx/search/ja.py @@ -29,7 +29,7 @@ from sphinx.errors import ExtensionError, SphinxError from sphinx.search import SearchLanguage -from sphinx.util import import_object +from sphinx.util._importer import import_object class BaseSplitter: @@ -505,12 +505,18 @@ class SearchJapanese(SearchLanguage): language_name = 'Japanese' def init(self, options: dict[str, str]) -> None: - dotted_path = options.get('type', 'sphinx.search.ja.DefaultSplitter') - try: - self.splitter = import_object(dotted_path)(options) - except ExtensionError as exc: - raise ExtensionError("Splitter module %r can't be imported" % - dotted_path) from exc + dotted_path = options.get('type') + if dotted_path is None: + self.splitter = DefaultSplitter(options) + else: + try: + splitter_cls = import_object( + dotted_path, "html_search_options['type'] setting" + ) + self.splitter = splitter_cls(options) + except ExtensionError as exc: + msg = f"Splitter module {dotted_path!r} can't be imported" + raise ExtensionError(msg) from exc def split(self, input: str) -> list[str]: return self.splitter.split(input) diff --git a/sphinx/util/__init__.py b/sphinx/util/__init__.py index 4271f908930..07169f20b9f 100644 --- a/sphinx/util/__init__.py +++ b/sphinx/util/__init__.py @@ -6,15 +6,14 @@ import os import posixpath import re -from importlib import import_module from os import path from typing import IO, Any from urllib.parse import parse_qsl, quote_plus, urlencode, urlsplit, urlunsplit -from sphinx.errors import ExtensionError, FiletypeNotFoundError +from sphinx.errors import FiletypeNotFoundError from sphinx.locale import __ +from sphinx.util import _importer, logging from sphinx.util import index_entries as _index_entries -from sphinx.util import logging from sphinx.util.console import strip_colors # NoQA: F401 from sphinx.util.matching import patfilter # NoQA: F401 from sphinx.util.nodes import ( # NoQA: F401 @@ -217,27 +216,6 @@ def parselinenos(spec: str, total: int) -> list[int]: return items -def import_object(objname: str, source: str | None = None) -> Any: - """Import python object by qualname.""" - try: - objpath = objname.split('.') - modname = objpath.pop(0) - obj = import_module(modname) - for name in objpath: - modname += '.' + name - try: - obj = getattr(obj, name) - except AttributeError: - obj = import_module(modname) - - return obj - except (AttributeError, ImportError) as exc: - if source: - raise ExtensionError('Could not import %s (needed for %s)' % - (objname, source), exc) from exc - raise ExtensionError('Could not import %s' % objname, exc) from exc - - def encode_uri(uri: str) -> str: split = list(urlsplit(uri)) split[1] = split[1].encode('idna').decode('ascii') @@ -262,6 +240,7 @@ def isurl(url: str) -> bool: (9, 0)), 'md5': (_md5, '', (9, 0)), 'sha1': (_sha1, '', (9, 0)), + 'import_object': (_importer.import_object, '', (10, 0)), } diff --git a/sphinx/util/_importer.py b/sphinx/util/_importer.py new file mode 100644 index 00000000000..915750d2d88 --- /dev/null +++ b/sphinx/util/_importer.py @@ -0,0 +1,27 @@ +from __future__ import annotations + +from importlib import import_module +from typing import Any + +from sphinx.errors import ExtensionError + + +def import_object(object_name: str, /, source: str = '') -> Any: + """Import python object by qualname.""" + obj_path = object_name.split('.') + module_name = obj_path.pop(0) + try: + obj = import_module(module_name) + for name in obj_path: + module_name += '.' + name + try: + obj = getattr(obj, name) + except AttributeError: + obj = import_module(module_name) + except (AttributeError, ImportError) as exc: + if source: + msg = f'Could not import {object_name} (needed for {source})' + raise ExtensionError(msg, exc) from exc + msg = f'Could not import {object_name}' + raise ExtensionError(msg, exc) from exc + return obj diff --git a/tests/test_util/test_util.py b/tests/test_util/test_util.py index e1788e0d16e..8cf40a02965 100644 --- a/tests/test_util/test_util.py +++ b/tests/test_util/test_util.py @@ -5,8 +5,7 @@ import pytest -from sphinx.errors import ExtensionError -from sphinx.util import encode_uri, import_object, parselinenos +from sphinx.util import encode_uri, parselinenos from sphinx.util.osutil import ensuredir @@ -40,26 +39,6 @@ def test_ensuredir(): assert os.path.isdir(path) -def test_import_object(): - module = import_object('sphinx') - assert module.__name__ == 'sphinx' - - module = import_object('sphinx.application') - assert module.__name__ == 'sphinx.application' - - obj = import_object('sphinx.application.Sphinx') - assert obj.__name__ == 'Sphinx' - - with pytest.raises(ExtensionError) as exc: - import_object('sphinx.unknown_module') - assert exc.value.args[0] == 'Could not import sphinx.unknown_module' - - with pytest.raises(ExtensionError) as exc: - import_object('sphinx.unknown_module', 'my extension') - expected = 'Could not import sphinx.unknown_module (needed for my extension)' - assert exc.value.args[0] == expected - - def test_parselinenos(): assert parselinenos('1,2,3', 10) == [0, 1, 2] assert parselinenos('4, 5, 6', 10) == [3, 4, 5] diff --git a/tests/test_util/test_util_importer.py b/tests/test_util/test_util_importer.py new file mode 100644 index 00000000000..57fd6e46fb6 --- /dev/null +++ b/tests/test_util/test_util_importer.py @@ -0,0 +1,26 @@ +"""Test sphinx.util._importer.""" + +import pytest + +from sphinx.errors import ExtensionError +from sphinx.util._importer import import_object + + +def test_import_object(): + module = import_object('sphinx') + assert module.__name__ == 'sphinx' + + module = import_object('sphinx.application') + assert module.__name__ == 'sphinx.application' + + obj = import_object('sphinx.application.Sphinx') + assert obj.__name__ == 'Sphinx' + + with pytest.raises(ExtensionError) as exc: + import_object('sphinx.unknown_module') + assert exc.value.args[0] == 'Could not import sphinx.unknown_module' + + with pytest.raises(ExtensionError) as exc: + import_object('sphinx.unknown_module', 'my extension') + expected = 'Could not import sphinx.unknown_module (needed for my extension)' + assert exc.value.args[0] == expected