Merge pull request #7152 from gpotter2/elipsispatch

Fix #7146: support (...) in type hint comments (V2)
This commit is contained in:
Takeshi KOMIYA 2020-02-16 20:35:40 +09:00 committed by GitHub
commit b80c7cd234
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
3 changed files with 122 additions and 15 deletions

View File

@ -8,13 +8,13 @@
:license: BSD, see LICENSE for details.
"""
import ast
from inspect import getsource
from typing import Any, Dict
from inspect import Parameter, Signature, getsource
from typing import Any, Dict, List
from typing import cast
import sphinx
from sphinx.application import Sphinx
from sphinx.pycode.ast import ast
from sphinx.pycode.ast import parse as ast_parse
from sphinx.pycode.ast import unparse as ast_unparse
from sphinx.util import inspect
@ -23,11 +23,73 @@ from sphinx.util import logging
logger = logging.getLogger(__name__)
def get_type_comment(obj: Any) -> ast.FunctionDef:
def not_suppressed(argtypes: List[ast.AST] = []) -> bool:
"""Check given *argtypes* is suppressed type_comment or not."""
if len(argtypes) == 0: # no argtypees
return False
elif len(argtypes) == 1 and ast_unparse(argtypes[0]) == "...": # suppressed
# Note: To support multiple versions of python, this uses ``ast_unparse()`` for
# comparison with Ellipsis. Since 3.8, ast.Constant has been used to represent
# Ellipsis node instead of ast.Ellipsis.
return False
else: # not suppressed
return True
def signature_from_ast(node: ast.FunctionDef, bound_method: bool,
type_comment: ast.FunctionDef) -> Signature:
"""Return a Signature object for the given *node*.
:param bound_method: Specify *node* is a bound method or not
"""
params = []
if hasattr(node.args, "posonlyargs"): # for py38+
for arg in node.args.posonlyargs: # type: ignore
param = Parameter(arg.arg, Parameter.POSITIONAL_ONLY, annotation=arg.type_comment)
params.append(param)
for arg in node.args.args:
param = Parameter(arg.arg, Parameter.POSITIONAL_OR_KEYWORD,
annotation=arg.type_comment or Parameter.empty)
params.append(param)
if node.args.vararg:
param = Parameter(node.args.vararg.arg, Parameter.VAR_POSITIONAL,
annotation=arg.type_comment or Parameter.empty)
params.append(param)
for arg in node.args.kwonlyargs:
param = Parameter(arg.arg, Parameter.KEYWORD_ONLY,
annotation=arg.type_comment or Parameter.empty)
params.append(param)
if node.args.kwarg:
param = Parameter(node.args.kwarg.arg, Parameter.VAR_KEYWORD,
annotation=arg.type_comment or Parameter.empty)
params.append(param)
# Remove first parameter when *obj* is bound_method
if bound_method and params:
params.pop(0)
# merge type_comment into signature
if not_suppressed(type_comment.argtypes): # type: ignore
for i, param in enumerate(params):
params[i] = param.replace(annotation=type_comment.argtypes[i]) # type: ignore
if node.returns:
return Signature(params, return_annotation=node.returns)
elif type_comment.returns:
return Signature(params, return_annotation=ast_unparse(type_comment.returns))
else:
return Signature(params)
def get_type_comment(obj: Any, bound_method: bool = False) -> Signature:
"""Get type_comment'ed FunctionDef object from living object.
This tries to parse original code for living object and returns
AST node for given *obj*. It requires py38+ or typed_ast module.
Signature for given *obj*. It requires py38+ or typed_ast module.
"""
try:
source = getsource(obj)
@ -41,7 +103,8 @@ def get_type_comment(obj: Any) -> ast.FunctionDef:
subject = cast(ast.FunctionDef, module.body[0]) # type: ignore
if getattr(subject, "type_comment", None):
return ast_parse(subject.type_comment, mode='func_type') # type: ignore
function = ast_parse(subject.type_comment, mode='func_type')
return signature_from_ast(subject, bound_method, function) # type: ignore
else:
return None
except (OSError, TypeError): # failed to load source code
@ -53,17 +116,17 @@ def get_type_comment(obj: Any) -> ast.FunctionDef:
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
type_sig = get_type_comment(obj, bound_method)
if type_sig:
sig = inspect.signature(obj, bound_method)
for param in sig.parameters.values():
if param.name not in obj.__annotations__:
annotation = type_sig.parameters[param.name].annotation
if annotation is not Parameter.empty:
obj.__annotations__[param.name] = ast_unparse(annotation)
if 'return' not in obj.__annotations__:
obj.__annotations__['return'] = ast_unparse(function.returns) # type: ignore
obj.__annotations__['return'] = type_sig.return_annotation
except NotImplementedError as exc: # failed to ast.unparse()
logger.warning("Failed to parse type_comment for %r: %s", obj, exc)

View File

@ -18,7 +18,27 @@ class Math:
# type: (int, int) -> int
return a - b
def nothing(self):
# type: () -> None
pass
def horse(self,
a, # type: str
b, # type: int
):
# type: (...) -> None
return
def complex_func(arg1, arg2, arg3=None, *args, **kwargs):
# type: (str, List[int], Tuple[int, Union[str, Unknown]], *str, **str) -> None
pass
def missing_attr(c,
a, # type: str
b=None # type: Optional[str]
):
# type: (...) -> str
return a + (b or "")

View File

@ -483,9 +483,17 @@ def test_autodoc_typehints_signature(app):
' :module: target.typehints',
' ',
' ',
' .. py:method:: Math.horse(a: str, b: int) -> None',
' :module: target.typehints',
' ',
' ',
' .. py:method:: Math.incr(a: int, b: int = 1) -> int',
' :module: target.typehints',
' ',
' ',
' .. py:method:: Math.nothing() -> None',
' :module: target.typehints',
' ',
'',
'.. py:function:: complex_func(arg1: str, arg2: List[int], arg3: Tuple[int, '
'Union[str, Unknown]] = None, *args: str, **kwargs: str) -> None',
@ -498,6 +506,10 @@ def test_autodoc_typehints_signature(app):
'',
'.. py:function:: incr(a: int, b: int = 1) -> int',
' :module: target.typehints',
'',
'',
'.. py:function:: missing_attr(c, a: str, b: Optional[str] = None) -> str',
' :module: target.typehints',
''
]
@ -522,9 +534,17 @@ def test_autodoc_typehints_none(app):
' :module: target.typehints',
' ',
' ',
' .. py:method:: Math.horse(a, b)',
' :module: target.typehints',
' ',
' ',
' .. py:method:: Math.incr(a, b=1)',
' :module: target.typehints',
' ',
' ',
' .. py:method:: Math.nothing()',
' :module: target.typehints',
' ',
'',
'.. py:function:: complex_func(arg1, arg2, arg3=None, *args, **kwargs)',
' :module: target.typehints',
@ -536,6 +556,10 @@ def test_autodoc_typehints_none(app):
'',
'.. py:function:: incr(a, b=1)',
' :module: target.typehints',
'',
'',
'.. py:function:: missing_attr(c, a, b=None)',
' :module: target.typehints',
''
]