diff --git a/CHANGES b/CHANGES index fa0b2a2b11e..7cc38490ac3 100644 --- a/CHANGES +++ b/CHANGES @@ -30,6 +30,9 @@ Features added down the build * #6837: LaTeX: Support a nested table * #6966: graphviz: Support ``:class:`` option +* #2755: autodoc: Support type_comment style (ex. ``# type: (str) -> str``) + annotation (python3.8+ or `typed_ast `_ + is required) Bugs fixed ---------- diff --git a/sphinx/ext/autodoc/__init__.py b/sphinx/ext/autodoc/__init__.py index 8c5ace92aee..c00b63d876e 100644 --- a/sphinx/ext/autodoc/__init__.py +++ b/sphinx/ext/autodoc/__init__.py @@ -983,8 +983,10 @@ def format_args(self, **kwargs: Any) -> str: not inspect.isbuiltin(self.object) and not inspect.isclass(self.object) and hasattr(self.object, '__call__')): + self.env.app.emit('autodoc-before-format-args', self.object.__call__, False) args = Signature(self.object.__call__).format_args(**kwargs) else: + self.env.app.emit('autodoc-before-format-args', self.object, False) args = Signature(self.object).format_args(**kwargs) except TypeError: if (inspect.is_builtin_class_method(self.object, '__new__') and @@ -995,9 +997,11 @@ def format_args(self, **kwargs: Any) -> str: # typing) we try to use the constructor signature as function # signature without the first argument. try: + self.env.app.emit('autodoc-before-format-args', self.object.__new__, True) sig = Signature(self.object.__new__, bound_method=True, has_retval=False) args = sig.format_args(**kwargs) except TypeError: + self.env.app.emit('autodoc-before-format-args', self.object.__init__, True) sig = Signature(self.object.__init__, bound_method=True, has_retval=False) args = sig.format_args(**kwargs) @@ -1080,6 +1084,7 @@ def format_args(self, **kwargs: Any) -> str: not(inspect.ismethod(initmeth) or inspect.isfunction(initmeth)): return None try: + self.env.app.emit('autodoc-before-format-args', initmeth, True) sig = Signature(initmeth, bound_method=True, has_retval=False) return sig.format_args(**kwargs) except TypeError: @@ -1283,8 +1288,10 @@ def format_args(self, **kwargs: Any) -> str: # can never get arguments of a C function or method return None if inspect.isstaticmethod(self.object, cls=self.parent, name=self.object_name): + self.env.app.emit('autodoc-before-format-args', self.object, False) args = Signature(self.object, bound_method=False).format_args(**kwargs) else: + self.env.app.emit('autodoc-before-format-args', self.object, True) args = Signature(self.object, bound_method=True).format_args(**kwargs) # escape backslashes for reST args = args.replace('\\', '\\\\') @@ -1555,10 +1562,12 @@ def setup(app: Sphinx) -> Dict[str, Any]: app.add_config_value('autodoc_typehints', "signature", True, ENUM("signature", "none")) app.add_config_value('autodoc_warningiserror', True, True) app.add_config_value('autodoc_inherit_docstrings', True, True) + app.add_event('autodoc-before-format-args') app.add_event('autodoc-process-docstring') app.add_event('autodoc-process-signature') app.add_event('autodoc-skip-member') app.connect('config-inited', merge_autodoc_default_flags) + app.setup_extension('sphinx.ext.autodoc.typehints') return {'version': sphinx.__display_version__, 'parallel_read_safe': True} diff --git a/sphinx/ext/autodoc/typehints.py b/sphinx/ext/autodoc/typehints.py new file mode 100644 index 00000000000..9a1f5f81fdc --- /dev/null +++ b/sphinx/ext/autodoc/typehints.py @@ -0,0 +1,109 @@ +""" + sphinx.ext.autodoc.typehints + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + + 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 inspect +import sys +from typing import Any, Dict, Generator, Union + +import sphinx +from sphinx.application import Sphinx + +if sys.version_info > (3, 8): + import ast +else: + try: + from typed_ast import ast3 as ast + except ImportError: + ast = None + + +def ast_parse(code: str, mode: str = 'exec') -> "ast.AST": + """Parse the *code* using built-in ast or typed_ast.""" + try: + return ast.parse(code, mode=mode, type_comments=True) # type: ignore + except TypeError: + # fallback to typed_ast3 + return ast.parse(code, mode=mode) + + +def iter_args(func: "Union[ast.FunctionDef, ast.AsyncFunctionDef]" + ) -> Generator["ast.arg", None, None]: + """Get an iterator for arguments names from FunctionDef node.""" + if hasattr(func.args, "posonlyargs"): # py38 or above + yield from func.args.posonlyargs # type: ignore + yield from func.args.args + if func.args.vararg: + yield func.args.vararg + if func.args.kwarg: + yield func.args.kwarg + + +def get_type_hints_from_type_comment(obj: Any, bound_method: bool) -> Dict[str, str]: + """Get type hints from type_comment style annotation.""" + def getvalue(typ: ast.AST) -> str: + if isinstance(typ, ast.Name): + return typ.id + elif isinstance(typ, ast.Subscript): + return "%s[%s]" % (getvalue(typ.value), getvalue(typ.slice)) + elif isinstance(typ, ast.Index): + return getvalue(typ.value) + elif isinstance(typ, ast.Tuple): + return ', '.join(getvalue(e) for e in typ.elts) + elif isinstance(typ, (ast.Constant, ast.NameConstant)): # type: ignore + return repr(typ.value) + else: + raise NotImplementedError('Unable to render %s instance' % type(typ).__name__) + + try: + source = inspect.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 = module.body[0].body[0] # type: ignore + else: + module = ast_parse(source) + subject = module.body[0] # type: ignore + + if subject.type_comment is None: # no type_comment + return {} + else: + func = ast.parse(subject.type_comment, mode='func_type') # type: Any + + type_hints = {} # type: Dict[str, Any] + type_hints['return'] = getvalue(func.returns) + + if func.argtypes != [ast.Ellipsis]: + args = iter_args(subject) + if bound_method: + next(args) + + for name, argtype in zip(args, func.argtypes): + type_hints[name.arg] = getvalue(argtype) + + return type_hints + except (OSError, TypeError): # failed to load source code + return {} + except SyntaxError: # failed to parse type_comments + return {} + + +def update_annotations_using_type_comments(app: Sphinx, obj: Any, bound_method: bool) -> None: + """Update annotations info of *obj* using type_comments.""" + if callable(obj) and hasattr(obj, '__annotations__'): + type_hints = get_type_hints_from_type_comment(obj, bound_method) + for key, typ in type_hints.items(): + obj.__annotations__[key] = typ + + +def setup(app: Sphinx) -> Dict[str, Any]: + app.connect('autodoc-before-format-args', 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', ''