Skip to content

Commit

Permalink
Fix sphinx-doc#8534: autoattribute failed to document a commented att…
Browse files Browse the repository at this point in the history
…ribute in alias class
  • Loading branch information
tk0miya committed Dec 15, 2020
1 parent 9d56d35 commit 5917c6f
Show file tree
Hide file tree
Showing 5 changed files with 121 additions and 18 deletions.
2 changes: 2 additions & 0 deletions CHANGES
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,8 @@ Bugs fixed
attributes
* #8503: autodoc: autoattribute could not create document for a GenericAlias as
class attributes correctly
* #8534: autodoc: autoattribute could not create document for a commented
attribute in alias class
* #8452: autodoc: autodoc_type_aliases doesn't work when autodoc_typehints is
set to "description"
* #8460: autodoc: autodata and autoattribute directives do not display type
Expand Down
101 changes: 83 additions & 18 deletions sphinx/ext/autodoc/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -2120,9 +2120,82 @@ def get_doc(self, encoding: str = None, ignore: int = None) -> List[List[str]]:
return super().get_doc(encoding, ignore) # type: ignore


class UninitializedInstanceAttributeMixin(DataDocumenterMixinBase):
"""
Mixin for AttributeDocumenter to provide the feature for supporting uninitialized
instance attributes.
"""

def get_attribute_comment(self, parent: Any) -> Optional[List[str]]:
try:
analyzer = ModuleAnalyzer.for_module(self.modname)
analyzer.analyze()

qualname = safe_getattr(parent, '__qualname__', None)
if qualname and self.objpath:
key = (qualname, self.objpath[-1])
if key in analyzer.attr_docs:
return list(analyzer.attr_docs[key])
except PycodeError:
pass

return None

def is_uninitialized_instance_attribute(self, parent: Any) -> bool:
"""Check the subject is an attribute defined in __init__()."""
# An instance variable defined inside __init__().
if self.get_attribute_comment(parent):
return True
else:
return False

def import_object(self, raiseerror: bool = False) -> bool:
try:
return super().import_object(raiseerror=True) # type: ignore
except ImportError as exc:
try:
ret = import_object(self.modname, self.objpath[:-1], 'class',
attrgetter=self.get_attr, # type: ignore
warningiserror=self.config.autodoc_warningiserror)
parent = ret[3]
if self.is_uninitialized_instance_attribute(parent):
self.object = UNINITIALIZED_ATTR
self.parent = parent
return True
except ImportError:
pass

if raiseerror:
raise
else:
logger.warning(exc.args[0], type='autodoc', subtype='import_object')
self.env.note_reread()
return False

def should_suppress_value_header(self) -> bool:
return (self.object == UNINITIALIZED_ATTR or
super().should_suppress_value_header())

def get_doc(self, encoding: str = None, ignore: int = None) -> List[List[str]]:
if self.object is UNINITIALIZED_ATTR:
comment = self.get_attribute_comment(self.parent)
if comment:
return [comment]

return super().get_doc(encoding, ignore) # type: ignore

def add_content(self, more_content: Optional[StringList], no_docstring: bool = False
) -> None:
if self.object is UNINITIALIZED_ATTR:
self.analyzer = None

super().add_content(more_content, no_docstring=no_docstring) # type: ignore


# Note: NonDataDescriptorMixin must be placed after SlotsMixin
class AttributeDocumenter(GenericAliasMixin, NewTypeMixin, SlotsMixin, # type: ignore
TypeVarMixin, NonDataDescriptorMixin, DocstringStripSignatureMixin,
TypeVarMixin, UninitializedInstanceAttributeMixin,
NonDataDescriptorMixin, DocstringStripSignatureMixin,
ClassLevelDocumenter):
"""
Specialized Documenter subclass for attributes.
Expand Down Expand Up @@ -2173,19 +2246,6 @@ def isinstanceattribute(self) -> bool:
except ImportError:
pass

# An instance variable defined inside __init__().
try:
analyzer = ModuleAnalyzer.for_module(self.modname)
attr_docs = analyzer.find_attr_docs()
if self.objpath:
key = ('.'.join(self.objpath[:-1]), self.objpath[-1])
if key in attr_docs:
return True

return False
except PycodeError:
pass

return False

def import_object(self, raiseerror: bool = False) -> bool:
Expand Down Expand Up @@ -2224,10 +2284,15 @@ def add_directive_header(self, sig: str) -> None:
objrepr = stringify_typehint(annotations.get(self.objpath[-1]))
self.add_line(' :type: ' + objrepr, sourcename)
else:
key = ('.'.join(self.objpath[:-1]), self.objpath[-1])
if self.analyzer and key in self.analyzer.annotations:
self.add_line(' :type: ' + self.analyzer.annotations[key],
sourcename)
try:
qualname = safe_getattr(self.parent, '__qualname__',
'.'.join(self.objpath[:-1]))
key = (qualname, self.objpath[-1])
if self.analyzer and key in self.analyzer.annotations:
self.add_line(' :type: ' + self.analyzer.annotations[key],
sourcename)
except AttributeError:
pass

try:
if (self.object is INSTANCEATTR or self.options.no_value or
Expand Down
3 changes: 3 additions & 0 deletions tests/roots/test-ext-autodoc/target/typed_vars.py
Original file line number Diff line number Diff line change
Expand Up @@ -29,3 +29,6 @@ def __init__(self):

class Derived(Class):
attr7: int


Alias = Class
5 changes: 5 additions & 0 deletions tests/test_ext_autodoc.py
Original file line number Diff line number Diff line change
Expand Up @@ -1561,6 +1561,11 @@ def test_autodoc_typed_instance_variables(app):
'.. py:module:: target.typed_vars',
'',
'',
'.. py:attribute:: Alias',
' :module: target.typed_vars',
'',
' alias of :class:`target.typed_vars.Class`',
'',
'.. py:class:: Class()',
' :module: target.typed_vars',
'',
Expand Down
28 changes: 28 additions & 0 deletions tests/test_ext_autodoc_autoattribute.py
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,19 @@ def test_autoattribute_typed_variable(app):
]


@pytest.mark.skipif(sys.version_info < (3, 6), reason='python 3.6+ is required.')
@pytest.mark.sphinx('html', testroot='ext-autodoc')
def test_autoattribute_typed_variable_in_alias(app):
actual = do_autodoc(app, 'attribute', 'target.typed_vars.Alias.attr2')
assert list(actual) == [
'',
'.. py:attribute:: Alias.attr2',
' :module: target.typed_vars',
' :type: int',
'',
]


@pytest.mark.skipif(sys.version_info < (3, 6), reason='python 3.6+ is required.')
@pytest.mark.sphinx('html', testroot='ext-autodoc')
def test_autoattribute_instance_variable(app):
Expand All @@ -72,6 +85,21 @@ def test_autoattribute_instance_variable(app):
]


@pytest.mark.skipif(sys.version_info < (3, 6), reason='python 3.6+ is required.')
@pytest.mark.sphinx('html', testroot='ext-autodoc')
def test_autoattribute_instance_variable_in_alias(app):
actual = do_autodoc(app, 'attribute', 'target.typed_vars.Alias.attr4')
assert list(actual) == [
'',
'.. py:attribute:: Alias.attr4',
' :module: target.typed_vars',
' :type: int',
'',
' attr4',
'',
]


@pytest.mark.sphinx('html', testroot='ext-autodoc')
def test_autoattribute_slots_variable_list(app):
actual = do_autodoc(app, 'attribute', 'target.slots.Foo.attr')
Expand Down

0 comments on commit 5917c6f

Please sign in to comment.