From 427b36188186fcbd0547c9ae38887e1df9b54438 Mon Sep 17 00:00:00 2001 From: Adam Turner <9087854+aa-turner@users.noreply.github.com> Date: Mon, 20 Oct 2025 16:45:08 +0100 Subject: [PATCH 1/3] Use ``_load_object_by_name()`` directly --- sphinx/ext/autodoc/_documenters.py | 64 ++++++++-------------- sphinx/ext/autodoc/_member_finder.py | 18 +++++- sphinx/ext/autodoc/directive.py | 17 +++++- sphinx/ext/autodoc/importer.py | 4 +- sphinx/ext/autosummary/__init__.py | 26 +++++++-- tests/test_ext_autodoc/autodoc_util.py | 17 +++++- tests/test_ext_autodoc/test_ext_autodoc.py | 25 ++++++++- 7 files changed, 117 insertions(+), 54 deletions(-) diff --git a/sphinx/ext/autodoc/_documenters.py b/sphinx/ext/autodoc/_documenters.py index c2646345232..f05a2e48784 100644 --- a/sphinx/ext/autodoc/_documenters.py +++ b/sphinx/ext/autodoc/_documenters.py @@ -98,13 +98,9 @@ def __init__( self.get_attr = directive.get_attr self.orig_name = orig_name self.indent: Final = indent - # the parent/owner of the object to document - self.parent: Any = None # the module analyzer to get at attribute docs, or None self.analyzer: ModuleAnalyzer | None = None - self._load_object_has_been_called = False - if isinstance(self, ModuleDocumenter): self.options = self.options.merge_member_options() elif isinstance(self, ClassDocumenter): @@ -122,35 +118,6 @@ def add_line(self, line: str, source: str, *lineno: int, indent: str) -> None: else: self.directive.result.append('', source, *lineno) - def _load_object_by_name(self) -> Literal[True] | None: - """Import the object given by *self.orig_name*. - - Returns True if parsing and resolving was successful, otherwise None. - """ - if self._load_object_has_been_called: - return True - - ret = _load_object_by_name( - name=self.orig_name, - objtype=self.objtype, # type: ignore[arg-type] - mock_imports=self.config.autodoc_mock_imports, - type_aliases=self.config.autodoc_type_aliases, - current_document=self._current_document, - config=self.config, - env=self.env, - events=self._events, - get_attr=self.get_attr, - options=self.options, - ) - if ret is None: - return None - props, parent = ret - - self.props = props - self.parent = parent - self._load_object_has_been_called = True - return True - def add_directive_header(self, *, indent: str) -> None: """Add the directive header and options to the generated content.""" domain_name = getattr(self, 'domain', 'py') @@ -301,16 +268,21 @@ def generate( True, only generate if the object is defined in the module name it is imported from. If *all_members* is True, document all members. """ - if isinstance(self, ClassDocumenter): - # Do not pass real_modname and use the name from the __module__ - # attribute of the class. - # If a class gets imported into the module real_modname - # the analyzer won't find the source of the class, if - # it looks in real_modname. - real_modname = None - - if self._load_object_by_name() is None: + props = _load_object_by_name( + name=self.orig_name, + objtype=self.objtype, # type: ignore[arg-type] + mock_imports=self.config.autodoc_mock_imports, + type_aliases=self.config.autodoc_type_aliases, + current_document=self._current_document, + config=self.config, + env=self.env, + events=self._events, + get_attr=self.get_attr, + options=self.options, + ) + if props is None: return + self.props = props self._generate(more_content, real_modname, check_module, all_members) @@ -321,6 +293,14 @@ def _generate( check_module: bool = False, all_members: bool = False, ) -> None: + if self.props.obj_type in {'class', 'exception'}: + # Do not pass real_modname and use the name from the __module__ + # attribute of the class. + # If a class gets imported into the module real_modname + # the analyzer won't find the source of the class, if + # it looks in real_modname. + real_modname = None + # If there is no real module defined, figure out which to use. # The real module is used in the module analyzer to look up the module # where the attribute documentation would actually be found in. diff --git a/sphinx/ext/autodoc/_member_finder.py b/sphinx/ext/autodoc/_member_finder.py index b94855d3d62..de27b88dcd3 100644 --- a/sphinx/ext/autodoc/_member_finder.py +++ b/sphinx/ext/autodoc/_member_finder.py @@ -10,6 +10,7 @@ from sphinx.ext.autodoc._directive_options import _AutoDocumenterOptions from sphinx.ext.autodoc._property_types import _ClassDefProperties, _ModuleProperties from sphinx.ext.autodoc._sentinels import ALL, INSTANCE_ATTR, SLOTS_ATTR +from sphinx.ext.autodoc.importer import _load_object_by_name from sphinx.ext.autodoc.mock import ismock, undecorate from sphinx.locale import __ from sphinx.pycode import ModuleAnalyzer @@ -174,6 +175,8 @@ def _gather_members( If *want_all* is True, document all members, else those given by *self.options.members*. """ + env = directive.env + if props.obj_type not in {'module', 'class', 'exception'}: msg = 'must be implemented in subclasses' raise NotImplementedError(msg) @@ -236,8 +239,21 @@ def _gather_members( # We now try to import all objects before ordering them. This is to # avoid possible circular imports if we were to import objects after # their associated documenters have been sorted. - if documenter._load_object_by_name() is None: + member_props = _load_object_by_name( + name=full_name, + objtype=obj_type, + mock_imports=config.autodoc_mock_imports, + type_aliases=config.autodoc_type_aliases, + current_document=current_document, + config=config, + env=env, + events=events, + get_attr=get_attr, + options=documenter.options, + ) + if member_props is None: continue + documenter.props = member_props member_documenters.append((documenter, is_attr)) diff --git a/sphinx/ext/autodoc/directive.py b/sphinx/ext/autodoc/directive.py index f0eb803de34..33099dc186f 100644 --- a/sphinx/ext/autodoc/directive.py +++ b/sphinx/ext/autodoc/directive.py @@ -11,6 +11,7 @@ _process_documenter_options, ) from sphinx.ext.autodoc._documenters import _AutodocAttrGetter +from sphinx.ext.autodoc.importer import _load_object_by_name from sphinx.util import logging from sphinx.util.docutils import SphinxDirective, switch_source_input from sphinx.util.parsing import nested_parse_to_nodes @@ -145,7 +146,21 @@ def run(self) -> list[Node]: self.env, reporter, documenter_options, lineno, self.state, get_attr ) documenter = doccls(params, self.arguments[0]) - documenter.generate(more_content=self.content) + props = _load_object_by_name( + name=self.arguments[0], + objtype=objtype, # type: ignore[arg-type] + mock_imports=self.config.autodoc_mock_imports, + type_aliases=self.config.autodoc_type_aliases, + current_document=self.env.current_document, + config=self.config, + env=self.env, + events=self.env.events, + get_attr=get_attr, + options=documenter.options, + ) + if props is not None: + documenter.props = props + documenter._generate(more_content=self.content) if not params.result: return [] diff --git a/sphinx/ext/autodoc/importer.py b/sphinx/ext/autodoc/importer.py index 8d55e029070..c5acb6ca512 100644 --- a/sphinx/ext/autodoc/importer.py +++ b/sphinx/ext/autodoc/importer.py @@ -495,7 +495,7 @@ def _load_object_by_name( events: EventManager, get_attr: _AttrGetter, options: _AutoDocumenterOptions, -) -> tuple[_ItemProperties, Any] | None: +) -> _ItemProperties | None: """Import and load the object given by *name*.""" parsed = _parse_name( name=name, @@ -916,7 +916,7 @@ def _load_object_by_name( f'{args} -> {retann}' if retann else str(args) for args, retann in signatures ) - return props, parent + return props def _parse_name( diff --git a/sphinx/ext/autosummary/__init__.py b/sphinx/ext/autosummary/__init__.py index cce68486152..474e3826962 100644 --- a/sphinx/ext/autosummary/__init__.py +++ b/sphinx/ext/autosummary/__init__.py @@ -71,7 +71,7 @@ from sphinx.ext.autodoc._member_finder import _best_object_type_for_member from sphinx.ext.autodoc._sentinels import INSTANCE_ATTR from sphinx.ext.autodoc.directive import DocumenterBridge -from sphinx.ext.autodoc.importer import import_module +from sphinx.ext.autodoc.importer import _load_object_by_name, import_module from sphinx.ext.autodoc.mock import mock from sphinx.locale import __ from sphinx.pycode import ModuleAnalyzer @@ -339,6 +339,12 @@ def get_items(self, names: list[str]) -> list[tuple[str, str | None, str, str]]: ) raise ValueError(msg) + env = self.env + config = env.config + current_document = env.current_document + events = env.events + get_attr = self.bridge.get_attr + max_item_chars = 50 for name in names: @@ -363,7 +369,7 @@ def get_items(self, names: list[str]) -> list[tuple[str, str | None, str, str]]: result = StringList() # initialize for each documenter obj_type = _get_documenter(obj, parent) - doccls = self.env._registry.documenters[obj_type] + doccls = env._registry.documenters[obj_type] if isinstance(obj, ModuleType): full_name = real_name else: @@ -374,7 +380,19 @@ def get_items(self, names: list[str]) -> list[tuple[str, str | None, str, str]]: # handle module prefixes slightly differently self.bridge.result = result documenter = doccls(self.bridge, full_name) - if documenter._load_object_by_name() is None: + props = _load_object_by_name( + name=full_name, + objtype=obj_type, + mock_imports=config.autodoc_mock_imports, + type_aliases=config.autodoc_type_aliases, + current_document=current_document, + config=config, + env=env, + events=events, + get_attr=get_attr, + options=documenter.options, + ) + if props is None: logger.warning( __('failed to import object %s'), real_name, @@ -382,7 +400,7 @@ def get_items(self, names: list[str]) -> list[tuple[str, str | None, str, str]]: ) items.append((display_name, '', '', real_name)) continue - props = documenter.props + documenter.props = props # try to also get a source code analyzer for attribute docs real_module = props._obj___module__ or props.module_name diff --git a/tests/test_ext_autodoc/autodoc_util.py b/tests/test_ext_autodoc/autodoc_util.py index ed92f1354a7..6e9a086341f 100644 --- a/tests/test_ext_autodoc/autodoc_util.py +++ b/tests/test_ext_autodoc/autodoc_util.py @@ -10,6 +10,7 @@ # NEVER import those objects from sphinx.ext.autodoc directly from sphinx.ext.autodoc.directive import DocumenterBridge +from sphinx.ext.autodoc.importer import _load_object_by_name from sphinx.util.docutils import LoggingReporter from sphinx.util.inspect import safe_getattr @@ -43,5 +44,19 @@ def do_autodoc( app.env, LoggingReporter(''), docoptions, 1, state, safe_getattr ) documenter = doccls(bridge, name) - documenter.generate() + props = _load_object_by_name( + name=name, + objtype=obj_type, + mock_imports=app.config.autodoc_mock_imports, + type_aliases=app.config.autodoc_type_aliases, + current_document=app.env.current_document, + config=app.config, + env=app.env, + events=app.events, + get_attr=safe_getattr, + options=documenter.options, + ) + if props is not None: + documenter.props = props + documenter._generate() return bridge.result diff --git a/tests/test_ext_autodoc/test_ext_autodoc.py b/tests/test_ext_autodoc/test_ext_autodoc.py index 9deab74123f..5238d5be583 100644 --- a/tests/test_ext_autodoc/test_ext_autodoc.py +++ b/tests/test_ext_autodoc/test_ext_autodoc.py @@ -28,7 +28,11 @@ ) from sphinx.ext.autodoc._sentinels import ALL from sphinx.ext.autodoc.directive import DocumenterBridge -from sphinx.ext.autodoc.importer import _format_signatures, _parse_name +from sphinx.ext.autodoc.importer import ( + _format_signatures, + _load_object_by_name, + _parse_name, +) from sphinx.util.inspect import safe_getattr from tests.test_ext_autodoc.autodoc_util import do_autodoc @@ -594,8 +598,23 @@ def _special_getattr(obj, attr_name, *defargs): def _assert_getter_works(app, directive, objtype, name, *attrs): getattr_spy.clear() - doccls = app.registry.documenters[objtype](directive, name) - doccls.generate() + doccls = app.registry.documenters[objtype] + documenter = doccls(directive, name) + props = _load_object_by_name( + name=name, + objtype=objtype, + mock_imports=app.config.autodoc_mock_imports, + type_aliases=app.config.autodoc_type_aliases, + current_document=app.env.current_document, + config=app.config, + env=app.env, + events=app.events, + get_attr=directive.get_attr, + options=documenter.options, + ) + if props is not None: + documenter.props = props + documenter._generate() hooked_members = {s[1] for s in getattr_spy} documented_members = {s[1] for s in processed_signatures} From 6faded52de89c88ff71a87fb85e7ac766ce52c57 Mon Sep 17 00:00:00 2001 From: Adam Turner <9087854+aa-turner@users.noreply.github.com> Date: Mon, 20 Oct 2025 17:25:05 +0100 Subject: [PATCH 2/3] Delete ``Documenter.generate()`` --- sphinx/ext/autodoc/_documenters.py | 29 ++--------------------------- 1 file changed, 2 insertions(+), 27 deletions(-) diff --git a/sphinx/ext/autodoc/_documenters.py b/sphinx/ext/autodoc/_documenters.py index f05a2e48784..3bb09d24fc2 100644 --- a/sphinx/ext/autodoc/_documenters.py +++ b/sphinx/ext/autodoc/_documenters.py @@ -253,14 +253,14 @@ def _assemble_more_content( return more_content - def generate( + def _generate( self, more_content: StringList | None = None, real_modname: str | None = None, check_module: bool = False, all_members: bool = False, ) -> None: - """Generate reST for the object given by *self.orig_name*, and possibly for + """Generate reST for the object given by *self.props*, and possibly for its members. If *more_content* is given, include that content. If *real_modname* is @@ -268,31 +268,6 @@ def generate( True, only generate if the object is defined in the module name it is imported from. If *all_members* is True, document all members. """ - props = _load_object_by_name( - name=self.orig_name, - objtype=self.objtype, # type: ignore[arg-type] - mock_imports=self.config.autodoc_mock_imports, - type_aliases=self.config.autodoc_type_aliases, - current_document=self._current_document, - config=self.config, - env=self.env, - events=self._events, - get_attr=self.get_attr, - options=self.options, - ) - if props is None: - return - self.props = props - - self._generate(more_content, real_modname, check_module, all_members) - - def _generate( - self, - more_content: StringList | None = None, - real_modname: str | None = None, - check_module: bool = False, - all_members: bool = False, - ) -> None: if self.props.obj_type in {'class', 'exception'}: # Do not pass real_modname and use the name from the __module__ # attribute of the class. From 4e1bbf8f63f09121396e2849db3476ba24f33e9c Mon Sep 17 00:00:00 2001 From: Adam Turner <9087854+aa-turner@users.noreply.github.com> Date: Mon, 20 Oct 2025 17:25:57 +0100 Subject: [PATCH 3/3] fixup! Delete ``Documenter.generate()`` --- sphinx/ext/autodoc/_documenters.py | 1 - 1 file changed, 1 deletion(-) diff --git a/sphinx/ext/autodoc/_documenters.py b/sphinx/ext/autodoc/_documenters.py index 3bb09d24fc2..24c7980b045 100644 --- a/sphinx/ext/autodoc/_documenters.py +++ b/sphinx/ext/autodoc/_documenters.py @@ -19,7 +19,6 @@ from sphinx.ext.autodoc._docstrings import _prepare_docstrings, _process_docstrings from sphinx.ext.autodoc._member_finder import _document_members from sphinx.ext.autodoc._renderer import _add_content, _directive_header_lines -from sphinx.ext.autodoc.importer import _load_object_by_name from sphinx.ext.autodoc.mock import ismock from sphinx.locale import _, __ from sphinx.pycode import ModuleAnalyzer