Skip to content

Commit

Permalink
Merge pull request #7946 from tk0miya/904_autofunction_ivar_support
Browse files Browse the repository at this point in the history
Fix #904: autodoc: An instance attribute cause a crash of autofunction
  • Loading branch information
tk0miya committed Jul 16, 2020
2 parents d7d14f2 + 610ab92 commit 8c8943f
Show file tree
Hide file tree
Showing 3 changed files with 73 additions and 26 deletions.
1 change: 1 addition & 0 deletions CHANGES
Expand Up @@ -43,6 +43,7 @@ Bugs fixed
* #7935: autodoc: function signature is not shown when the function has a
parameter having ``inspect._empty`` as its default value
* #7901: autodoc: type annotations for overloaded functions are not resolved
* #904: autodoc: An instance attribute cause a crash of autofunction directive
* #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
83 changes: 58 additions & 25 deletions sphinx/ext/autodoc/__init__.py
Expand Up @@ -336,7 +336,7 @@ def parse_name(self) -> bool:
('.' + '.'.join(self.objpath) if self.objpath else '')
return True

def import_object(self) -> bool:
def import_object(self, raiseerror: bool = False) -> bool:
"""Import the object given by *self.modname* and *self.objpath* and set
it as *self.object*.
Expand All @@ -350,9 +350,12 @@ def import_object(self) -> bool:
self.module, self.parent, self.object_name, self.object = ret
return True
except ImportError as exc:
logger.warning(exc.args[0], type='autodoc', subtype='import_object')
self.env.note_reread()
return False
if raiseerror:
raise
else:
logger.warning(exc.args[0], type='autodoc', subtype='import_object')
self.env.note_reread()
return False

def get_real_modname(self) -> str:
"""Get the real module name of an object to document.
Expand Down Expand Up @@ -886,7 +889,7 @@ def parse_name(self) -> bool:
type='autodoc')
return ret

def import_object(self) -> Any:
def import_object(self, raiseerror: bool = False) -> bool:
def is_valid_module_all(__all__: Any) -> bool:
"""Check the given *__all__* is valid for a module."""
if (isinstance(__all__, (list, tuple)) and
Expand All @@ -895,7 +898,7 @@ def is_valid_module_all(__all__: Any) -> bool:
else:
return False

ret = super().import_object()
ret = super().import_object(raiseerror)

if not self.options.ignore_module_all:
__all__ = getattr(self.object, '__all__', None)
Expand Down Expand Up @@ -1291,8 +1294,8 @@ def can_document_member(cls, member: Any, membername: str, isattr: bool, parent:
) -> bool:
return isinstance(member, type)

def import_object(self) -> Any:
ret = super().import_object()
def import_object(self, raiseerror: bool = False) -> bool:
ret = super().import_object(raiseerror)
# if the class is documented under another name, document it
# as data/attribute
if ret:
Expand Down Expand Up @@ -1606,7 +1609,7 @@ def can_document_member(cls, member: Any, membername: str, isattr: bool, parent:
isattr and
member is INSTANCEATTR)

def import_object(self) -> bool:
def import_object(self, raiseerror: bool = False) -> bool:
"""Never import anything."""
# disguise as a data
self.objtype = 'data'
Expand Down Expand Up @@ -1705,8 +1708,8 @@ def can_document_member(cls, member: Any, membername: str, isattr: bool, parent:
return inspect.isroutine(member) and \
not isinstance(parent, ModuleDocumenter)

def import_object(self) -> Any:
ret = super().import_object()
def import_object(self, raiseerror: bool = False) -> bool:
ret = super().import_object(raiseerror)
if not ret:
return ret

Expand Down Expand Up @@ -1873,15 +1876,42 @@ def can_document_member(cls, member: Any, membername: str, isattr: bool, parent:
def document_members(self, all_members: bool = False) -> None:
pass

def import_object(self) -> Any:
ret = super().import_object()
if inspect.isenumattribute(self.object):
self.object = self.object.value
if inspect.isattributedescriptor(self.object):
self._datadescriptor = True
else:
# if it's not a data descriptor
self._datadescriptor = False
def isinstanceattribute(self) -> bool:
"""Check the subject is an instance attribute."""
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:
return False

def import_object(self, raiseerror: bool = False) -> bool:
try:
ret = super().import_object(raiseerror=True)
if inspect.isenumattribute(self.object):
self.object = self.object.value
if inspect.isattributedescriptor(self.object):
self._datadescriptor = True
else:
# if it's not a data descriptor
self._datadescriptor = False
except ImportError as exc:
if self.isinstanceattribute():
self.object = INSTANCEATTR
self._datadescriptor = False
ret = True
elif raiseerror:
raise
else:
logger.warning(exc.args[0], type='autodoc', subtype='import_object')
self.env.note_reread()
ret = False

return ret

def get_real_modname(self) -> str:
Expand Down Expand Up @@ -1988,7 +2018,7 @@ def can_document_member(cls, member: Any, membername: str, isattr: bool, parent:
isattr and
member is INSTANCEATTR)

def import_object(self) -> bool:
def import_object(self, raiseerror: bool = False) -> bool:
"""Never import anything."""
# disguise as an attribute
self.objtype = 'attribute'
Expand Down Expand Up @@ -2019,7 +2049,7 @@ def can_document_member(cls, member: Any, membername: str, isattr: bool, parent:
"""This documents only SLOTSATTR members."""
return member is SLOTSATTR

def import_object(self) -> Any:
def import_object(self, raiseerror: bool = False) -> bool:
"""Never import anything."""
# disguise as an attribute
self.objtype = 'attribute'
Expand All @@ -2033,9 +2063,12 @@ def import_object(self) -> Any:
self.module, _, _, self.parent = ret
return True
except ImportError as exc:
logger.warning(exc.args[0], type='autodoc', subtype='import_object')
self.env.note_reread()
return False
if raiseerror:
raise
else:
logger.warning(exc.args[0], type='autodoc', subtype='import_object')
self.env.note_reread()
return False

def get_doc(self, encoding: str = None, ignore: int = None) -> List[List[str]]:
"""Decode and return lines of the docstring(s) for the object."""
Expand Down
15 changes: 14 additions & 1 deletion tests/test_ext_autodoc.py
Expand Up @@ -1047,7 +1047,7 @@ def test_class_attributes(app):


@pytest.mark.sphinx('html', testroot='ext-autodoc')
def test_instance_attributes(app):
def test_autoclass_instance_attributes(app):
options = {"members": None}
actual = do_autodoc(app, 'class', 'target.InstAttCls', options)
assert list(actual) == [
Expand Down Expand Up @@ -1120,6 +1120,19 @@ def test_instance_attributes(app):
]


@pytest.mark.sphinx('html', testroot='ext-autodoc')
def test_autoattribute_instance_attributes(app):
actual = do_autodoc(app, 'attribute', 'target.InstAttCls.ia1')
assert list(actual) == [
'',
'.. py:attribute:: InstAttCls.ia1',
' :module: target',
'',
' Doc comment for instance attribute InstAttCls.ia1',
''
]


@pytest.mark.sphinx('html', testroot='ext-autodoc')
def test_slots(app):
options = {"members": None,
Expand Down

0 comments on commit 8c8943f

Please sign in to comment.