mirror of
https://github.com/sphinx-doc/sphinx.git
synced 2025-02-25 18:55:22 -06:00
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:
commit
e3dc782364
1
CHANGES
1
CHANGES
@ -79,6 +79,7 @@ Bugs fixed
|
|||||||
* #8522: autodoc: ``__bool__`` method could be called
|
* #8522: autodoc: ``__bool__`` method could be called
|
||||||
* #8067: autodoc: A typehint for the instance variable having type_comment on
|
* #8067: autodoc: A typehint for the instance variable having type_comment on
|
||||||
super class is not displayed
|
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
|
* #8477: autosummary: non utf-8 reST files are generated when template contains
|
||||||
multibyte characters
|
multibyte characters
|
||||||
* #8501: autosummary: summary extraction splits text after "el at." unexpectedly
|
* #8501: autosummary: summary extraction splits text after "el at." unexpectedly
|
||||||
|
@ -26,7 +26,8 @@ from sphinx.config import ENUM, Config
|
|||||||
from sphinx.deprecation import (RemovedInSphinx40Warning, RemovedInSphinx50Warning,
|
from sphinx.deprecation import (RemovedInSphinx40Warning, RemovedInSphinx50Warning,
|
||||||
RemovedInSphinx60Warning)
|
RemovedInSphinx60Warning)
|
||||||
from sphinx.environment import BuildEnvironment
|
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.ext.autodoc.mock import mock
|
||||||
from sphinx.locale import _, __
|
from sphinx.locale import _, __
|
||||||
from sphinx.pycode import ModuleAnalyzer, PycodeError
|
from sphinx.pycode import ModuleAnalyzer, PycodeError
|
||||||
@ -274,9 +275,11 @@ class ObjectMember(tuple):
|
|||||||
def __new__(cls, name: str, obj: Any, **kwargs: Any) -> Any:
|
def __new__(cls, name: str, obj: Any, **kwargs: Any) -> Any:
|
||||||
return super().__new__(cls, (name, obj)) # type: ignore
|
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.__name__ = name
|
||||||
self.object = obj
|
self.object = obj
|
||||||
|
self.docstring = docstring
|
||||||
self.skipped = skipped
|
self.skipped = skipped
|
||||||
|
|
||||||
|
|
||||||
@ -708,6 +711,11 @@ class Documenter:
|
|||||||
cls_doc = self.get_attr(cls, '__doc__', None)
|
cls_doc = self.get_attr(cls, '__doc__', None)
|
||||||
if cls_doc == doc:
|
if cls_doc == doc:
|
||||||
doc = None
|
doc = None
|
||||||
|
|
||||||
|
if isinstance(obj, ObjectMember) and obj.docstring:
|
||||||
|
# hack for ClassDocumenter to inject docstring via ObjectMember
|
||||||
|
doc = obj.docstring
|
||||||
|
|
||||||
has_doc = bool(doc)
|
has_doc = bool(doc)
|
||||||
|
|
||||||
metadata = extract_metadata(doc)
|
metadata = extract_metadata(doc)
|
||||||
@ -1576,7 +1584,7 @@ class ClassDocumenter(DocstringSignatureMixin, ModuleLevelDocumenter): # type:
|
|||||||
self.add_line(' ' + _('Bases: %s') % ', '.join(bases), sourcename)
|
self.add_line(' ' + _('Bases: %s') % ', '.join(bases), sourcename)
|
||||||
|
|
||||||
def get_object_members(self, want_all: bool) -> Tuple[bool, ObjectMembers]:
|
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 want_all:
|
||||||
if not self.options.members:
|
if not self.options.members:
|
||||||
return False, [] # type: ignore
|
return False, [] # type: ignore
|
||||||
@ -1584,16 +1592,18 @@ class ClassDocumenter(DocstringSignatureMixin, ModuleLevelDocumenter): # type:
|
|||||||
selected = []
|
selected = []
|
||||||
for name in self.options.members: # type: str
|
for name in self.options.members: # type: str
|
||||||
if name in members:
|
if name in members:
|
||||||
selected.append((name, members[name].value))
|
selected.append(ObjectMember(name, members[name].value,
|
||||||
|
docstring=members[name].docstring))
|
||||||
else:
|
else:
|
||||||
logger.warning(__('missing attribute %s in object %s') %
|
logger.warning(__('missing attribute %s in object %s') %
|
||||||
(name, self.fullname), type='autodoc')
|
(name, self.fullname), type='autodoc')
|
||||||
return False, selected
|
return False, selected
|
||||||
elif self.options.inherited_members:
|
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:
|
else:
|
||||||
return False, [(m.name, m.value) for m in members.values()
|
return False, [ObjectMember(m.name, m.value, docstring=m.docstring)
|
||||||
if m.directly_defined]
|
for m in members.values() if m.class_ == self.object]
|
||||||
|
|
||||||
def get_doc(self, encoding: str = None, ignore: int = None) -> List[List[str]]:
|
def get_doc(self, encoding: str = None, ignore: int = None) -> List[List[str]]:
|
||||||
if encoding is not None:
|
if encoding is not None:
|
||||||
|
@ -241,6 +241,83 @@ def get_object_members(subject: Any, objpath: List[str], attrgetter: Callable,
|
|||||||
return members
|
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
|
from sphinx.ext.autodoc.mock import (MockFinder, MockLoader, _MockModule, _MockObject, # NOQA
|
||||||
mock)
|
mock)
|
||||||
|
|
||||||
|
@ -1,8 +1,12 @@
|
|||||||
class Foo:
|
class Foo:
|
||||||
|
"""docstring"""
|
||||||
|
|
||||||
__slots__ = ['attr']
|
__slots__ = ['attr']
|
||||||
|
|
||||||
|
|
||||||
class Bar:
|
class Bar:
|
||||||
|
"""docstring"""
|
||||||
|
|
||||||
__slots__ = {'attr1': 'docstring of attr1',
|
__slots__ = {'attr1': 'docstring of attr1',
|
||||||
'attr2': 'docstring of attr2',
|
'attr2': 'docstring of attr2',
|
||||||
'attr3': None}
|
'attr3': None}
|
||||||
@ -12,4 +16,6 @@ class Bar:
|
|||||||
|
|
||||||
|
|
||||||
class Baz:
|
class Baz:
|
||||||
|
"""docstring"""
|
||||||
|
|
||||||
__slots__ = 'attr'
|
__slots__ = 'attr'
|
||||||
|
@ -1172,6 +1172,8 @@ def test_slots(app):
|
|||||||
'.. py:class:: Bar()',
|
'.. py:class:: Bar()',
|
||||||
' :module: target.slots',
|
' :module: target.slots',
|
||||||
'',
|
'',
|
||||||
|
' docstring',
|
||||||
|
'',
|
||||||
'',
|
'',
|
||||||
' .. py:attribute:: Bar.attr1',
|
' .. py:attribute:: Bar.attr1',
|
||||||
' :module: target.slots',
|
' :module: target.slots',
|
||||||
@ -1192,6 +1194,8 @@ def test_slots(app):
|
|||||||
'.. py:class:: Baz()',
|
'.. py:class:: Baz()',
|
||||||
' :module: target.slots',
|
' :module: target.slots',
|
||||||
'',
|
'',
|
||||||
|
' docstring',
|
||||||
|
'',
|
||||||
'',
|
'',
|
||||||
' .. py:attribute:: Baz.attr',
|
' .. py:attribute:: Baz.attr',
|
||||||
' :module: target.slots',
|
' :module: target.slots',
|
||||||
@ -1200,6 +1204,8 @@ def test_slots(app):
|
|||||||
'.. py:class:: Foo()',
|
'.. py:class:: Foo()',
|
||||||
' :module: target.slots',
|
' :module: target.slots',
|
||||||
'',
|
'',
|
||||||
|
' docstring',
|
||||||
|
'',
|
||||||
'',
|
'',
|
||||||
' .. py:attribute:: Foo.attr',
|
' .. py:attribute:: Foo.attr',
|
||||||
' :module: target.slots',
|
' :module: target.slots',
|
||||||
|
@ -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.skipif(sys.version_info < (3, 7), reason='python 3.7+ is required.')
|
||||||
@pytest.mark.sphinx('html', testroot='ext-autodoc')
|
@pytest.mark.sphinx('html', testroot='ext-autodoc')
|
||||||
def test_show_inheritance_for_subclass_of_generic_type(app):
|
def test_show_inheritance_for_subclass_of_generic_type(app):
|
||||||
|
Loading…
Reference in New Issue
Block a user