diff --git a/CHANGES b/CHANGES index 07b96779758..46f4c7f754e 100644 --- a/CHANGES +++ b/CHANGES @@ -35,6 +35,10 @@ 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) +* #2755: autodoc: Support type_comment style (ex. ``# type: (str) -> str``) + annotation (python3.8+ or `typed_ast `_ + is required) +* #2755: autodoc: Add new event: :event:`autodoc-before-format-args` Bugs fixed ---------- diff --git a/doc/usage/extensions/autodoc.rst b/doc/usage/extensions/autodoc.rst index 3bb9630bd4b..4a7ea3f3c81 100644 --- a/doc/usage/extensions/autodoc.rst +++ b/doc/usage/extensions/autodoc.rst @@ -494,6 +494,17 @@ autodoc provides the following additional events: auto directive :param lines: the lines of the docstring, see above +.. event:: autodoc-before-process-signature (app, obj, bound_method) + + .. versionadded:: 2.4 + + Emitted before autodoc formats a signature for an object. The event handler + can modify an object to change its signature. + + :param app: the Sphinx application object + :param obj: the object itself + :param bound_method: a boolean indicates an object is bound method or not + .. event:: autodoc-process-signature (app, what, name, obj, options, signature, return_annotation) .. versionadded:: 0.5 diff --git a/sphinx/ext/autodoc/__init__.py b/sphinx/ext/autodoc/__init__.py index 2124b2d25fa..3012b97c953 100644 --- a/sphinx/ext/autodoc/__init__.py +++ b/sphinx/ext/autodoc/__init__.py @@ -983,8 +983,11 @@ 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-process-signature', + self.object.__call__, False) sig = inspect.signature(self.object.__call__) else: + self.env.app.emit('autodoc-before-process-signature', self.object, False) sig = inspect.signature(self.object) args = stringify_signature(sig, **kwargs) except TypeError: @@ -996,9 +999,13 @@ 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-process-signature', + self.object.__new__, True) sig = inspect.signature(self.object.__new__, bound_method=True) args = stringify_signature(sig, show_return_annotation=False, **kwargs) except TypeError: + self.env.app.emit('autodoc-before-process-signature', + self.object.__init__, True) sig = inspect.signature(self.object.__init__, bound_method=True) args = stringify_signature(sig, show_return_annotation=False, **kwargs) @@ -1081,6 +1088,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-process-signature', initmeth, True) sig = inspect.signature(initmeth, bound_method=True) return stringify_signature(sig, show_return_annotation=False, **kwargs) except TypeError: @@ -1284,8 +1292,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-process-signature', self.object, False) sig = inspect.signature(self.object, bound_method=False) else: + self.env.app.emit('autodoc-before-process-signature', self.object, True) sig = inspect.signature(self.object, bound_method=True) args = stringify_signature(sig, **kwargs) @@ -1558,10 +1568,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-process-signature') 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.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..a168c428615 --- /dev/null +++ b/sphinx/ext/autodoc/type_comment.py @@ -0,0 +1,69 @@ +""" + 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: + 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]) + else: + module = ast_parse(source) + subject = cast(ast.FunctionDef, module.body[0]) + + if subject.type_comment is None: + return None + else: + return ast_parse(subject.type_comment, mode='func_type') + 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) + except NotImplementedError as exc: # failed to ast.unparse() + logger.warning(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', ''