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 <9087854+AA-Turner@users.noreply.github.com>
This commit is contained in:
Jens Hedegaard Nielsen 2023-04-07 00:33:43 +02:00 committed by GitHub
parent 7ecf037280
commit 3edae68904
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
4 changed files with 86 additions and 8 deletions

View File

@ -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:

View File

@ -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"]

View File

@ -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

View File

@ -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})