Skip to content

Commit

Permalink
Fix sphinx-doc#8041: autodoc: An ivar on super class is not shown une…
Browse files Browse the repository at this point in the history
…xpectedly

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.
  • Loading branch information
tk0miya committed Aug 3, 2020
1 parent a721631 commit cfa0c91
Show file tree
Hide file tree
Showing 5 changed files with 47 additions and 9 deletions.
2 changes: 2 additions & 0 deletions CHANGES
Expand Up @@ -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
Expand Down
17 changes: 13 additions & 4 deletions sphinx/ext/autodoc/__init__.py
Expand Up @@ -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

Expand Down Expand Up @@ -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:
Expand Down Expand Up @@ -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:
Expand Down
30 changes: 26 additions & 4 deletions sphinx/ext/autodoc/importer.py
Expand Up @@ -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__)


Expand Down Expand Up @@ -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."""
Expand Down Expand Up @@ -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
Expand Down
2 changes: 1 addition & 1 deletion tests/roots/test-ext-autodoc/target/typed_vars.py
Expand Up @@ -28,4 +28,4 @@ def __init__(self):


class Derived(Class):
pass
attr7: int
5 changes: 5 additions & 0 deletions tests/test_ext_autodoc.py
Expand Up @@ -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',
Expand Down

0 comments on commit cfa0c91

Please sign in to comment.