diff --git a/CHANGES b/CHANGES index c1421fef847..3eeb9333992 100644 --- a/CHANGES +++ b/CHANGES @@ -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 diff --git a/sphinx/ext/autodoc/__init__.py b/sphinx/ext/autodoc/__init__.py index 2dab6807dbf..8c3509c3bbb 100644 --- a/sphinx/ext/autodoc/__init__.py +++ b/sphinx/ext/autodoc/__init__.py @@ -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*. @@ -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. @@ -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 @@ -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) @@ -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: @@ -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' @@ -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 @@ -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: @@ -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' @@ -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' @@ -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.""" diff --git a/tests/test_ext_autodoc.py b/tests/test_ext_autodoc.py index 7b4823a2f8c..d3524ac9dee 100644 --- a/tests/test_ext_autodoc.py +++ b/tests/test_ext_autodoc.py @@ -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) == [ @@ -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,