Close #7051: autodoc: Support instance variables without defaults (PEP-526)

This commit is contained in:
Takeshi KOMIYA
2020-01-25 00:57:36 +09:00
parent f8fc6075ba
commit ecf38edb43
6 changed files with 126 additions and 13 deletions
+1
View File
@@ -39,6 +39,7 @@ Features added
* #2755: autodoc: Support type_comment style (ex. ``# type: (str) -> str``) * #2755: autodoc: Support type_comment style (ex. ``# type: (str) -> str``)
annotation (python3.8+ or `typed_ast <https://github.com/python/typed_ast>`_ annotation (python3.8+ or `typed_ast <https://github.com/python/typed_ast>`_
is required) is required)
* #7051: autodoc: Support instance variables without defaults (PEP-526)
* SphinxTranslator now calls visitor/departure method for super node class if * SphinxTranslator now calls visitor/departure method for super node class if
visitor/departure method for original node class not found visitor/departure method for original node class not found
+43 -8
View File
@@ -24,7 +24,7 @@ from sphinx.deprecation import (
RemovedInSphinx30Warning, RemovedInSphinx40Warning, deprecated_alias RemovedInSphinx30Warning, RemovedInSphinx40Warning, deprecated_alias
) )
from sphinx.environment import BuildEnvironment from sphinx.environment import BuildEnvironment
from sphinx.ext.autodoc.importer import import_object, get_object_members from sphinx.ext.autodoc.importer import import_object, get_module_members, get_object_members
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
@@ -32,9 +32,7 @@ from sphinx.util import inspect
from sphinx.util import logging from sphinx.util import logging
from sphinx.util import rpartition from sphinx.util import rpartition
from sphinx.util.docstrings import prepare_docstring from sphinx.util.docstrings import prepare_docstring
from sphinx.util.inspect import ( from sphinx.util.inspect import getdoc, object_description, safe_getattr, stringify_signature
getdoc, object_description, safe_getattr, safe_getmembers, stringify_signature
)
if False: if False:
# For type annotation # For type annotation
@@ -529,7 +527,10 @@ class Documenter:
# process members and determine which to skip # process members and determine which to skip
for (membername, member) in members: for (membername, member) in members:
# if isattr is True, the member is documented as an attribute # if isattr is True, the member is documented as an attribute
isattr = False if member is INSTANCEATTR:
isattr = True
else:
isattr = False
doc = getdoc(member, self.get_attr, self.env.config.autodoc_inherit_docstrings) doc = getdoc(member, self.get_attr, self.env.config.autodoc_inherit_docstrings)
@@ -793,7 +794,7 @@ class ModuleDocumenter(Documenter):
hasattr(self.object, '__all__')): hasattr(self.object, '__all__')):
# for implicit module members, check __module__ to avoid # for implicit module members, check __module__ to avoid
# documenting imported objects # documenting imported objects
return True, safe_getmembers(self.object) return True, get_module_members(self.object)
else: else:
memberlist = self.object.__all__ memberlist = self.object.__all__
# Sometimes __all__ is broken... # Sometimes __all__ is broken...
@@ -806,7 +807,7 @@ class ModuleDocumenter(Documenter):
type='autodoc' type='autodoc'
) )
# fall back to all members # fall back to all members
return True, safe_getmembers(self.object) return True, get_module_members(self.object)
else: else:
memberlist = self.options.members or [] memberlist = self.options.members or []
ret = [] ret = []
@@ -1251,6 +1252,37 @@ class DataDocumenter(ModuleLevelDocumenter):
or self.modname or self.modname
class DataDeclarationDocumenter(DataDocumenter):
"""
Specialized Documenter subclass for data that cannot be imported
because they are declared without initial value (refs: PEP-526).
"""
objtype = 'datadecl'
directivetype = 'data'
member_order = 60
# must be higher than AttributeDocumenter
priority = 11
@classmethod
def can_document_member(cls, member: Any, membername: str, isattr: bool, parent: Any
) -> bool:
"""This documents only INSTANCEATTR members."""
return (isinstance(parent, ModuleDocumenter) and
isattr and
member is INSTANCEATTR)
def import_object(self) -> bool:
"""Never import anything."""
# disguise as a data
self.objtype = 'data'
return True
def add_content(self, more_content: Any, no_docstring: bool = False) -> None:
"""Never try to get a docstring from the object."""
super().add_content(more_content, no_docstring=True)
class MethodDocumenter(DocstringSignatureMixin, ClassLevelDocumenter): # type: ignore class MethodDocumenter(DocstringSignatureMixin, ClassLevelDocumenter): # type: ignore
""" """
Specialized Documenter subclass for methods (normal, static and class). Specialized Documenter subclass for methods (normal, static and class).
@@ -1438,7 +1470,9 @@ class InstanceAttributeDocumenter(AttributeDocumenter):
def can_document_member(cls, member: Any, membername: str, isattr: bool, parent: Any def can_document_member(cls, member: Any, membername: str, isattr: bool, parent: Any
) -> bool: ) -> bool:
"""This documents only INSTANCEATTR members.""" """This documents only INSTANCEATTR members."""
return isattr and (member is INSTANCEATTR) return (not isinstance(parent, ModuleDocumenter) and
isattr and
member is INSTANCEATTR)
def import_object(self) -> bool: def import_object(self) -> bool:
"""Never import anything.""" """Never import anything."""
@@ -1550,6 +1584,7 @@ def setup(app: Sphinx) -> Dict[str, Any]:
app.add_autodocumenter(ClassDocumenter) app.add_autodocumenter(ClassDocumenter)
app.add_autodocumenter(ExceptionDocumenter) app.add_autodocumenter(ExceptionDocumenter)
app.add_autodocumenter(DataDocumenter) app.add_autodocumenter(DataDocumenter)
app.add_autodocumenter(DataDeclarationDocumenter)
app.add_autodocumenter(FunctionDocumenter) app.add_autodocumenter(FunctionDocumenter)
app.add_autodocumenter(DecoratorDocumenter) app.add_autodocumenter(DecoratorDocumenter)
app.add_autodocumenter(MethodDocumenter) app.add_autodocumenter(MethodDocumenter)
+30 -3
View File
@@ -12,7 +12,7 @@ import importlib
import traceback import traceback
import warnings import warnings
from collections import namedtuple from collections import namedtuple
from typing import Any, Callable, Dict, List from typing import Any, Callable, Dict, List, Tuple
from sphinx.deprecation import RemovedInSphinx40Warning, deprecated_alias from sphinx.deprecation import RemovedInSphinx40Warning, deprecated_alias
from sphinx.util import logging from sphinx.util import logging
@@ -101,12 +101,35 @@ def import_object(modname: str, objpath: List[str], objtype: str = '',
raise ImportError(errmsg) raise ImportError(errmsg)
def get_module_members(module: Any) -> List[Tuple[str, Any]]:
"""Get members of target module."""
from sphinx.ext.autodoc import INSTANCEATTR
members = {} # type: Dict[str, Tuple[str, Any]]
for name in dir(module):
try:
value = safe_getattr(module, name, None)
members[name] = (name, value)
except AttributeError:
continue
# annotation only member (ex. attr: int)
if hasattr(module, '__annotations__'):
for name in module.__annotations__:
if name not in members:
members[name] = (name, INSTANCEATTR)
return sorted(list(members.values()))
Attribute = namedtuple('Attribute', ['name', 'directly_defined', 'value']) Attribute = namedtuple('Attribute', ['name', 'directly_defined', 'value'])
def get_object_members(subject: Any, objpath: List[str], attrgetter: Callable, def get_object_members(subject: Any, objpath: List[str], attrgetter: Callable,
analyzer: Any = None) -> Dict[str, Attribute]: analyzer: Any = None) -> Dict[str, Attribute]:
"""Get members and attributes of target object.""" """Get members and attributes of target object."""
from sphinx.ext.autodoc import INSTANCEATTR
# the members directly defined in the class # the members directly defined in the class
obj_dict = attrgetter(subject, '__dict__', {}) obj_dict = attrgetter(subject, '__dict__', {})
@@ -140,10 +163,14 @@ def get_object_members(subject: Any, objpath: List[str], attrgetter: Callable,
except AttributeError: except AttributeError:
continue continue
# annotation only member (ex. attr: int)
if hasattr(subject, '__annotations__'):
for name in subject.__annotations__:
if name not in members:
members[name] = Attribute(name, True, INSTANCEATTR)
if analyzer: if analyzer:
# append instance attributes (cf. self.attr1) if analyzer knows # append instance attributes (cf. self.attr1) if analyzer knows
from sphinx.ext.autodoc import INSTANCEATTR
namespace = '.'.join(objpath) namespace = '.'.join(objpath)
for (ns, name) in analyzer.find_attr_docs(): for (ns, name) in analyzer.find_attr_docs():
if namespace == ns and name not in members: if namespace == ns and name not in members:
+2 -2
View File
@@ -71,13 +71,13 @@ def setup_documenters(app: Any) -> None:
ModuleDocumenter, ClassDocumenter, ExceptionDocumenter, DataDocumenter, ModuleDocumenter, ClassDocumenter, ExceptionDocumenter, DataDocumenter,
FunctionDocumenter, MethodDocumenter, AttributeDocumenter, FunctionDocumenter, MethodDocumenter, AttributeDocumenter,
InstanceAttributeDocumenter, DecoratorDocumenter, PropertyDocumenter, InstanceAttributeDocumenter, DecoratorDocumenter, PropertyDocumenter,
SlotsAttributeDocumenter, SlotsAttributeDocumenter, DataDeclarationDocumenter,
) )
documenters = [ documenters = [
ModuleDocumenter, ClassDocumenter, ExceptionDocumenter, DataDocumenter, ModuleDocumenter, ClassDocumenter, ExceptionDocumenter, DataDocumenter,
FunctionDocumenter, MethodDocumenter, AttributeDocumenter, FunctionDocumenter, MethodDocumenter, AttributeDocumenter,
InstanceAttributeDocumenter, DecoratorDocumenter, PropertyDocumenter, InstanceAttributeDocumenter, DecoratorDocumenter, PropertyDocumenter,
SlotsAttributeDocumenter, SlotsAttributeDocumenter, DataDeclarationDocumenter,
] # type: List[Type[Documenter]] ] # type: List[Type[Documenter]]
for documenter in documenters: for documenter in documenters:
app.registry.add_documenter(documenter.objtype, documenter) app.registry.add_documenter(documenter.objtype, documenter)
@@ -0,0 +1,9 @@
#: attr1
attr1: str = ''
#: attr2
attr2: str
class Class:
attr1: int = 0
attr2: int
+41
View File
@@ -1388,6 +1388,47 @@ def test_partialmethod_undoc_members(app):
assert list(actual) == expected assert list(actual) == expected
@pytest.mark.skipif(sys.version_info < (3, 6), reason='py36+ is available since python3.6.')
@pytest.mark.sphinx('html', testroot='ext-autodoc')
def test_autodoc_typed_instance_variables(app):
options = {"members": None,
"undoc-members": True}
actual = do_autodoc(app, 'module', 'target.typed_vars', options)
assert list(actual) == [
'',
'.. py:module:: target.typed_vars',
'',
'',
'.. py:class:: Class',
' :module: target.typed_vars',
'',
' ',
' .. py:attribute:: Class.attr1',
' :module: target.typed_vars',
' :annotation: = 0',
' ',
' ',
' .. py:attribute:: Class.attr2',
' :module: target.typed_vars',
' :annotation: = None',
' ',
'',
'.. py:data:: attr1',
' :module: target.typed_vars',
" :annotation: = ''",
'',
' attr1',
' ',
'',
'.. py:data:: attr2',
' :module: target.typed_vars',
" :annotation: = None",
'',
' attr2',
' '
]
@pytest.mark.sphinx('html', testroot='pycode-egg') @pytest.mark.sphinx('html', testroot='pycode-egg')
def test_autodoc_for_egged_code(app): def test_autodoc_for_egged_code(app):
options = {"members": None, options = {"members": None,