From ffdfb6cb877421009bcdf70715cf15ffeefbb015 Mon Sep 17 00:00:00 2001 From: Takeshi KOMIYA Date: Wed, 1 Jan 2020 23:00:44 +0900 Subject: [PATCH] Close #2755: autodoc: Support type_comment style annotation Note: python3.8+ or typed_ast is required --- CHANGES | 3 + sphinx/ext/autodoc/__init__.py | 1 + sphinx/ext/autodoc/type_comment.py | 74 +++++++++++++++++++ .../test-ext-autodoc/target/typehints.py | 14 ++++ tests/test_ext_autodoc_configs.py | 25 +++++++ 5 files changed, 117 insertions(+) create mode 100644 sphinx/ext/autodoc/type_comment.py diff --git a/CHANGES b/CHANGES index f6f1d44fd4b..31f18b850ac 100644 --- a/CHANGES +++ b/CHANGES @@ -36,6 +36,9 @@ Features added * #6994: imgconverter: Support illustrator file (.ai) to .png conversion * autodoc: Support Positional-Only Argument separator (PEP-570 compliant) * #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 `_ + is required) * SphinxTranslator now calls visitor/departure method for super node class if visitor/departure method for original node class not found diff --git a/sphinx/ext/autodoc/__init__.py b/sphinx/ext/autodoc/__init__.py index 1dc2d0f69fd..3012b97c953 100644 --- a/sphinx/ext/autodoc/__init__.py +++ b/sphinx/ext/autodoc/__init__.py @@ -1574,5 +1574,6 @@ def setup(app: Sphinx) -> Dict[str, Any]: app.add_event('autodoc-skip-member') app.connect('config-inited', merge_autodoc_default_flags) + app.setup_extension('sphinx.ext.autodoc.type_comment') return {'version': sphinx.__display_version__, 'parallel_read_safe': True} diff --git a/sphinx/ext/autodoc/type_comment.py b/sphinx/ext/autodoc/type_comment.py new file mode 100644 index 00000000000..c94020bf011 --- /dev/null +++ b/sphinx/ext/autodoc/type_comment.py @@ -0,0 +1,74 @@ +""" + sphinx.ext.autodoc.type_comment + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + + Update annotations info of living objects using type_comments. + + :copyright: Copyright 2007-2020 by the Sphinx team, see AUTHORS. + :license: BSD, see LICENSE for details. +""" + +import ast +from inspect import getsource +from typing import Any, Dict +from typing import cast + +import sphinx +from sphinx.application import Sphinx +from sphinx.pycode.ast import parse as ast_parse +from sphinx.pycode.ast import unparse as ast_unparse +from sphinx.util import inspect +from sphinx.util import logging + +logger = logging.getLogger(__name__) + + +def get_type_comment(obj: Any) -> ast.FunctionDef: + """Get type_comment'ed FunctionDef object from living object. + + This tries to parse original code for living object and returns + AST node for given *obj*. It requires py38+ or typed_ast module. + """ + try: + source = getsource(obj) + if source.startswith((' ', r'\t')): + # subject is placed inside class or block. To read its docstring, + # this adds if-block before the declaration. + module = ast_parse('if True:\n' + source) + subject = cast(ast.FunctionDef, module.body[0].body[0]) # type: ignore + else: + module = ast_parse(source) + subject = cast(ast.FunctionDef, module.body[0]) # type: ignore + + if getattr(subject, "type_comment", None): + return ast_parse(subject.type_comment, mode='func_type') # type: ignore + else: + return None + except (OSError, TypeError): # failed to load source code + return None + except SyntaxError: # failed to parse type_comments + return None + + +def update_annotations_using_type_comments(app: Sphinx, obj: Any, bound_method: bool) -> None: + """Update annotations info of *obj* using type_comments.""" + try: + function = get_type_comment(obj) + if function and hasattr(function, 'argtypes'): + if function.argtypes != [ast.Ellipsis]: # type: ignore + sig = inspect.signature(obj, bound_method) + for i, param in enumerate(sig.parameters.values()): + if param.name not in obj.__annotations__: + annotation = ast_unparse(function.argtypes[i]) # type: ignore + obj.__annotations__[param.name] = annotation + + if 'return' not in obj.__annotations__: + obj.__annotations__['return'] = ast_unparse(function.returns) # type: ignore + except NotImplementedError as exc: # failed to ast.unparse() + logger.warning("Failed to parse type_comment for %r: %s", obj, exc) + + +def setup(app: Sphinx) -> Dict[str, Any]: + app.connect('autodoc-before-process-signature', update_annotations_using_type_comments) + + return {'version': sphinx.__display_version__, 'parallel_read_safe': True} diff --git a/tests/roots/test-ext-autodoc/target/typehints.py b/tests/roots/test-ext-autodoc/target/typehints.py index eedaab3b997..842530c13e7 100644 --- a/tests/roots/test-ext-autodoc/target/typehints.py +++ b/tests/roots/test-ext-autodoc/target/typehints.py @@ -2,9 +2,23 @@ def incr(a: int, b: int = 1) -> int: return a + b +def decr(a, b = 1): + # type: (int, int) -> int + return a - b + + class Math: def __init__(self, s: str, o: object = None) -> None: pass def incr(self, a: int, b: int = 1) -> int: return a + b + + def decr(self, a, b = 1): + # type: (int, int) -> int + return a - b + + +def complex_func(arg1, arg2, arg3=None, *args, **kwargs): + # type: (str, List[int], Tuple[int, Union[str, Unknown]], *str, **str) -> None + pass diff --git a/tests/test_ext_autodoc_configs.py b/tests/test_ext_autodoc_configs.py index 81fd5a49c47..0da91c7f028 100644 --- a/tests/test_ext_autodoc_configs.py +++ b/tests/test_ext_autodoc_configs.py @@ -479,10 +479,23 @@ def test_autodoc_typehints_signature(app): ' :module: target.typehints', '', ' ', + ' .. py:method:: Math.decr(a: int, b: int = 1) -> int', + ' :module: target.typehints', + ' ', + ' ', ' .. py:method:: Math.incr(a: int, b: int = 1) -> int', ' :module: target.typehints', ' ', '', + '.. py:function:: complex_func(arg1: str, arg2: List[int], arg3: Tuple[int, ' + 'Union[str, Unknown]] = None, *args: str, **kwargs: str) -> None', + ' :module: target.typehints', + '', + '', + '.. py:function:: decr(a: int, b: int = 1) -> int', + ' :module: target.typehints', + '', + '', '.. py:function:: incr(a: int, b: int = 1) -> int', ' :module: target.typehints', '' @@ -505,10 +518,22 @@ def test_autodoc_typehints_none(app): ' :module: target.typehints', '', ' ', + ' .. py:method:: Math.decr(a, b=1)', + ' :module: target.typehints', + ' ', + ' ', ' .. py:method:: Math.incr(a, b=1)', ' :module: target.typehints', ' ', '', + '.. py:function:: complex_func(arg1, arg2, arg3=None, *args, **kwargs)', + ' :module: target.typehints', + '', + '', + '.. py:function:: decr(a, b=1)', + ' :module: target.typehints', + '', + '', '.. py:function:: incr(a, b=1)', ' :module: target.typehints', ''