From cfa0c91242220d828314fa62b42278d6afdd4746 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 | 5 ++++ 5 files changed, 47 insertions(+), 9 deletions(-) diff --git a/CHANGES b/CHANGES index d8a3cb915a7..e50ed26c76a 100644 --- a/CHANGES +++ b/CHANGES @@ -67,6 +67,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..ac567c4c44b 100644 --- a/tests/test_ext_autodoc.py +++ b/tests/test_ext_autodoc.py @@ -1590,6 +1590,11 @@ def test_autodoc_typed_instance_variables(app): ' :type: int', '', '', + ' .. py:attribute:: Derived.descr7', + ' :module: target.typed_vars', + ' :type: int', + '', + '', '.. py:data:: attr1', ' :module: target.typed_vars', ' :type: str',