Skip to content

Commit

Permalink
Close sphinx-doc#2755: autodoc: Support type_comment style annotation
Browse files Browse the repository at this point in the history
Note: python3.8+ or typed_ast is required
  • Loading branch information
tk0miya committed Jan 11, 2020
1 parent 60fc8fc commit b930af1
Show file tree
Hide file tree
Showing 6 changed files with 135 additions and 0 deletions.
4 changes: 4 additions & 0 deletions CHANGES
Expand Up @@ -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 <https://github.com/python/typed_ast>`_
is required)
* #2755: autodoc: Add new event: :event:`autodoc-before-format-args`

Bugs fixed
----------
Expand Down
11 changes: 11 additions & 0 deletions doc/usage/extensions/autodoc.rst
Expand Up @@ -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
Expand Down
12 changes: 12 additions & 0 deletions sphinx/ext/autodoc/__init__.py
Expand Up @@ -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:
Expand All @@ -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)

Expand Down Expand Up @@ -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:
Expand Down Expand Up @@ -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)

Expand Down Expand Up @@ -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}
69 changes: 69 additions & 0 deletions 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 getattr(subject, "type_comment", None):
return ast_parse(subject.type_comment, mode='func_type')
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)
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}
14 changes: 14 additions & 0 deletions tests/roots/test-ext-autodoc/target/typehints.py
Expand Up @@ -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
25 changes: 25 additions & 0 deletions tests/test_ext_autodoc_configs.py
Expand Up @@ -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',
''
Expand All @@ -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',
''
Expand Down

0 comments on commit b930af1

Please sign in to comment.