summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorTakeshi KOMIYA <i.tkomiya@gmail.com>2020-01-23 23:47:04 +0900
committerTakeshi KOMIYA <i.tkomiya@gmail.com>2020-01-25 00:57:36 +0900
commitecf38edb439cb8dc76d3a31adc5e358c8c859f97 (patch)
tree9f79e3c7e50fd3ce07a4d1f65c752203685418f1
parentf8fc6075ba317ba694ae4df5227a5358a08df6e3 (diff)
downloadsphinx-git-ecf38edb439cb8dc76d3a31adc5e358c8c859f97.tar.gz
Close #7051: autodoc: Support instance variables without defaults (PEP-526)
-rw-r--r--CHANGES1
-rw-r--r--sphinx/ext/autodoc/__init__.py51
-rw-r--r--sphinx/ext/autodoc/importer.py33
-rw-r--r--sphinx/ext/autosummary/generate.py4
-rw-r--r--tests/roots/test-ext-autodoc/target/typed_vars.py9
-rw-r--r--tests/test_autodoc.py41
6 files changed, 126 insertions, 13 deletions
diff --git a/CHANGES b/CHANGES
index 1466548e4..14427b3c2 100644
--- a/CHANGES
+++ b/CHANGES
@@ -39,6 +39,7 @@ Features added
* #2755: autodoc: Support type_comment style (ex. ``# type: (str) -> str``)
annotation (python3.8+ or `typed_ast <https://github.com/python/typed_ast>`_
is required)
+* #7051: autodoc: Support instance variables without defaults (PEP-526)
* SphinxTranslator now calls visitor/departure method for super node class if
visitor/departure method for original node class not found
diff --git a/sphinx/ext/autodoc/__init__.py b/sphinx/ext/autodoc/__init__.py
index 9c4d4ba40..c9eb5207f 100644
--- a/sphinx/ext/autodoc/__init__.py
+++ b/sphinx/ext/autodoc/__init__.py
@@ -24,7 +24,7 @@ from sphinx.deprecation import (
RemovedInSphinx30Warning, RemovedInSphinx40Warning, deprecated_alias
)
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.locale import _, __
from sphinx.pycode import ModuleAnalyzer, PycodeError
@@ -32,9 +32,7 @@ from sphinx.util import inspect
from sphinx.util import logging
from sphinx.util import rpartition
from sphinx.util.docstrings import prepare_docstring
-from sphinx.util.inspect import (
- getdoc, object_description, safe_getattr, safe_getmembers, stringify_signature
-)
+from sphinx.util.inspect import getdoc, object_description, safe_getattr, stringify_signature
if False:
# For type annotation
@@ -529,7 +527,10 @@ class Documenter:
# process members and determine which to skip
for (membername, member) in members:
# 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)
@@ -793,7 +794,7 @@ class ModuleDocumenter(Documenter):
hasattr(self.object, '__all__')):
# for implicit module members, check __module__ to avoid
# documenting imported objects
- return True, safe_getmembers(self.object)
+ return True, get_module_members(self.object)
else:
memberlist = self.object.__all__
# Sometimes __all__ is broken...
@@ -806,7 +807,7 @@ class ModuleDocumenter(Documenter):
type='autodoc'
)
# fall back to all members
- return True, safe_getmembers(self.object)
+ return True, get_module_members(self.object)
else:
memberlist = self.options.members or []
ret = []
@@ -1251,6 +1252,37 @@ class DataDocumenter(ModuleLevelDocumenter):
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
"""
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
) -> bool:
"""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:
"""Never import anything."""
@@ -1550,6 +1584,7 @@ def setup(app: Sphinx) -> Dict[str, Any]:
app.add_autodocumenter(ClassDocumenter)
app.add_autodocumenter(ExceptionDocumenter)
app.add_autodocumenter(DataDocumenter)
+ app.add_autodocumenter(DataDeclarationDocumenter)
app.add_autodocumenter(FunctionDocumenter)
app.add_autodocumenter(DecoratorDocumenter)
app.add_autodocumenter(MethodDocumenter)
diff --git a/sphinx/ext/autodoc/importer.py b/sphinx/ext/autodoc/importer.py
index 31bc6042d..672d90ec7 100644
--- a/sphinx/ext/autodoc/importer.py
+++ b/sphinx/ext/autodoc/importer.py
@@ -12,7 +12,7 @@ import importlib
import traceback
import warnings
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.util import logging
@@ -101,12 +101,35 @@ def import_object(modname: str, objpath: List[str], objtype: str = '',
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'])
def get_object_members(subject: Any, objpath: List[str], attrgetter: Callable,
analyzer: Any = None) -> Dict[str, Attribute]:
"""Get members and attributes of target object."""
+ from sphinx.ext.autodoc import INSTANCEATTR
+
# the members directly defined in the class
obj_dict = attrgetter(subject, '__dict__', {})
@@ -140,10 +163,14 @@ def get_object_members(subject: Any, objpath: List[str], attrgetter: Callable,
except AttributeError:
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:
# append instance attributes (cf. self.attr1) if analyzer knows
- from sphinx.ext.autodoc import INSTANCEATTR
-
namespace = '.'.join(objpath)
for (ns, name) in analyzer.find_attr_docs():
if namespace == ns and name not in members:
diff --git a/sphinx/ext/autosummary/generate.py b/sphinx/ext/autosummary/generate.py
index cf7f52f33..57082c943 100644
--- a/sphinx/ext/autosummary/generate.py
+++ b/sphinx/ext/autosummary/generate.py
@@ -71,13 +71,13 @@ def setup_documenters(app: Any) -> None:
ModuleDocumenter, ClassDocumenter, ExceptionDocumenter, DataDocumenter,
FunctionDocumenter, MethodDocumenter, AttributeDocumenter,
InstanceAttributeDocumenter, DecoratorDocumenter, PropertyDocumenter,
- SlotsAttributeDocumenter,
+ SlotsAttributeDocumenter, DataDeclarationDocumenter,
)
documenters = [
ModuleDocumenter, ClassDocumenter, ExceptionDocumenter, DataDocumenter,
FunctionDocumenter, MethodDocumenter, AttributeDocumenter,
InstanceAttributeDocumenter, DecoratorDocumenter, PropertyDocumenter,
- SlotsAttributeDocumenter,
+ SlotsAttributeDocumenter, DataDeclarationDocumenter,
] # type: List[Type[Documenter]]
for documenter in documenters:
app.registry.add_documenter(documenter.objtype, documenter)
diff --git a/tests/roots/test-ext-autodoc/target/typed_vars.py b/tests/roots/test-ext-autodoc/target/typed_vars.py
new file mode 100644
index 000000000..9c71cd55b
--- /dev/null
+++ b/tests/roots/test-ext-autodoc/target/typed_vars.py
@@ -0,0 +1,9 @@
+#: attr1
+attr1: str = ''
+#: attr2
+attr2: str
+
+
+class Class:
+ attr1: int = 0
+ attr2: int
diff --git a/tests/test_autodoc.py b/tests/test_autodoc.py
index 6ee2c6ea9..05cc8bc74 100644
--- a/tests/test_autodoc.py
+++ b/tests/test_autodoc.py
@@ -1388,6 +1388,47 @@ def test_partialmethod_undoc_members(app):
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')
def test_autodoc_for_egged_code(app):
options = {"members": None,