diff --git a/sphinx/ext/autosummary/__init__.py b/sphinx/ext/autosummary/__init__.py index 097b8cc80..7f458b8bf 100644 --- a/sphinx/ext/autosummary/__init__.py +++ b/sphinx/ext/autosummary/__init__.py @@ -88,7 +88,7 @@ from sphinx.util.docutils import ( new_document, switch_source_input, ) -from sphinx.util.inspect import signature_from_str +from sphinx.util.inspect import getmro, signature_from_str from sphinx.util.matching import Matcher from sphinx.util.typing import OptionSpec from sphinx.writers.html import HTML5Translator @@ -715,12 +715,22 @@ def import_ivar_by_name(name: str, prefixes: list[str | None] = [None], try: name, attr = name.rsplit(".", 1) real_name, obj, parent, modname = import_by_name(name, prefixes, grouped_exception) - qualname = real_name.replace(modname + ".", "") - analyzer = ModuleAnalyzer.for_module(getattr(obj, '__module__', modname)) - analyzer.analyze() - # check for presence in `annotations` to include dataclass attributes - if (qualname, attr) in analyzer.attr_docs or (qualname, attr) in analyzer.annotations: - return real_name + "." + attr, INSTANCEATTR, obj, modname + + # Get ancestors of the object (class.__mro__ includes the class itself as + # the first entry) + candidate_objects = getmro(obj) + if len(candidate_objects) == 0: + candidate_objects = (obj,) + + for candidate_obj in candidate_objects: + analyzer = ModuleAnalyzer.for_module(getattr(candidate_obj, '__module__', modname)) + analyzer.analyze() + # check for presence in `annotations` to include dataclass attributes + found_attrs = set() + found_attrs |= {attr for (qualname, attr) in analyzer.attr_docs} + found_attrs |= {attr for (qualname, attr) in analyzer.annotations} + if attr in found_attrs: + return real_name + "." + attr, INSTANCEATTR, obj, modname except (ImportError, ValueError, PycodeError) as exc: raise ImportError from exc except ImportExceptionGroup: diff --git a/tests/roots/test-ext-autosummary/autosummary_dummy_inherited_module.py b/tests/roots/test-ext-autosummary/autosummary_dummy_inherited_module.py new file mode 100644 index 000000000..2b3d2da84 --- /dev/null +++ b/tests/roots/test-ext-autosummary/autosummary_dummy_inherited_module.py @@ -0,0 +1,13 @@ +from autosummary_dummy_module import Foo + + +class InheritedAttrClass(Foo): + + def __init__(self): + #: other docstring + self.subclassattr = "subclassattr" + + super().__init__() + + +__all__ = ["InheritedAttrClass"] diff --git a/tests/roots/test-ext-autosummary/index.rst b/tests/roots/test-ext-autosummary/index.rst index 904c5fdcb..08bd0f093 100644 --- a/tests/roots/test-ext-autosummary/index.rst +++ b/tests/roots/test-ext-autosummary/index.rst @@ -13,4 +13,6 @@ autosummary_dummy_module.Foo.value autosummary_dummy_module.bar autosummary_dummy_module.qux + autosummary_dummy_inherited_module.InheritedAttrClass + autosummary_dummy_inherited_module.InheritedAttrClass.subclassattr autosummary_importfail diff --git a/tests/test_ext_autosummary.py b/tests/test_ext_autosummary.py index 69b4b76bc..7226a1167 100644 --- a/tests/test_ext_autosummary.py +++ b/tests/test_ext_autosummary.py @@ -319,6 +319,33 @@ def test_autosummary_generate_content_for_module_imported_members(app): assert context['objtype'] == 'module' +@pytest.mark.sphinx(testroot='ext-autosummary') +def test_autosummary_generate_content_for_module_imported_members_inherited_module(app): + import autosummary_dummy_inherited_module + template = Mock() + + generate_autosummary_content('autosummary_dummy_inherited_module', + autosummary_dummy_inherited_module, None, + template, None, True, app, False, {}) + assert template.render.call_args[0][0] == 'module' + + context = template.render.call_args[0][1] + assert context['members'] == ['Foo', 'InheritedAttrClass', '__all__', '__builtins__', '__cached__', + '__doc__', '__file__', '__loader__', '__name__', + '__package__', '__spec__'] + assert context['functions'] == [] + assert context['classes'] == ['Foo', 'InheritedAttrClass'] + assert context['exceptions'] == [] + assert context['all_exceptions'] == [] + assert context['attributes'] == [] + assert context['all_attributes'] == [] + assert context['fullname'] == 'autosummary_dummy_inherited_module' + assert context['module'] == 'autosummary_dummy_inherited_module' + assert context['objname'] == '' + assert context['name'] == '' + assert context['objtype'] == 'module' + + @pytest.mark.sphinx('dummy', testroot='ext-autosummary') def test_autosummary_generate(app, status, warning): app.builder.build_all() @@ -337,16 +364,20 @@ def test_autosummary_generate(app, status, warning): nodes.row, 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]) == 6 + assert len(doctree[3][0][0][2]) == 8 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.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' + assert doctree[3][0][0][2][6].astext() == 'autosummary_dummy_inherited_module.InheritedAttrClass()\n\n' + assert doctree[3][0][0][2][7].astext() == 'autosummary_dummy_inherited_module.InheritedAttrClass.subclassattr\n\nother docstring' module = (app.srcdir / 'generated' / 'autosummary_dummy_module.rst').read_text(encoding='utf8') @@ -392,6 +423,28 @@ def test_autosummary_generate(app, status, warning): '\n' '.. autodata:: qux' in qux) + InheritedAttrClass = (app.srcdir / 'generated' / 'autosummary_dummy_inherited_module.InheritedAttrClass.rst').read_text(encoding='utf8') + print(InheritedAttrClass) + assert '.. automethod:: __init__' in Foo + assert (' .. autosummary::\n' + ' \n' + ' ~InheritedAttrClass.__init__\n' + ' ~InheritedAttrClass.bar\n' + ' \n' in InheritedAttrClass) + assert (' .. autosummary::\n' + ' \n' + ' ~InheritedAttrClass.CONSTANT3\n' + ' ~InheritedAttrClass.CONSTANT4\n' + ' ~InheritedAttrClass.baz\n' + ' ~InheritedAttrClass.subclassattr\n' + ' ~InheritedAttrClass.value\n' + ' \n' in InheritedAttrClass) + + InheritedAttrClass_subclassattr = (app.srcdir / 'generated' / 'autosummary_dummy_inherited_module.InheritedAttrClass.subclassattr.rst').read_text(encoding='utf8') + assert ('.. currentmodule:: autosummary_dummy_inherited_module\n' + '\n' + '.. autoattribute:: InheritedAttrClass.subclassattr' in InheritedAttrClass_subclassattr) + @pytest.mark.sphinx('dummy', testroot='ext-autosummary', confoverrides={'autosummary_generate_overwrite': False})