diff --git a/CHANGES b/CHANGES index 32cb7b4057b..035a4f2a496 100644 --- a/CHANGES +++ b/CHANGES @@ -59,6 +59,8 @@ Bugs fixed undocumented * #9185: autodoc: typehints for overloaded functions and methods are inaccurate * #9250: autodoc: The inherited method not having docstring is wrongly parsed +* #9283: autodoc: autoattribute directive failed to generate document for an + attribute not having any comment * #9270: html theme : pyramid theme generates incorrect logo links * #9217: manpage: The name of manpage directory that is generated by :confval:`man_make_section_directory` is not correct diff --git a/sphinx/ext/autodoc/__init__.py b/sphinx/ext/autodoc/__init__.py index ec1472e202c..7cf06752d7a 100644 --- a/sphinx/ext/autodoc/__init__.py +++ b/sphinx/ext/autodoc/__init__.py @@ -2356,9 +2356,29 @@ def is_runtime_instance_attribute(self, parent: Any) -> bool: # An instance variable defined in __init__(). if self.get_attribute_comment(parent, self.objpath[-1]): # type: ignore return True + elif self.is_runtime_instance_attribute_not_commented(parent): + return True else: return False + def is_runtime_instance_attribute_not_commented(self, parent: Any) -> bool: + """Check the subject is an attribute defined in __init__() without comment.""" + for cls in inspect.getmro(parent): + try: + module = safe_getattr(cls, '__module__') + qualname = safe_getattr(cls, '__qualname__') + + analyzer = ModuleAnalyzer.for_module(module) + analyzer.analyze() + if qualname and self.objpath: + key = '.'.join([qualname, self.objpath[-1]]) + if key in analyzer.tagorder: + return True + except (AttributeError, PycodeError): + pass + + return None + def import_object(self, raiseerror: bool = False) -> bool: """Check the existence of runtime instance attribute when failed to import the attribute.""" @@ -2389,6 +2409,13 @@ def should_suppress_value_header(self) -> bool: return (self.object is self.RUNTIME_INSTANCE_ATTRIBUTE or super().should_suppress_value_header()) + def get_doc(self, ignore: int = None) -> Optional[List[List[str]]]: + if (self.object is self.RUNTIME_INSTANCE_ATTRIBUTE and + self.is_runtime_instance_attribute_not_commented(self.parent)): + return None + else: + return super().get_doc(ignore) # type: ignore + class UninitializedInstanceAttributeMixin(DataDocumenterMixinBase): """ diff --git a/tests/roots/test-ext-autodoc/target/instance_variable.py b/tests/roots/test-ext-autodoc/target/instance_variable.py index ae86d1edb1e..1d393bc8743 100644 --- a/tests/roots/test-ext-autodoc/target/instance_variable.py +++ b/tests/roots/test-ext-autodoc/target/instance_variable.py @@ -8,3 +8,4 @@ class Bar(Foo): def __init__(self): self.attr2 = None #: docstring bar self.attr3 = None #: docstring bar + self.attr4 = None diff --git a/tests/test_ext_autodoc_autoattribute.py b/tests/test_ext_autodoc_autoattribute.py index 5e72202342b..20317b8da39 100644 --- a/tests/test_ext_autodoc_autoattribute.py +++ b/tests/test_ext_autodoc_autoattribute.py @@ -100,6 +100,17 @@ def test_autoattribute_instance_variable_in_alias(app): ] +@pytest.mark.sphinx('html', testroot='ext-autodoc') +def test_autoattribute_instance_variable_without_comment(app): + actual = do_autodoc(app, 'attribute', 'target.instance_variable.Bar.attr4') + assert list(actual) == [ + '', + '.. py:attribute:: Bar.attr4', + ' :module: target.instance_variable', + '', + ] + + @pytest.mark.sphinx('html', testroot='ext-autodoc') def test_autoattribute_slots_variable_list(app): actual = do_autodoc(app, 'attribute', 'target.slots.Foo.attr')