diff --git a/CHANGES b/CHANGES index 8fa9f570735..4ced3e08f52 100644 --- a/CHANGES +++ b/CHANGES @@ -74,6 +74,7 @@ Bugs fixed information of TypeVars * #8493: autodoc: references to builtins not working in class aliases * #8522: autodoc: ``__bool__`` method could be called +* #8545: autodoc: a __slots__ attribute is not documented even having docstring * #8477: autosummary: non utf-8 reST files are generated when template contains multibyte characters * #8501: autosummary: summary extraction splits text after "el at." unexpectedly diff --git a/sphinx/ext/autodoc/__init__.py b/sphinx/ext/autodoc/__init__.py index fbf9fb7491b..2e540e07661 100644 --- a/sphinx/ext/autodoc/__init__.py +++ b/sphinx/ext/autodoc/__init__.py @@ -275,9 +275,11 @@ class ObjectMember(tuple): def __new__(cls, name: str, obj: Any, **kwargs: Any) -> Any: return super().__new__(cls, (name, obj)) # type: ignore - def __init__(self, name: str, obj: Any, skipped: bool = False) -> None: + def __init__(self, name: str, obj: Any, docstring: Optional[str] = None, + skipped: bool = False) -> None: self.__name__ = name self.object = obj + self.docstring = docstring self.skipped = skipped @@ -709,6 +711,11 @@ def is_filtered_inherited_member(name: str) -> bool: cls_doc = self.get_attr(cls, '__doc__', None) if cls_doc == doc: doc = None + + if isinstance(obj, ObjectMember) and obj.docstring: + # hack for ClassDocumenter to inject docstring via ObjectMember + doc = obj.docstring + has_doc = bool(doc) metadata = extract_metadata(doc) @@ -1585,16 +1592,18 @@ def get_object_members(self, want_all: bool) -> Tuple[bool, ObjectMembers]: selected = [] for name in self.options.members: # type: str if name in members: - selected.append((name, members[name].value)) + selected.append(ObjectMember(name, members[name].value, + docstring=members[name].docstring)) else: logger.warning(__('missing attribute %s in object %s') % (name, self.fullname), type='autodoc') return False, selected elif self.options.inherited_members: - return False, [(m.name, m.value) for m in members.values()] + return False, [ObjectMember(m.name, m.value, docstring=m.docstring) + for m in members.values()] else: - return False, [(m.name, m.value) for m in members.values() - if m.class_ == self.object] + return False, [ObjectMember(m.name, m.value, docstring=m.docstring) + for m in members.values() if m.class_ == self.object] def get_doc(self, encoding: str = None, ignore: int = None) -> List[List[str]]: if encoding is not None: diff --git a/sphinx/ext/autodoc/importer.py b/sphinx/ext/autodoc/importer.py index 9917fea9e69..17ad010ec0c 100644 --- a/sphinx/ext/autodoc/importer.py +++ b/sphinx/ext/autodoc/importer.py @@ -312,9 +312,10 @@ def get_class_members(subject: Any, objpath: List[str], attrgetter: Callable, if analyzer: # append instance attributes (cf. self.attr1) if analyzer knows namespace = '.'.join(objpath) - for (ns, name) in analyzer.find_attr_docs(): + for (ns, name), docstring in analyzer.attr_docs.items(): if namespace == ns and name not in members: - members[name] = ClassAttribute(subject, name, INSTANCEATTR) + members[name] = ClassAttribute(subject, name, INSTANCEATTR, + '\n'.join(docstring)) return members diff --git a/tests/roots/test-ext-autodoc/target/slots.py b/tests/roots/test-ext-autodoc/target/slots.py index 144f97c95a5..32822fd3816 100644 --- a/tests/roots/test-ext-autodoc/target/slots.py +++ b/tests/roots/test-ext-autodoc/target/slots.py @@ -1,8 +1,12 @@ class Foo: + """docstring""" + __slots__ = ['attr'] class Bar: + """docstring""" + __slots__ = {'attr1': 'docstring of attr1', 'attr2': 'docstring of attr2', 'attr3': None} @@ -12,4 +16,6 @@ def __init__(self): class Baz: + """docstring""" + __slots__ = 'attr' diff --git a/tests/test_ext_autodoc.py b/tests/test_ext_autodoc.py index 52f69dd2e7e..2ab6ba86787 100644 --- a/tests/test_ext_autodoc.py +++ b/tests/test_ext_autodoc.py @@ -1172,6 +1172,8 @@ def test_slots(app): '.. py:class:: Bar()', ' :module: target.slots', '', + ' docstring', + '', '', ' .. py:attribute:: Bar.attr1', ' :module: target.slots', @@ -1192,6 +1194,8 @@ def test_slots(app): '.. py:class:: Baz()', ' :module: target.slots', '', + ' docstring', + '', '', ' .. py:attribute:: Baz.attr', ' :module: target.slots', @@ -1200,6 +1204,8 @@ def test_slots(app): '.. py:class:: Foo()', ' :module: target.slots', '', + ' docstring', + '', '', ' .. py:attribute:: Foo.attr', ' :module: target.slots', diff --git a/tests/test_ext_autodoc_autoclass.py b/tests/test_ext_autodoc_autoclass.py index 17c7f894424..c36305de34c 100644 --- a/tests/test_ext_autodoc_autoclass.py +++ b/tests/test_ext_autodoc_autoclass.py @@ -77,6 +77,32 @@ def test_decorators(app): ] +@pytest.mark.sphinx('html', testroot='ext-autodoc') +def test_slots_attribute(app): + options = {"members": None} + actual = do_autodoc(app, 'class', 'target.slots.Bar', options) + assert list(actual) == [ + '', + '.. py:class:: Bar()', + ' :module: target.slots', + '', + ' docstring', + '', + '', + ' .. py:attribute:: Bar.attr1', + ' :module: target.slots', + '', + ' docstring of attr1', + '', + '', + ' .. py:attribute:: Bar.attr2', + ' :module: target.slots', + '', + ' docstring of instance attr2', + '', + ] + + @pytest.mark.skipif(sys.version_info < (3, 7), reason='python 3.7+ is required.') @pytest.mark.sphinx('html', testroot='ext-autodoc') def test_show_inheritance_for_subclass_of_generic_type(app):