Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Close #2755: autodoc: Support type_comment style annotation #6982

Closed
wants to merge 1 commit into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
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)
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think this is good way to do this. But we have to let handlers know the target object is bound method or not...

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[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(str(getvalue(e)) for e in typ.elts)
tk0miya marked this conversation as resolved.
Show resolved Hide resolved
elif isinstance(typ, (ast.Constant, ast.NameConstant)): # type: ignore
return repr(typ.value)
else:
raise NotImplementedError
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
raise NotImplementedError
raise NotImplementedError('Unable to render {} instances'.format(type(type).__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}
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, Literal['ham', 'eggs'], 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: Literal['ham', 'eggs'], 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