Fix #8800: autodoc: Uninitialized attributes in superclass are recognized as undocumented

Unintentionally, uninitialized attributes defined at superclasses are
recognized as undocumented in the filtering step.  Therefore, they are
filtered if `:undoc-members:` option given.
This commit is contained in:
Takeshi KOMIYA 2021-01-31 19:21:15 +09:00
parent 7ca279e33a
commit 6d8c9183fa
4 changed files with 97 additions and 9 deletions

View File

@ -77,6 +77,8 @@ Bugs fixed
contains invalid type comments contains invalid type comments
* #8693: autodoc: Default values for overloaded functions are rendered as string * #8693: autodoc: Default values for overloaded functions are rendered as string
* #8134: autodoc: crashes when mocked decorator takes arguments * #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 * #8306: autosummary: mocked modules are documented as empty page when using
:recursive: option :recursive: option
* #8232: graphviz: Image node is not rendered if graph file is in subdirectory * #8232: graphviz: Image node is not rendered if graph file is in subdirectory

View File

@ -294,24 +294,35 @@ def get_class_members(subject: Any, objpath: List[str], attrgetter: Callable
try: try:
for cls in getmro(subject): 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: try:
modname = safe_getattr(cls, '__module__') modname = safe_getattr(cls, '__module__')
qualname = safe_getattr(cls, '__qualname__') qualname = safe_getattr(cls, '__qualname__')
analyzer = ModuleAnalyzer.for_module(modname) analyzer = ModuleAnalyzer.for_module(modname)
analyzer.analyze() 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(): for (ns, name), docstring in analyzer.attr_docs.items():
if ns == qualname and name not in members: if ns == qualname and name not in members:
members[name] = ObjectMember(name, INSTANCEATTR, class_=cls, members[name] = ObjectMember(name, INSTANCEATTR, class_=cls,
docstring='\n'.join(docstring)) docstring='\n'.join(docstring))
except (AttributeError, PycodeError):
pass
except AttributeError: except AttributeError:
pass pass

View File

@ -0,0 +1,8 @@
class Base:
attr1: int #: docstring
attr2: str
class Derived(Base):
attr3: int #: docstring
attr4: str

View File

@ -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): def test_decorators(app):
actual = do_autodoc(app, 'class', 'target.decorator.Baz') actual = do_autodoc(app, 'class', 'target.decorator.Baz')
assert list(actual) == [ assert list(actual) == [