From 5ba725724d63d120cea62d7a09c15f5700a9b44d Mon Sep 17 00:00:00 2001 From: Takeshi KOMIYA Date: Sun, 20 Dec 2020 19:41:19 +0900 Subject: [PATCH] refactor: Add UninitializedInstanceAttributeMixin Add a new mix-in for uninitialized instance attributes that is defined with annotation but not initialized (based on PEP-526). --- sphinx/ext/autodoc/__init__.py | 77 ++++++++++++++++++++++++---------- 1 file changed, 55 insertions(+), 22 deletions(-) diff --git a/sphinx/ext/autodoc/__init__.py b/sphinx/ext/autodoc/__init__.py index 78ded1463..d1dfbcf25 100644 --- a/sphinx/ext/autodoc/__init__.py +++ b/sphinx/ext/autodoc/__init__.py @@ -2203,10 +2203,59 @@ class RuntimeInstanceAttributeMixin(DataDocumenterMixinBase): super().should_suppress_value_header()) +class UninitializedInstanceAttributeMixin(DataDocumenterMixinBase): + """ + Mixin for AttributeDocumenter to provide the feature for supporting uninitialized + instance attributes (PEP-526 styled, annotation only attributes). + + Example: + + class Foo: + attr: int #: This is a target of this mix-in. + """ + + def is_uninitialized_instance_attribute(self, parent: Any) -> bool: + """Check the subject is an annotation only attribute.""" + annotations = get_type_hints(parent, None, self.config.autodoc_type_aliases) + if self.objpath[-1] in annotations: + return True + else: + return False + + def import_object(self, raiseerror: bool = False) -> bool: + """Check the exisitence of uninitialized instance attribute when failed to import + the attribute.""" + 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 is UNINITIALIZED_ATTR or + super().should_suppress_value_header()) + + class AttributeDocumenter(GenericAliasMixin, NewTypeMixin, SlotsMixin, # type: ignore TypeVarMixin, RuntimeInstanceAttributeMixin, - NonDataDescriptorMixin, DocstringStripSignatureMixin, - ClassLevelDocumenter): + UninitializedInstanceAttributeMixin, NonDataDescriptorMixin, + DocstringStripSignatureMixin, ClassLevelDocumenter): """ Specialized Documenter subclass for attributes. """ @@ -2283,21 +2332,9 @@ class AttributeDocumenter(GenericAliasMixin, NewTypeMixin, SlotsMixin, # type: pass 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 - except ImportError as exc: - if self.isinstanceattribute(): - self.object = INSTANCEATTR - ret = True - elif raiseerror: - raise - else: - logger.warning(exc.args[0], type='autodoc', subtype='import_object') - self.env.note_reread() - ret = False - + ret = super().import_object(raiseerror) + if inspect.isenumattribute(self.object): + self.object = self.object.value if self.parent: self.update_annotations(self.parent) @@ -2322,8 +2359,7 @@ class AttributeDocumenter(GenericAliasMixin, NewTypeMixin, SlotsMixin, # type: self.add_line(' :type: ' + objrepr, sourcename) try: - if (self.object is INSTANCEATTR or self.options.no_value or - self.should_suppress_value_header()): + if self.options.no_value or self.should_suppress_value_header(): pass else: objrepr = object_description(self.object) @@ -2357,9 +2393,6 @@ class AttributeDocumenter(GenericAliasMixin, NewTypeMixin, SlotsMixin, # type: if comment: return [comment] - if self.object is INSTANCEATTR: - return [] - try: # Disable `autodoc_inherit_docstring` temporarily to avoid to obtain # a docstring from the value which descriptor returns unexpectedly.