summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorJens Hedegaard Nielsen <Jens.Nielsen@microsoft.com>2023-04-07 00:33:43 +0200
committerGitHub <noreply@github.com>2023-04-06 23:33:43 +0100
commit3edae689040301e0b0833bb0321f265784874ff5 (patch)
tree6a68f81038d67d71d2777dbcaf5be38e2b282d1a
parent7ecf0372809825b97082a73587b3c27660db01a7 (diff)
downloadsphinx-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__.py24
-rw-r--r--tests/roots/test-ext-autosummary/autosummary_dummy_inherited_module.py13
-rw-r--r--tests/roots/test-ext-autosummary/index.rst2
-rw-r--r--tests/test_ext_autosummary.py55
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})