From 610ab926a45b36683cb276469120dc7c4d9d10a0 Mon Sep 17 00:00:00 2001 From: Takeshi KOMIYA Date: Thu, 16 Jul 2020 23:59:22 +0900 Subject: [PATCH] fix --- CHANGES | 1 + sphinx/ext/autodoc/__init__.py | 83 ++++++++++++++++++++++++---------- tests/test_ext_autodoc.py | 15 +++++- 3 files changed, 73 insertions(+), 26 deletions(-) diff --git a/CHANGES b/CHANGES index 27268df6625..4b02efdd6ab 100644 --- a/CHANGES +++ b/CHANGES @@ -41,6 +41,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 31f390a9915..80852b00234 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. @@ -892,7 +895,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 @@ -901,7 +904,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) @@ -1297,8 +1300,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: @@ -1612,7 +1615,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' @@ -1711,8 +1714,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 @@ -1879,15 +1882,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: @@ -1994,7 +2024,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' @@ -2025,7 +2055,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' @@ -2039,9 +2069,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,