From 93d6c212f7cbab727db7a643d0092837979bbaa4 Mon Sep 17 00:00:00 2001 From: Takeshi KOMIYA Date: Wed, 18 Nov 2020 22:02:04 +0900 Subject: [PATCH] Fix #8460: autodoc: Support custom types defined by typing.NewType A custom type defined by typing.NewType was rendered as a function because the generated type is a function having special attributes. This renders it as a variable. Note: The module name where the NewType object defined is lost on generating it. So it is hard to make cross-reference for these custom types. --- CHANGES | 1 + sphinx/ext/autodoc/__init__.py | 41 ++++++++++++++++--- sphinx/ext/autosummary/generate.py | 7 ++-- sphinx/util/inspect.py | 10 +++++ .../roots/test-ext-autodoc/target/typevar.py | 5 ++- tests/test_ext_autodoc.py | 8 ++++ tests/test_ext_autodoc_autodata.py | 15 +++++++ 7 files changed, 77 insertions(+), 10 deletions(-) diff --git a/CHANGES b/CHANGES index 5ad0d36a57a..a6cbd97340c 100644 --- a/CHANGES +++ b/CHANGES @@ -30,6 +30,7 @@ Features added value equal to None is set. * #8209: autodoc: Add ``:no-value:`` option to :rst:dir:`autoattribute` and :rst:dir:`autodata` directive to suppress the default value of the variable +* #8460: autodoc: Support custom types defined by typing.NewType * #6914: Add a new event :event:`warn-missing-reference` to custom warning messages when failed to resolve a cross-reference * #6914: Emit a detailed warning when failed to resolve a ``:ref:`` reference diff --git a/sphinx/ext/autodoc/__init__.py b/sphinx/ext/autodoc/__init__.py index 278e47d886d..47d3d4c45f7 100644 --- a/sphinx/ext/autodoc/__init__.py +++ b/sphinx/ext/autodoc/__init__.py @@ -1714,7 +1714,12 @@ def import_object(self, raiseerror: bool = False) -> bool: def add_directive_header(self, sig: str) -> None: super().add_directive_header(sig) sourcename = self.get_sourcename() - if not self.options.annotation: + if self.options.annotation is SUPPRESS or inspect.isNewType(self.object): + pass + elif self.options.annotation: + self.add_line(' :annotation: %s' % self.options.annotation, + sourcename) + else: # obtain annotation for this data annotations = get_type_hints(self.parent, None, self.config.autodoc_type_aliases) if self.objpath[-1] in annotations: @@ -1734,11 +1739,6 @@ def add_directive_header(self, sig: str) -> None: self.add_line(' :value: ' + objrepr, sourcename) except ValueError: pass - elif self.options.annotation is SUPPRESS: - pass - else: - self.add_line(' :annotation: %s' % self.options.annotation, - sourcename) def document_members(self, all_members: bool = False) -> None: pass @@ -1753,8 +1753,18 @@ def add_content(self, more_content: Optional[StringList], no_docstring: bool = F # suppress docstring of the value super().add_content(more_content, no_docstring=True) else: + if not more_content: + more_content = StringList() + + if inspect.isNewType(self.object): + self.update_content_for_NewType(more_content) super().add_content(more_content, no_docstring=no_docstring) + def update_content_for_NewType(self, more_content: StringList) -> None: + supertype = restify(self.object.__supertype__) + more_content.append(_('alias of %s') % supertype, '') + more_content.append('', '') + class DataDeclarationDocumenter(DataDocumenter): """ @@ -1800,6 +1810,24 @@ def add_content(self, more_content: Optional[StringList], no_docstring: bool = F super().add_content(content) +class NewTypeDataDocumenter(DataDocumenter): + """ + Specialized Documenter subclass for NewTypes. + + Note: This must be invoked before FunctionDocumenter because NewType is a kind of + function object. + """ + + objtype = 'newtypedata' + directivetype = 'data' + priority = FunctionDocumenter.priority + 1 + + @classmethod + def can_document_member(cls, member: Any, membername: str, isattr: bool, parent: Any + ) -> bool: + return inspect.isNewType(member) and isattr + + class TypeVarDocumenter(DataDocumenter): """ Specialized Documenter subclass for TypeVars. @@ -2307,6 +2335,7 @@ def setup(app: Sphinx) -> Dict[str, Any]: app.add_autodocumenter(ExceptionDocumenter) app.add_autodocumenter(DataDocumenter) app.add_autodocumenter(GenericAliasDocumenter) + app.add_autodocumenter(NewTypeDataDocumenter) app.add_autodocumenter(TypeVarDocumenter) app.add_autodocumenter(FunctionDocumenter) app.add_autodocumenter(DecoratorDocumenter) diff --git a/sphinx/ext/autosummary/generate.py b/sphinx/ext/autosummary/generate.py index f6a84b4e423..5a70863327d 100644 --- a/sphinx/ext/autosummary/generate.py +++ b/sphinx/ext/autosummary/generate.py @@ -89,11 +89,12 @@ def setup_documenters(app: Any) -> None: DecoratorDocumenter, ExceptionDocumenter, FunctionDocumenter, GenericAliasDocumenter, InstanceAttributeDocumenter, MethodDocumenter, - ModuleDocumenter, PropertyDocumenter, - SingledispatchFunctionDocumenter, SlotsAttributeDocumenter) + ModuleDocumenter, NewTypeDataDocumenter, + PropertyDocumenter, SingledispatchFunctionDocumenter, + SlotsAttributeDocumenter) documenters = [ ModuleDocumenter, ClassDocumenter, ExceptionDocumenter, DataDocumenter, - FunctionDocumenter, MethodDocumenter, AttributeDocumenter, + FunctionDocumenter, MethodDocumenter, NewTypeDataDocumenter, AttributeDocumenter, InstanceAttributeDocumenter, DecoratorDocumenter, PropertyDocumenter, SlotsAttributeDocumenter, GenericAliasDocumenter, SingledispatchFunctionDocumenter, ] # type: List[Type[Documenter]] diff --git a/sphinx/util/inspect.py b/sphinx/util/inspect.py index c7375aa57df..698dd85d24c 100644 --- a/sphinx/util/inspect.py +++ b/sphinx/util/inspect.py @@ -174,6 +174,16 @@ def getslots(obj: Any) -> Optional[Dict]: raise ValueError +def isNewType(obj: Any) -> bool: + """Check the if object is a kind of NewType.""" + __module__ = safe_getattr(obj, '__module__', None) + __qualname__ = safe_getattr(obj, '__qualname__', None) + if __module__ == 'typing' and __qualname__ == 'NewType..new_type': + return True + else: + return False + + def isenumclass(x: Any) -> bool: """Check if the object is subclass of enum.""" return inspect.isclass(x) and issubclass(x, enum.Enum) diff --git a/tests/roots/test-ext-autodoc/target/typevar.py b/tests/roots/test-ext-autodoc/target/typevar.py index 9c6b0eab0bb..f71efd4c245 100644 --- a/tests/roots/test-ext-autodoc/target/typevar.py +++ b/tests/roots/test-ext-autodoc/target/typevar.py @@ -1,4 +1,4 @@ -from typing import TypeVar +from typing import NewType, TypeVar #: T1 T1 = TypeVar("T1") @@ -13,3 +13,6 @@ #: T5 T5 = TypeVar("T5", contravariant=True) + +#: T6 +T6 = NewType("T6", int) diff --git a/tests/test_ext_autodoc.py b/tests/test_ext_autodoc.py index d8e1f730e87..1354e3460a0 100644 --- a/tests/test_ext_autodoc.py +++ b/tests/test_ext_autodoc.py @@ -1758,6 +1758,14 @@ def test_autodoc_TypeVar(app): ' T5', '', " alias of TypeVar('T5', contravariant=True)", + '', + '.. py:data:: T6', + ' :module: target.typevar', + '', + ' T6', + '', + ' alias of :class:`int`', + '', ] diff --git a/tests/test_ext_autodoc_autodata.py b/tests/test_ext_autodoc_autodata.py index a4f8ca78aa3..9c765676f0e 100644 --- a/tests/test_ext_autodoc_autodata.py +++ b/tests/test_ext_autodoc_autodata.py @@ -73,3 +73,18 @@ def test_autodata_type_comment(app): ' attr3', '', ] + + +@pytest.mark.sphinx('html', testroot='ext-autodoc') +def test_autodata_NewType(app): + actual = do_autodoc(app, 'data', 'target.typevar.T6') + assert list(actual) == [ + '', + '.. py:data:: T6', + ' :module: target.typevar', + '', + ' T6', + '', + ' alias of :class:`int`', + '', + ]