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 314be09
Show file tree
Hide file tree
Showing 7 changed files with 164 additions and 2 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}
109 changes: 109 additions & 0 deletions 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[str, None, None]:
"""Get an iterator for arguments names from FunctionDef node."""
if hasattr(func.args, "posonlyargs"): # py38 or above
yield from (a.arg for a in func.args.posonlyargs) # type: ignore
yield from (a.arg for a in func.args.args)
if func.args.vararg:
yield func.args.vararg.arg
if func.args.kwarg:
yield func.args.kwarg.arg


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) -> Any:
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(str(getvalue(e)) for e in typ.elts)
elif isinstance(typ, (ast.Constant, ast.NameConstant)): # type: ignore
return typ.value
else:
raise NotImplementedError

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] = 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}
3 changes: 2 additions & 1 deletion sphinx/util/inspect.py
Expand Up @@ -8,6 +8,7 @@
:license: BSD, see LICENSE for details.
"""

import ast
import builtins
import enum
import inspect
Expand All @@ -20,7 +21,7 @@
isclass, ismethod, ismethoddescriptor, isroutine
)
from io import StringIO
from typing import Any, Callable, Mapping, List, Tuple
from typing import Any, Callable, Dict, Generator, Mapping, List, Tuple, Union

from sphinx.deprecation import RemovedInSphinx30Warning
from sphinx.util import logging
Expand Down
16 changes: 16 additions & 0 deletions tests/roots/test-ext-autodoc/target/typehints.py
Expand Up @@ -2,9 +2,25 @@ 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
1 change: 0 additions & 1 deletion tests/typing_test_data.py
Expand Up @@ -96,7 +96,6 @@ def f19(*args: int, **kwargs: str):
pass



class Node:
def __init__(self, parent: Optional['Node']) -> None:
pass
Expand Down

0 comments on commit 314be09

Please sign in to comment.