diff --git a/CHANGES b/CHANGES index d35544684c4..d53184ff406 100644 --- a/CHANGES +++ b/CHANGES @@ -20,6 +20,8 @@ Features added * #2076: autodoc: Allow overriding of exclude-members in skip-member function * #2024: autosummary: Add :confval:`autosummary_filename_map` to avoid conflict of filenames between two object with different case +* #8011: autosummary: Support instance attributes as a target of autosummary + directive * #7849: html: Add :confval:`html_codeblock_linenos_style` to change the style of line numbers for code-blocks * #7853: C and C++, support parameterized GNU style attributes. diff --git a/sphinx/ext/autosummary/__init__.py b/sphinx/ext/autosummary/__init__.py index 350604387b0..855affa9550 100644 --- a/sphinx/ext/autosummary/__init__.py +++ b/sphinx/ext/autosummary/__init__.py @@ -75,7 +75,7 @@ from sphinx.deprecation import RemovedInSphinx40Warning, RemovedInSphinx50Warning from sphinx.environment import BuildEnvironment from sphinx.environment.adapters.toctree import TocTree -from sphinx.ext.autodoc import Documenter +from sphinx.ext.autodoc import Documenter, INSTANCEATTR from sphinx.ext.autodoc.directive import DocumenterBridge, Options from sphinx.ext.autodoc.importer import import_module from sphinx.ext.autodoc.mock import mock @@ -285,6 +285,19 @@ def run(self) -> List[Node]: return nodes + def import_by_name(self, name: str, prefixes: List[str]) -> Tuple[str, Any, Any, str]: + with mock(self.config.autosummary_mock_imports): + try: + return import_by_name(name, prefixes) + except ImportError as exc: + # check existence of instance attribute + try: + return import_ivar_by_name(name, prefixes) + except ImportError: + pass + + raise exc # re-raise ImportError if instance attribute not found + def get_items(self, names: List[str]) -> List[Tuple[str, str, str, str]]: """Try to import the given names, and return a list of ``[(name, signature, summary_string, real_name), ...]``. @@ -302,8 +315,7 @@ def get_items(self, names: List[str]) -> List[Tuple[str, str, str, str]]: display_name = name.split('.')[-1] try: - with mock(self.config.autosummary_mock_imports): - real_name, obj, parent, modname = import_by_name(name, prefixes=prefixes) + real_name, obj, parent, modname = self.import_by_name(name, prefixes=prefixes) except ImportError: logger.warning(__('autosummary: failed to import %s'), name, location=self.get_source_info()) @@ -659,6 +671,23 @@ def _import_by_name(name: str) -> Tuple[Any, Any, str]: raise ImportError(*e.args) from e +def import_ivar_by_name(name: str, prefixes: List[str] = [None]) -> Tuple[str, Any, Any, str]: + """Import an instance variable that has the given *name*, under one of the + *prefixes*. The first name that succeeds is used. + """ + try: + name, attr = name.rsplit(".", 1) + real_name, obj, parent, modname = import_by_name(name, prefixes) + qualname = real_name.replace(modname + ".", "") + analyzer = ModuleAnalyzer.for_module(modname) + if (qualname, attr) in analyzer.find_attr_docs(): + return real_name + "." + attr, INSTANCEATTR, obj, modname + except (ImportError, ValueError): + pass + + raise ImportError + + # -- :autolink: (smart default role) ------------------------------------------- def autolink_role(typ: str, rawtext: str, etext: str, lineno: int, inliner: Inliner, diff --git a/sphinx/ext/autosummary/generate.py b/sphinx/ext/autosummary/generate.py index c1b50de577a..7580285aa69 100644 --- a/sphinx/ext/autosummary/generate.py +++ b/sphinx/ext/autosummary/generate.py @@ -41,7 +41,7 @@ from sphinx.config import Config from sphinx.deprecation import RemovedInSphinx40Warning, RemovedInSphinx50Warning from sphinx.ext.autodoc import Documenter -from sphinx.ext.autosummary import import_by_name, get_documenter +from sphinx.ext.autosummary import import_by_name, import_ivar_by_name, get_documenter from sphinx.locale import __ from sphinx.pycode import ModuleAnalyzer, PycodeError from sphinx.registry import SphinxComponentRegistry @@ -413,8 +413,13 @@ def generate_autosummary_docs(sources: List[str], output_dir: str = None, name, obj, parent, modname = import_by_name(entry.name) qualname = name.replace(modname + ".", "") except ImportError as e: - _warn(__('[autosummary] failed to import %r: %s') % (entry.name, e)) - continue + try: + # try to importl as an instance attribute + name, obj, parent, modname = import_ivar_by_name(entry.name) + qualname = name.replace(modname + ".", "") + except ImportError: + _warn(__('[autosummary] failed to import %r: %s') % (entry.name, e)) + continue context = {} if app: diff --git a/tests/roots/test-ext-autosummary/autosummary_dummy_module.py b/tests/roots/test-ext-autosummary/autosummary_dummy_module.py index 76158b6b92f..77eee8b5cdb 100644 --- a/tests/roots/test-ext-autosummary/autosummary_dummy_module.py +++ b/tests/roots/test-ext-autosummary/autosummary_dummy_module.py @@ -16,7 +16,8 @@ class Bar: pass def __init__(self): - pass + #: docstring + self.value = 1 def bar(self): pass diff --git a/tests/roots/test-ext-autosummary/index.rst b/tests/roots/test-ext-autosummary/index.rst index 9f657bb736d..904c5fdcbd2 100644 --- a/tests/roots/test-ext-autosummary/index.rst +++ b/tests/roots/test-ext-autosummary/index.rst @@ -10,6 +10,7 @@ autosummary_dummy_module autosummary_dummy_module.Foo autosummary_dummy_module.Foo.Bar + autosummary_dummy_module.Foo.value autosummary_dummy_module.bar autosummary_dummy_module.qux autosummary_importfail diff --git a/tests/test_ext_autosummary.py b/tests/test_ext_autosummary.py index 567a8caeaa5..96b39ce0ea0 100644 --- a/tests/test_ext_autosummary.py +++ b/tests/test_ext_autosummary.py @@ -293,15 +293,17 @@ def test_autosummary_generate(app, status, warning): nodes.row, nodes.row, nodes.row, + nodes.row, nodes.row)])]) assert_node(doctree[4][0], addnodes.toctree, caption="An autosummary") - assert len(doctree[3][0][0][2]) == 5 + assert len(doctree[3][0][0][2]) == 6 assert doctree[3][0][0][2][0].astext() == 'autosummary_dummy_module\n\n' assert doctree[3][0][0][2][1].astext() == 'autosummary_dummy_module.Foo()\n\n' assert doctree[3][0][0][2][2].astext() == 'autosummary_dummy_module.Foo.Bar()\n\n' - assert doctree[3][0][0][2][3].astext() == 'autosummary_dummy_module.bar(x[, y])\n\n' - assert doctree[3][0][0][2][4].astext() == 'autosummary_dummy_module.qux\n\na module-level attribute' + assert doctree[3][0][0][2][3].astext() == 'autosummary_dummy_module.Foo.value\n\ndocstring' + assert doctree[3][0][0][2][4].astext() == 'autosummary_dummy_module.bar(x[, y])\n\n' + assert doctree[3][0][0][2][5].astext() == 'autosummary_dummy_module.qux\n\na module-level attribute' module = (app.srcdir / 'generated' / 'autosummary_dummy_module.rst').read_text() assert (' .. autosummary::\n' @@ -333,6 +335,11 @@ def test_autosummary_generate(app, status, warning): '\n' '.. autoclass:: Foo.Bar\n' in FooBar) + Foo_value = (app.srcdir / 'generated' / 'autosummary_dummy_module.Foo.value.rst').read_text() + assert ('.. currentmodule:: autosummary_dummy_module\n' + '\n' + '.. autoattribute:: Foo.value' in Foo_value) + qux = (app.srcdir / 'generated' / 'autosummary_dummy_module.qux.rst').read_text() assert ('.. currentmodule:: autosummary_dummy_module\n' '\n'