diff --git a/CHANGES b/CHANGES index ebdd331ed..0fb3c943b 100644 --- a/CHANGES +++ b/CHANGES @@ -77,6 +77,8 @@ Bugs fixed contains invalid type comments * #8693: autodoc: Default values for overloaded functions are rendered as string * #8134: autodoc: crashes when mocked decorator takes arguments +* #8800: autodoc: Uninitialized attributes in superclass are recognized as + undocumented * #8306: autosummary: mocked modules are documented as empty page when using :recursive: option * #8232: graphviz: Image node is not rendered if graph file is in subdirectory diff --git a/sphinx/ext/autodoc/importer.py b/sphinx/ext/autodoc/importer.py index 477aae247..b792b1a5f 100644 --- a/sphinx/ext/autodoc/importer.py +++ b/sphinx/ext/autodoc/importer.py @@ -294,24 +294,35 @@ def get_class_members(subject: Any, objpath: List[str], attrgetter: Callable try: for cls in getmro(subject): - # annotation only member (ex. attr: int) - for name in getannotations(cls): - name = unmangle(cls, name) - if name and name not in members: - members[name] = ObjectMember(name, INSTANCEATTR, class_=cls) - - # append instance attributes (cf. self.attr1) if analyzer knows try: modname = safe_getattr(cls, '__module__') qualname = safe_getattr(cls, '__qualname__') analyzer = ModuleAnalyzer.for_module(modname) analyzer.analyze() + except AttributeError: + qualname = None + analyzer = None + except PycodeError: + analyzer = None + + # annotation only member (ex. attr: int) + for name in getannotations(cls): + name = unmangle(cls, name) + if name and name not in members: + if analyzer and (qualname, name) in analyzer.attr_docs: + docstring = '\n'.join(analyzer.attr_docs[qualname, name]) + else: + docstring = None + + members[name] = ObjectMember(name, INSTANCEATTR, class_=cls, + docstring=docstring) + + # append instance attributes (cf. self.attr1) if analyzer knows + if analyzer: for (ns, name), docstring in analyzer.attr_docs.items(): if ns == qualname and name not in members: members[name] = ObjectMember(name, INSTANCEATTR, class_=cls, docstring='\n'.join(docstring)) - except (AttributeError, PycodeError): - pass except AttributeError: pass diff --git a/tests/roots/test-ext-autodoc/target/uninitialized_attributes.py b/tests/roots/test-ext-autodoc/target/uninitialized_attributes.py new file mode 100644 index 000000000..e0f229c76 --- /dev/null +++ b/tests/roots/test-ext-autodoc/target/uninitialized_attributes.py @@ -0,0 +1,8 @@ +class Base: + attr1: int #: docstring + attr2: str + + +class Derived(Base): + attr3: int #: docstring + attr4: str diff --git a/tests/test_ext_autodoc_autoclass.py b/tests/test_ext_autodoc_autoclass.py index 488b72263..74ddb02f9 100644 --- a/tests/test_ext_autodoc_autoclass.py +++ b/tests/test_ext_autodoc_autoclass.py @@ -106,6 +106,73 @@ def test_inherited_instance_variable(app): ] +@pytest.mark.skipif(sys.version_info < (3, 6), reason='py36+ is available since python3.6.') +@pytest.mark.sphinx('html', testroot='ext-autodoc') +def test_uninitialized_attributes(app): + options = {"members": None, + "inherited-members": True} + actual = do_autodoc(app, 'class', 'target.uninitialized_attributes.Derived', options) + assert list(actual) == [ + '', + '.. py:class:: Derived()', + ' :module: target.uninitialized_attributes', + '', + '', + ' .. py:attribute:: Derived.attr1', + ' :module: target.uninitialized_attributes', + ' :type: int', + '', + ' docstring', + '', + '', + ' .. py:attribute:: Derived.attr3', + ' :module: target.uninitialized_attributes', + ' :type: int', + '', + ' docstring', + '', + ] + + +@pytest.mark.skipif(sys.version_info < (3, 6), reason='py36+ is available since python3.6.') +@pytest.mark.sphinx('html', testroot='ext-autodoc') +def test_undocumented_uninitialized_attributes(app): + options = {"members": None, + "inherited-members": True, + "undoc-members": True} + actual = do_autodoc(app, 'class', 'target.uninitialized_attributes.Derived', options) + assert list(actual) == [ + '', + '.. py:class:: Derived()', + ' :module: target.uninitialized_attributes', + '', + '', + ' .. py:attribute:: Derived.attr1', + ' :module: target.uninitialized_attributes', + ' :type: int', + '', + ' docstring', + '', + '', + ' .. py:attribute:: Derived.attr2', + ' :module: target.uninitialized_attributes', + ' :type: str', + '', + '', + ' .. py:attribute:: Derived.attr3', + ' :module: target.uninitialized_attributes', + ' :type: int', + '', + ' docstring', + '', + '', + ' .. py:attribute:: Derived.attr4', + ' :module: target.uninitialized_attributes', + ' :type: str', + '', + ] + + def test_decorators(app): actual = do_autodoc(app, 'class', 'target.decorator.Baz') assert list(actual) == [