Merge pull request #8546 from tk0miya/8545_slots_attributes_having_docstring

Fix #8545: autodoc: a __slots__ attribute is not documented even having docstring
This commit is contained in:
Takeshi KOMIYA 2020-12-17 22:34:58 +09:00 committed by GitHub
commit e3dc782364
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
6 changed files with 133 additions and 7 deletions

View File

@ -79,6 +79,7 @@ Bugs fixed
* #8522: autodoc: ``__bool__`` method could be called
* #8067: autodoc: A typehint for the instance variable having type_comment on
super class is not displayed
* #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

View File

@ -26,7 +26,8 @@ from sphinx.config import ENUM, Config
from sphinx.deprecation import (RemovedInSphinx40Warning, RemovedInSphinx50Warning,
RemovedInSphinx60Warning)
from sphinx.environment import BuildEnvironment
from sphinx.ext.autodoc.importer import get_module_members, get_object_members, import_object
from sphinx.ext.autodoc.importer import (get_class_members, get_module_members,
get_object_members, import_object)
from sphinx.ext.autodoc.mock import mock
from sphinx.locale import _, __
from sphinx.pycode import ModuleAnalyzer, PycodeError
@ -274,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
@ -708,6 +711,11 @@ class Documenter:
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)
@ -1576,7 +1584,7 @@ class ClassDocumenter(DocstringSignatureMixin, ModuleLevelDocumenter): # type:
self.add_line(' ' + _('Bases: %s') % ', '.join(bases), sourcename)
def get_object_members(self, want_all: bool) -> Tuple[bool, ObjectMembers]:
members = get_object_members(self.object, self.objpath, self.get_attr, self.analyzer)
members = get_class_members(self.object, self.objpath, self.get_attr, self.analyzer)
if not want_all:
if not self.options.members:
return False, [] # type: ignore
@ -1584,16 +1592,18 @@ class ClassDocumenter(DocstringSignatureMixin, ModuleLevelDocumenter): # type:
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.directly_defined]
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:

View File

@ -241,6 +241,83 @@ def get_object_members(subject: Any, objpath: List[str], attrgetter: Callable,
return members
class ClassAttribute:
"""The attribute of the class."""
def __init__(self, cls: Any, name: str, value: Any, docstring: Optional[str] = None):
self.class_ = cls
self.name = name
self.value = value
self.docstring = docstring
def get_class_members(subject: Any, objpath: List[str], attrgetter: Callable,
analyzer: ModuleAnalyzer = None) -> Dict[str, ClassAttribute]:
"""Get members and attributes of target class."""
from sphinx.ext.autodoc import INSTANCEATTR
# the members directly defined in the class
obj_dict = attrgetter(subject, '__dict__', {})
members = {} # type: Dict[str, ClassAttribute]
# enum members
if isenumclass(subject):
for name, value in subject.__members__.items():
if name not in members:
members[name] = ClassAttribute(subject, name, value)
superclass = subject.__mro__[1]
for name in obj_dict:
if name not in superclass.__dict__:
value = safe_getattr(subject, name)
members[name] = ClassAttribute(subject, name, value)
# members in __slots__
try:
__slots__ = getslots(subject)
if __slots__:
from sphinx.ext.autodoc import SLOTSATTR
for name, docstring in __slots__.items():
members[name] = ClassAttribute(subject, name, SLOTSATTR, docstring)
except (AttributeError, TypeError, ValueError):
pass
# other members
for name in dir(subject):
try:
value = attrgetter(subject, name)
unmangled = unmangle(subject, name)
if unmangled and unmangled not in members:
if name in obj_dict:
members[unmangled] = ClassAttribute(subject, unmangled, value)
else:
members[unmangled] = ClassAttribute(None, unmangled, value)
except AttributeError:
continue
# annotation only member (ex. attr: int)
for cls in getmro(subject):
try:
for name in getannotations(cls):
name = unmangle(cls, name)
if name and name not in members:
members[name] = ClassAttribute(cls, name, INSTANCEATTR)
except AttributeError:
pass
if analyzer:
# append instance attributes (cf. self.attr1) if analyzer knows
namespace = '.'.join(objpath)
for (ns, name), docstring in analyzer.attr_docs.items():
if namespace == ns and name not in members:
members[name] = ClassAttribute(subject, name, INSTANCEATTR,
'\n'.join(docstring))
return members
from sphinx.ext.autodoc.mock import (MockFinder, MockLoader, _MockModule, _MockObject, # NOQA
mock)

View File

@ -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 @@ class Bar:
class Baz:
"""docstring"""
__slots__ = 'attr'

View File

@ -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',

View File

@ -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):