Skip to content

Commit

Permalink
Add sphinx.util._importer (#12762)
Browse files Browse the repository at this point in the history
  • Loading branch information
AA-Turner authored Aug 11, 2024
1 parent 655d1c7 commit d39ba32
Show file tree
Hide file tree
Showing 8 changed files with 85 additions and 56 deletions.
4 changes: 4 additions & 0 deletions CHANGES.rst
Original file line number Diff line number Diff line change
Expand Up @@ -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
--------------

Expand Down
5 changes: 5 additions & 0 deletions doc/extdev/deprecated.rst
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
9 changes: 6 additions & 3 deletions sphinx/builders/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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()
Expand Down
20 changes: 13 additions & 7 deletions sphinx/search/ja.py
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down Expand Up @@ -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)
Expand Down
27 changes: 3 additions & 24 deletions sphinx/util/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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')
Expand All @@ -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)),
}


Expand Down
27 changes: 27 additions & 0 deletions sphinx/util/_importer.py
Original file line number Diff line number Diff line change
@@ -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
23 changes: 1 addition & 22 deletions tests/test_util/test_util.py
Original file line number Diff line number Diff line change
Expand Up @@ -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


Expand Down Expand Up @@ -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]
Expand Down
26 changes: 26 additions & 0 deletions tests/test_util/test_util_importer.py
Original file line number Diff line number Diff line change
@@ -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

0 comments on commit d39ba32

Please sign in to comment.