diff options
author | Jens Hedegaard Nielsen <Jens.Nielsen@microsoft.com> | 2023-04-07 00:33:43 +0200 |
---|---|---|
committer | GitHub <noreply@github.com> | 2023-04-06 23:33:43 +0100 |
commit | 3edae689040301e0b0833bb0321f265784874ff5 (patch) | |
tree | 6a68f81038d67d71d2777dbcaf5be38e2b282d1a | |
parent | 7ecf0372809825b97082a73587b3c27660db01a7 (diff) | |
download | sphinx-git-3edae689040301e0b0833bb0321f265784874ff5.tar.gz |
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>
-rw-r--r-- | sphinx/ext/autosummary/__init__.py | 24 | ||||
-rw-r--r-- | tests/roots/test-ext-autosummary/autosummary_dummy_inherited_module.py | 13 | ||||
-rw-r--r-- | tests/roots/test-ext-autosummary/index.rst | 2 | ||||
-rw-r--r-- | tests/test_ext_autosummary.py | 55 |
4 files changed, 86 insertions, 8 deletions
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}) |