From 277aba935d1fc50f823ca9a72047d4556e5d8717 Mon Sep 17 00:00:00 2001 From: Takeshi KOMIYA Date: Wed, 22 May 2019 02:20:46 +0900 Subject: [PATCH] Close #6325: autodoc: Support attributes in __slots__ --- CHANGES | 2 + sphinx/ext/autodoc/__init__.py | 51 ++++++++++++++++++++ sphinx/ext/autodoc/importer.py | 9 +++- tests/roots/test-ext-autodoc/target/slots.py | 11 +++++ tests/test_autodoc.py | 40 +++++++++++++++ 5 files changed, 112 insertions(+), 1 deletion(-) create mode 100644 tests/roots/test-ext-autodoc/target/slots.py diff --git a/CHANGES b/CHANGES index 2040dac035b..961c91fe53d 100644 --- a/CHANGES +++ b/CHANGES @@ -83,6 +83,8 @@ Features added ``imported-members`` option * #4777: autodoc: Support coroutine * #744: autodoc: Support abstractmethod +* #6325: autodoc: Support attributes in __slots__. For dict-style __slots__, + autodoc considers values as a docstring of the attribute * #6212 autosummary: Add :confval:`autosummary_imported_members` to display imported members on autosummary * #6271: ``make clean`` is catastrophically broken if building into '.' diff --git a/sphinx/ext/autodoc/__init__.py b/sphinx/ext/autodoc/__init__.py index 3048b8e481a..65a292b6a91 100644 --- a/sphinx/ext/autodoc/__init__.py +++ b/sphinx/ext/autodoc/__init__.py @@ -67,6 +67,7 @@ def identity(x): ALL = object() INSTANCEATTR = object() +SLOTSATTR = object() def members_option(arg): @@ -1493,6 +1494,55 @@ def add_content(self, more_content, no_docstring=False): super().add_content(more_content, no_docstring=True) +class SlotsAttributeDocumenter(AttributeDocumenter): + """ + Specialized Documenter subclass for attributes that cannot be imported + because they are attributes in __slots__. + """ + objtype = 'slotsattribute' + directivetype = 'attribute' + member_order = 60 + + # must be higher than AttributeDocumenter + priority = 11 + + @classmethod + def can_document_member(cls, member, membername, isattr, parent): + # type: (Any, str, bool, Any) -> bool + """This documents only SLOTSATTR members.""" + return member is SLOTSATTR + + def import_object(self): + # type: () -> bool + """Never import anything.""" + # disguise as an attribute + self.objtype = 'attribute' + self._datadescriptor = True + + with mock(self.env.config.autodoc_mock_imports): + try: + ret = import_object(self.modname, self.objpath[:-1], 'class', + attrgetter=self.get_attr, + warningiserror=self.env.config.autodoc_warningiserror) + self.module, _, _, self.parent = ret + return True + except ImportError as exc: + logger.warning(exc.args[0], type='autodoc', subtype='import_object') + self.env.note_reread() + return False + + def get_doc(self, encoding=None, ignore=1): + # type: (str, int) -> List[List[str]] + """Decode and return lines of the docstring(s) for the object.""" + name = self.objpath[-1] + __slots__ = safe_getattr(self.parent, '__slots__', []) + if isinstance(__slots__, dict) and isinstance(__slots__.get(name), str): + docstring = prepare_docstring(__slots__[name]) + return [docstring] + else: + return [] + + def get_documenters(app): # type: (Sphinx) -> Dict[str, Type[Documenter]] """Returns registered Documenter classes""" @@ -1554,6 +1604,7 @@ def setup(app): app.add_autodocumenter(AttributeDocumenter) app.add_autodocumenter(PropertyDocumenter) app.add_autodocumenter(InstanceAttributeDocumenter) + app.add_autodocumenter(SlotsAttributeDocumenter) app.add_config_value('autoclass_content', 'class', True) app.add_config_value('autodoc_member_order', 'alphabetic', True) diff --git a/sphinx/ext/autodoc/importer.py b/sphinx/ext/autodoc/importer.py index a06d93d5260..37bdae8a94c 100644 --- a/sphinx/ext/autodoc/importer.py +++ b/sphinx/ext/autodoc/importer.py @@ -15,7 +15,7 @@ from sphinx.deprecation import RemovedInSphinx40Warning, deprecated_alias from sphinx.util import logging -from sphinx.util.inspect import isenumclass, safe_getattr +from sphinx.util.inspect import isclass, isenumclass, safe_getattr if False: # For type annotation @@ -127,6 +127,13 @@ def get_object_members(subject, objpath, attrgetter, analyzer=None): if name not in superclass.__dict__: members[name] = Attribute(name, True, value) + # members in __slots__ + if isclass(subject) and hasattr(subject, '__slots__'): + from sphinx.ext.autodoc import SLOTSATTR + + for name in subject.__slots__: + members[name] = Attribute(name, True, SLOTSATTR) + # other members for name in dir(subject): try: diff --git a/tests/roots/test-ext-autodoc/target/slots.py b/tests/roots/test-ext-autodoc/target/slots.py new file mode 100644 index 00000000000..44e7503205f --- /dev/null +++ b/tests/roots/test-ext-autodoc/target/slots.py @@ -0,0 +1,11 @@ +class Foo: + __slots__ = ['attr'] + + +class Bar: + __slots__ = {'attr1': 'docstring of attr1', + 'attr2': 'docstring of attr2', + 'attr3': None} + + def __init__(self): + self.attr2 = None #: docstring of instance attr2 diff --git a/tests/test_autodoc.py b/tests/test_autodoc.py index 3d3a5943c82..8f08bb7fa12 100644 --- a/tests/test_autodoc.py +++ b/tests/test_autodoc.py @@ -1320,6 +1320,46 @@ def test_instance_attributes(app): ] +@pytest.mark.sphinx('html', testroot='ext-autodoc') +def test_slots(app): + options = {"members": None, + "undoc-members": True} + actual = do_autodoc(app, 'module', 'target.slots', options) + assert list(actual) == [ + '', + '.. py:module:: target.slots', + '', + '', + '.. py:class:: Bar()', + ' :module: target.slots', + '', + ' ', + ' .. py:attribute:: Bar.attr1', + ' :module: target.slots', + ' ', + ' docstring of attr1', + ' ', + ' ', + ' .. py:attribute:: Bar.attr2', + ' :module: target.slots', + ' ', + ' docstring of instance attr2', + ' ', + ' ', + ' .. py:attribute:: Bar.attr3', + ' :module: target.slots', + ' ', + '', + '.. py:class:: Foo', + ' :module: target.slots', + '', + ' ', + ' .. py:attribute:: Foo.attr', + ' :module: target.slots', + ' ', + ] + + @pytest.mark.sphinx('html', testroot='ext-autodoc') def test_enum_class(app): options = {"members": None,