Skip to content

Commit 3edae68

Browse files
autosummary: Support documenting inherited attributes (#10691)
The current implementation of ``import_ivar_by_name`` filters attributes if the name of the object that the attribute belongs to does not match the object being documented. However, for inherited attributes this is not the case. Filtering only on the attribute name seems to resolve the issue. It is not clear to me if there are any unwanted sideeffects of this and we should filter on the list of qualnames for the object and all its super classes (if any). Co-authored-by: Adam Turner <[email protected]>
1 parent 7ecf037 commit 3edae68

File tree

4 files changed

+86
-8
lines changed

4 files changed

+86
-8
lines changed

sphinx/ext/autosummary/__init__.py

+17-7
Original file line numberDiff line numberDiff line change
@@ -88,7 +88,7 @@
8888
new_document,
8989
switch_source_input,
9090
)
91-
from sphinx.util.inspect import signature_from_str
91+
from sphinx.util.inspect import getmro, signature_from_str
9292
from sphinx.util.matching import Matcher
9393
from sphinx.util.typing import OptionSpec
9494
from sphinx.writers.html import HTML5Translator
@@ -715,12 +715,22 @@ def import_ivar_by_name(name: str, prefixes: list[str | None] = [None],
715715
try:
716716
name, attr = name.rsplit(".", 1)
717717
real_name, obj, parent, modname = import_by_name(name, prefixes, grouped_exception)
718-
qualname = real_name.replace(modname + ".", "")
719-
analyzer = ModuleAnalyzer.for_module(getattr(obj, '__module__', modname))
720-
analyzer.analyze()
721-
# check for presence in `annotations` to include dataclass attributes
722-
if (qualname, attr) in analyzer.attr_docs or (qualname, attr) in analyzer.annotations:
723-
return real_name + "." + attr, INSTANCEATTR, obj, modname
718+
719+
# Get ancestors of the object (class.__mro__ includes the class itself as
720+
# the first entry)
721+
candidate_objects = getmro(obj)
722+
if len(candidate_objects) == 0:
723+
candidate_objects = (obj,)
724+
725+
for candidate_obj in candidate_objects:
726+
analyzer = ModuleAnalyzer.for_module(getattr(candidate_obj, '__module__', modname))
727+
analyzer.analyze()
728+
# check for presence in `annotations` to include dataclass attributes
729+
found_attrs = set()
730+
found_attrs |= {attr for (qualname, attr) in analyzer.attr_docs}
731+
found_attrs |= {attr for (qualname, attr) in analyzer.annotations}
732+
if attr in found_attrs:
733+
return real_name + "." + attr, INSTANCEATTR, obj, modname
724734
except (ImportError, ValueError, PycodeError) as exc:
725735
raise ImportError from exc
726736
except ImportExceptionGroup:
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
from autosummary_dummy_module import Foo
2+
3+
4+
class InheritedAttrClass(Foo):
5+
6+
def __init__(self):
7+
#: other docstring
8+
self.subclassattr = "subclassattr"
9+
10+
super().__init__()
11+
12+
13+
__all__ = ["InheritedAttrClass"]

tests/roots/test-ext-autosummary/index.rst

+2
Original file line numberDiff line numberDiff line change
@@ -13,4 +13,6 @@
1313
autosummary_dummy_module.Foo.value
1414
autosummary_dummy_module.bar
1515
autosummary_dummy_module.qux
16+
autosummary_dummy_inherited_module.InheritedAttrClass
17+
autosummary_dummy_inherited_module.InheritedAttrClass.subclassattr
1618
autosummary_importfail

tests/test_ext_autosummary.py

+54-1
Original file line numberDiff line numberDiff line change
@@ -319,6 +319,33 @@ def test_autosummary_generate_content_for_module_imported_members(app):
319319
assert context['objtype'] == 'module'
320320

321321

322+
@pytest.mark.sphinx(testroot='ext-autosummary')
323+
def test_autosummary_generate_content_for_module_imported_members_inherited_module(app):
324+
import autosummary_dummy_inherited_module
325+
template = Mock()
326+
327+
generate_autosummary_content('autosummary_dummy_inherited_module',
328+
autosummary_dummy_inherited_module, None,
329+
template, None, True, app, False, {})
330+
assert template.render.call_args[0][0] == 'module'
331+
332+
context = template.render.call_args[0][1]
333+
assert context['members'] == ['Foo', 'InheritedAttrClass', '__all__', '__builtins__', '__cached__',
334+
'__doc__', '__file__', '__loader__', '__name__',
335+
'__package__', '__spec__']
336+
assert context['functions'] == []
337+
assert context['classes'] == ['Foo', 'InheritedAttrClass']
338+
assert context['exceptions'] == []
339+
assert context['all_exceptions'] == []
340+
assert context['attributes'] == []
341+
assert context['all_attributes'] == []
342+
assert context['fullname'] == 'autosummary_dummy_inherited_module'
343+
assert context['module'] == 'autosummary_dummy_inherited_module'
344+
assert context['objname'] == ''
345+
assert context['name'] == ''
346+
assert context['objtype'] == 'module'
347+
348+
322349
@pytest.mark.sphinx('dummy', testroot='ext-autosummary')
323350
def test_autosummary_generate(app, status, warning):
324351
app.builder.build_all()
@@ -337,16 +364,20 @@ def test_autosummary_generate(app, status, warning):
337364
nodes.row,
338365
nodes.row,
339366
nodes.row,
367+
nodes.row,
368+
nodes.row,
340369
nodes.row)])])
341370
assert_node(doctree[4][0], addnodes.toctree, caption="An autosummary")
342371

343-
assert len(doctree[3][0][0][2]) == 6
372+
assert len(doctree[3][0][0][2]) == 8
344373
assert doctree[3][0][0][2][0].astext() == 'autosummary_dummy_module\n\n'
345374
assert doctree[3][0][0][2][1].astext() == 'autosummary_dummy_module.Foo()\n\n'
346375
assert doctree[3][0][0][2][2].astext() == 'autosummary_dummy_module.Foo.Bar()\n\n'
347376
assert doctree[3][0][0][2][3].astext() == 'autosummary_dummy_module.Foo.value\n\ndocstring'
348377
assert doctree[3][0][0][2][4].astext() == 'autosummary_dummy_module.bar(x[, y])\n\n'
349378
assert doctree[3][0][0][2][5].astext() == 'autosummary_dummy_module.qux\n\na module-level attribute'
379+
assert doctree[3][0][0][2][6].astext() == 'autosummary_dummy_inherited_module.InheritedAttrClass()\n\n'
380+
assert doctree[3][0][0][2][7].astext() == 'autosummary_dummy_inherited_module.InheritedAttrClass.subclassattr\n\nother docstring'
350381

351382
module = (app.srcdir / 'generated' / 'autosummary_dummy_module.rst').read_text(encoding='utf8')
352383

@@ -392,6 +423,28 @@ def test_autosummary_generate(app, status, warning):
392423
'\n'
393424
'.. autodata:: qux' in qux)
394425

426+
InheritedAttrClass = (app.srcdir / 'generated' / 'autosummary_dummy_inherited_module.InheritedAttrClass.rst').read_text(encoding='utf8')
427+
print(InheritedAttrClass)
428+
assert '.. automethod:: __init__' in Foo
429+
assert (' .. autosummary::\n'
430+
' \n'
431+
' ~InheritedAttrClass.__init__\n'
432+
' ~InheritedAttrClass.bar\n'
433+
' \n' in InheritedAttrClass)
434+
assert (' .. autosummary::\n'
435+
' \n'
436+
' ~InheritedAttrClass.CONSTANT3\n'
437+
' ~InheritedAttrClass.CONSTANT4\n'
438+
' ~InheritedAttrClass.baz\n'
439+
' ~InheritedAttrClass.subclassattr\n'
440+
' ~InheritedAttrClass.value\n'
441+
' \n' in InheritedAttrClass)
442+
443+
InheritedAttrClass_subclassattr = (app.srcdir / 'generated' / 'autosummary_dummy_inherited_module.InheritedAttrClass.subclassattr.rst').read_text(encoding='utf8')
444+
assert ('.. currentmodule:: autosummary_dummy_inherited_module\n'
445+
'\n'
446+
'.. autoattribute:: InheritedAttrClass.subclassattr' in InheritedAttrClass_subclassattr)
447+
395448

396449
@pytest.mark.sphinx('dummy', testroot='ext-autosummary',
397450
confoverrides={'autosummary_generate_overwrite': False})

0 commit comments

Comments
 (0)