From 88b2ec6c34fb405cf468475e6f2a7754e2c705eb Mon Sep 17 00:00:00 2001 From: Takeshi KOMIYA Date: Mon, 3 Aug 2020 01:41:24 +0900 Subject: [PATCH] Fix #8041: autodoc: An ivar on super class is not shown unexpectedly An annotated instance variable on super class is not documented when derived class has also other annotated instance variables because `obj.__annotations__` is overrided by derived class's type hints. To get annotations of the target class correctly, this scans MRO to get all of annotated instance variables. --- CHANGES | 2 + sphinx/ext/autodoc/__init__.py | 17 +++++-- sphinx/ext/autodoc/importer.py | 30 ++++++++++-- .../test-ext-autodoc/target/typed_vars.py | 2 +- tests/test_ext_autodoc.py | 48 ++++++++++++++++--- 5 files changed, 84 insertions(+), 15 deletions(-) diff --git a/CHANGES b/CHANGES index 5d50da73927..aa27f5a6c0b 100644 --- a/CHANGES +++ b/CHANGES @@ -71,6 +71,8 @@ Bugs fixed when ``:inherited-members:`` option given * #8032: autodoc: A type hint for the instance variable defined at parent class is not shown in the document of the derived class +* #8041: autodoc: An annotated instance variable on super class is not + documented when derived class has other annotated instance variables * #7839: autosummary: cannot handle umlauts in function names * #7865: autosummary: Failed to extract summary line when abbreviations found * #7866: autosummary: Failed to extract correct summary line when docstring diff --git a/sphinx/ext/autodoc/__init__.py b/sphinx/ext/autodoc/__init__.py index 832ee4a9209..0a64b56e2b9 100644 --- a/sphinx/ext/autodoc/__init__.py +++ b/sphinx/ext/autodoc/__init__.py @@ -18,6 +18,7 @@ from typing import ( Any, Callable, Dict, Iterator, List, Optional, Sequence, Set, Tuple, Type, TypeVar, Union ) +from typing import get_type_hints from docutils.statemachine import StringList @@ -1605,8 +1606,12 @@ def add_directive_header(self, sig: str) -> None: sourcename = self.get_sourcename() if not self.options.annotation: # obtain annotation for this data - annotations = getattr(self.parent, '__annotations__', {}) - if annotations and self.objpath[-1] in annotations: + try: + annotations = get_type_hints(self.parent) + except TypeError: + annotations = {} + + if self.objpath[-1] in annotations: objrepr = stringify_typehint(annotations.get(self.objpath[-1])) self.add_line(' :type: ' + objrepr, sourcename) else: @@ -1971,8 +1976,12 @@ def add_directive_header(self, sig: str) -> None: sourcename = self.get_sourcename() if not self.options.annotation: # obtain type annotation for this attribute - annotations = getattr(self.parent, '__annotations__', {}) - if annotations and self.objpath[-1] in annotations: + try: + annotations = get_type_hints(self.parent) + except TypeError: + annotations = {} + + if self.objpath[-1] in annotations: objrepr = stringify_typehint(annotations.get(self.objpath[-1])) self.add_line(' :type: ' + objrepr, sourcename) else: diff --git a/sphinx/ext/autodoc/importer.py b/sphinx/ext/autodoc/importer.py index 031911de29b..133ce1439c9 100644 --- a/sphinx/ext/autodoc/importer.py +++ b/sphinx/ext/autodoc/importer.py @@ -18,6 +18,10 @@ from sphinx.util import logging from sphinx.util.inspect import isclass, isenumclass, safe_getattr +if False: + # For type annotation + from typing import Type # NOQA + logger = logging.getLogger(__name__) @@ -158,6 +162,24 @@ def get_module_members(module: Any) -> List[Tuple[str, Any]]: ('value', Any)]) +def _getmro(obj: Any) -> Tuple["Type", ...]: + """Get __mro__ from given *obj* safely.""" + __mro__ = safe_getattr(obj, '__mro__', None) + if isinstance(__mro__, tuple): + return __mro__ + else: + return tuple() + + +def _getannotations(obj: Any) -> Mapping[str, Any]: + """Get __annotations__ from given *obj* safely.""" + __annotations__ = safe_getattr(obj, '__annotations__', None) + if isinstance(__annotations__, Mapping): + return __annotations__ + else: + return {} + + def get_object_members(subject: Any, objpath: List[str], attrgetter: Callable, analyzer: ModuleAnalyzer = None) -> Dict[str, Attribute]: """Get members and attributes of target object.""" @@ -199,11 +221,11 @@ def get_object_members(subject: Any, objpath: List[str], attrgetter: Callable, continue # annotation only member (ex. attr: int) - if hasattr(subject, '__annotations__') and isinstance(subject.__annotations__, Mapping): - for name in subject.__annotations__: - name = unmangle(subject, name) + for i, cls in enumerate(_getmro(subject)): + for name in _getannotations(cls): + name = unmangle(cls, name) if name and name not in members: - members[name] = Attribute(name, True, INSTANCEATTR) + members[name] = Attribute(name, i == 0, INSTANCEATTR) if analyzer: # append instance attributes (cf. self.attr1) if analyzer knows diff --git a/tests/roots/test-ext-autodoc/target/typed_vars.py b/tests/roots/test-ext-autodoc/target/typed_vars.py index 43c58deb05b..ba9657f1870 100644 --- a/tests/roots/test-ext-autodoc/target/typed_vars.py +++ b/tests/roots/test-ext-autodoc/target/typed_vars.py @@ -28,4 +28,4 @@ def __init__(self): class Derived(Class): - pass + attr7: int diff --git a/tests/test_ext_autodoc.py b/tests/test_ext_autodoc.py index d6f973f4998..15e1f35395e 100644 --- a/tests/test_ext_autodoc.py +++ b/tests/test_ext_autodoc.py @@ -1580,12 +1580,7 @@ def test_autodoc_typed_instance_variables(app): ' :module: target.typed_vars', '', '', - ' .. py:attribute:: Derived.attr2', - ' :module: target.typed_vars', - ' :type: int', - '', - '', - ' .. py:attribute:: Derived.descr4', + ' .. py:attribute:: Derived.attr7', ' :module: target.typed_vars', ' :type: int', '', @@ -1615,6 +1610,47 @@ def test_autodoc_typed_instance_variables(app): ] +@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_inherited_instance_variables(app): + options = {"members": None, + "undoc-members": True, + "inherited-members": True} + actual = do_autodoc(app, 'class', 'target.typed_vars.Derived', options) + assert list(actual) == [ + '', + '.. py:class:: Derived()', + ' :module: target.typed_vars', + '', + '', + ' .. py:attribute:: Derived.attr1', + ' :module: target.typed_vars', + ' :type: int', + ' :value: 0', + '', + '', + ' .. py:attribute:: Derived.attr2', + ' :module: target.typed_vars', + ' :type: int', + '', + '', + ' .. py:attribute:: Derived.attr3', + ' :module: target.typed_vars', + ' :value: 0', + '', + '', + ' .. py:attribute:: Derived.attr7', + ' :module: target.typed_vars', + ' :type: int', + '', + '', + ' .. py:attribute:: Derived.descr4', + ' :module: target.typed_vars', + ' :type: int', + '', + ] + + @pytest.mark.sphinx('html', testroot='ext-autodoc') def test_autodoc_GenericAlias(app): options = {"members": None,