Skip to content

Commit

Permalink
Fix #8041: autodoc: An ivar on super class is not shown unexpectedly
Browse files Browse the repository at this point in the history
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 7, 2020
1 parent 5aa774b commit 88b2ec6
Show file tree
Hide file tree
Showing 5 changed files with 84 additions and 15 deletions.
2 changes: 2 additions & 0 deletions CHANGES
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
17 changes: 13 additions & 4 deletions sphinx/ext/autodoc/__init__.py
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
Expand Up @@ -28,4 +28,4 @@ def __init__(self):


class Derived(Class):
pass
attr7: int
48 changes: 42 additions & 6 deletions tests/test_ext_autodoc.py
Original file line number Diff line number Diff line change
Expand Up @@ -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',
'',
Expand Down Expand Up @@ -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,
Expand Down

0 comments on commit 88b2ec6

Please sign in to comment.