From 5e4e44c19598aaeeda15422422cf5eec8136a9ea Mon Sep 17 00:00:00 2001 From: Takeshi KOMIYA Date: Sat, 1 Feb 2020 13:38:26 +0900 Subject: [PATCH] autodoc: Support type annotations for variables --- CHANGES | 1 + sphinx/ext/autodoc/__init__.py | 34 +++++++++++++++++++----- tests/test_autodoc.py | 48 ++++++++++++++++++---------------- 3 files changed, 54 insertions(+), 29 deletions(-) diff --git a/CHANGES b/CHANGES index a761731b8..b8b42e51c 100644 --- a/CHANGES +++ b/CHANGES @@ -47,6 +47,7 @@ Features added images (imagesize-1.2.0 or above is required) * #6994: imgconverter: Support illustrator file (.ai) to .png conversion * autodoc: Support Positional-Only Argument separator (PEP-570 compliant) +* autodoc: Support type annotations for variables * #2755: autodoc: Add new event: :event:`autodoc-before-process-signature` * #2755: autodoc: Support type_comment style (ex. ``# type: (str) -> str``) annotation (python3.8+ or `typed_ast `_ diff --git a/sphinx/ext/autodoc/__init__.py b/sphinx/ext/autodoc/__init__.py index e42169684..682fa1cdd 100644 --- a/sphinx/ext/autodoc/__init__.py +++ b/sphinx/ext/autodoc/__init__.py @@ -10,6 +10,7 @@ :license: BSD, see LICENSE for details. """ +import importlib import re import warnings from types import ModuleType @@ -33,6 +34,7 @@ from sphinx.util import logging from sphinx.util import rpartition from sphinx.util.docstrings import prepare_docstring from sphinx.util.inspect import getdoc, object_description, safe_getattr, stringify_signature +from sphinx.util.typing import stringify as stringify_typehint if False: # For type annotation @@ -1233,11 +1235,18 @@ class DataDocumenter(ModuleLevelDocumenter): sourcename = self.get_sourcename() if not self.options.annotation: try: - objrepr = object_description(self.object) + annotations = getattr(self.parent, '__annotations__', {}) + if self.objpath[-1] in annotations: + objrepr = stringify_typehint(annotations.get(self.objpath[-1])) + self.add_line(' :type: ' + objrepr, sourcename) + except ValueError: + pass + + try: + objrepr = object_description(self.object) + self.add_line(' :value: ' + objrepr, sourcename) except ValueError: pass - else: - self.add_line(' :annotation: = ' + objrepr, sourcename) elif self.options.annotation is SUPPRESS: pass else: @@ -1276,6 +1285,12 @@ class DataDeclarationDocumenter(DataDocumenter): """Never import anything.""" # disguise as a data self.objtype = 'data' + try: + # import module to obtain type annotation + self.parent = importlib.import_module(self.modname) + except ImportError: + pass + return True def add_content(self, more_content: Any, no_docstring: bool = False) -> None: @@ -1405,11 +1420,18 @@ class AttributeDocumenter(DocstringStripSignatureMixin, ClassLevelDocumenter): if not self.options.annotation: if not self._datadescriptor: try: - objrepr = object_description(self.object) + annotations = getattr(self.parent, '__annotations__', {}) + if self.objpath[-1] in annotations: + objrepr = stringify_typehint(annotations.get(self.objpath[-1])) + self.add_line(' :type: ' + objrepr, sourcename) + except ValueError: + pass + + try: + objrepr = object_description(self.object) + self.add_line(' :value: ' + objrepr, sourcename) except ValueError: pass - else: - self.add_line(' :annotation: = ' + objrepr, sourcename) elif self.options.annotation is SUPPRESS: pass else: diff --git a/tests/test_autodoc.py b/tests/test_autodoc.py index b7c645be8..c8ab55479 100644 --- a/tests/test_autodoc.py +++ b/tests/test_autodoc.py @@ -906,7 +906,7 @@ def test_autodoc_module_scope(app): '', '.. py:attribute:: Class.mdocattr', ' :module: target', - ' :annotation: = <_io.StringIO object>', + ' :value: <_io.StringIO object>', '', ' should be documented as well - süß', ' ' @@ -922,7 +922,7 @@ def test_autodoc_class_scope(app): '', '.. py:attribute:: Class.mdocattr', ' :module: target', - ' :annotation: = <_io.StringIO object>', + ' :value: <_io.StringIO object>', '', ' should be documented as well - süß', ' ' @@ -942,12 +942,12 @@ def test_class_attributes(app): ' ', ' .. py:attribute:: AttCls.a1', ' :module: target', - ' :annotation: = hello world', + ' :value: hello world', ' ', ' ', ' .. py:attribute:: AttCls.a2', ' :module: target', - ' :annotation: = None', + ' :value: None', ' ' ] @@ -966,7 +966,7 @@ def test_instance_attributes(app): ' ', ' .. py:attribute:: InstAttCls.ca1', ' :module: target', - " :annotation: = 'a'", + " :value: 'a'", ' ', ' Doc comment for class attribute InstAttCls.ca1.', ' It can have multiple lines.', @@ -974,28 +974,28 @@ def test_instance_attributes(app): ' ', ' .. py:attribute:: InstAttCls.ca2', ' :module: target', - " :annotation: = 'b'", + " :value: 'b'", ' ', ' Doc comment for InstAttCls.ca2. One line only.', ' ', ' ', ' .. py:attribute:: InstAttCls.ca3', ' :module: target', - " :annotation: = 'c'", + " :value: 'c'", ' ', ' Docstring for class attribute InstAttCls.ca3.', ' ', ' ', ' .. py:attribute:: InstAttCls.ia1', ' :module: target', - ' :annotation: = None', + ' :value: None', ' ', ' Doc comment for instance attribute InstAttCls.ia1', ' ', ' ', ' .. py:attribute:: InstAttCls.ia2', ' :module: target', - ' :annotation: = None', + ' :value: None', ' ', ' Docstring for instance attribute InstAttCls.ia2.', ' ' @@ -1014,7 +1014,7 @@ def test_instance_attributes(app): ' ', ' .. py:attribute:: InstAttCls.ca1', ' :module: target', - " :annotation: = 'a'", + " :value: 'a'", ' ', ' Doc comment for class attribute InstAttCls.ca1.', ' It can have multiple lines.', @@ -1022,7 +1022,7 @@ def test_instance_attributes(app): ' ', ' .. py:attribute:: InstAttCls.ia1', ' :module: target', - ' :annotation: = None', + ' :value: None', ' ', ' Doc comment for instance attribute InstAttCls.ia1', ' ' @@ -1090,28 +1090,28 @@ def test_enum_class(app): ' ', ' .. py:attribute:: EnumCls.val1', ' :module: target.enum', - ' :annotation: = 12', + ' :value: 12', ' ', ' doc for val1', ' ', ' ', ' .. py:attribute:: EnumCls.val2', ' :module: target.enum', - ' :annotation: = 23', + ' :value: 23', ' ', ' doc for val2', ' ', ' ', ' .. py:attribute:: EnumCls.val3', ' :module: target.enum', - ' :annotation: = 34', + ' :value: 34', ' ', ' doc for val3', ' ', ' ', ' .. py:attribute:: EnumCls.val4', ' :module: target.enum', - ' :annotation: = 34', + ' :value: 34', ' ' ] @@ -1121,7 +1121,7 @@ def test_enum_class(app): '', '.. py:attribute:: EnumCls.val1', ' :module: target.enum', - ' :annotation: = 12', + ' :value: 12', '', ' doc for val1', ' ' @@ -1405,38 +1405,40 @@ def test_autodoc_typed_instance_variables(app): ' ', ' .. py:attribute:: Class.attr1', ' :module: target.typed_vars', - ' :annotation: = 0', + ' :type: int', + ' :value: 0', ' ', ' ', ' .. py:attribute:: Class.attr2', ' :module: target.typed_vars', - ' :annotation: = None', + ' :value: None', ' ', ' ', ' .. py:attribute:: Class.attr3', ' :module: target.typed_vars', - ' :annotation: = None', + ' :value: None', ' ', ' attr3', ' ', ' ', ' .. py:attribute:: Class.attr4', ' :module: target.typed_vars', - ' :annotation: = None', + ' :value: None', ' ', ' attr4', ' ', '', '.. py:data:: attr1', ' :module: target.typed_vars', - " :annotation: = ''", + ' :type: str', + " :value: ''", '', ' attr1', ' ', '', '.. py:data:: attr2', ' :module: target.typed_vars', - " :annotation: = None", + " :value: None", '', ' attr2', ' ' @@ -1455,7 +1457,7 @@ def test_autodoc_for_egged_code(app): '', '.. py:data:: CONSTANT', ' :module: sample', - ' :annotation: = 1', + ' :value: 1', '', ' constant on sample.py', ' ',