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 2, 2020
1 parent 577b80c commit dac0e13
Show file tree
Hide file tree
Showing 5 changed files with 163 additions and 0 deletions.
3 changes: 3 additions & 0 deletions CHANGES
Expand Up @@ -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 <https://github.com/python/typed_ast>`_
is required)

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

Expand Down Expand Up @@ -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:
Expand Down Expand Up @@ -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('\\', '\\\\')
Expand Down Expand Up @@ -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}
112 changes: 112 additions & 0 deletions sphinx/ext/autodoc/typehints.py
@@ -0,0 +1,112 @@
"""
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 ast is None: # failed to load ast module
return

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}
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 dac0e13

Please sign in to comment.