refactor: Support suppressed type_comment (refs: #7152)

This commit is contained in:
Takeshi KOMIYA
2020-02-16 17:26:32 +09:00
parent 51b80ab121
commit 98d24464f1
2 changed files with 75 additions and 42 deletions

View File

@@ -9,9 +9,8 @@
"""
import ast
import itertools
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
@@ -24,11 +23,73 @@ from sphinx.util import logging
logger = logging.getLogger(__name__)
def get_type_comment(obj: Any) -> Dict[str, Any]:
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
parameter dictionary 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)
@@ -42,34 +103,8 @@ def get_type_comment(obj: Any) -> Dict[str, Any]:
subject = cast(ast.FunctionDef, module.body[0]) # type: ignore
if getattr(subject, "type_comment", None):
comment = subject.type_comment # type: ignore
if not comment.startswith("(...)"):
node = cast(
ast.FunctionDef,
ast_parse(comment, mode='func_type')
)
results = {
'returns': node.returns,
'explicit': False,
'args': node.argtypes # type: ignore
}
else:
node = subject
results = {
'returns': node.returns,
'explicit': True,
'args': {
arg.arg: arg.type_comment
for arg in itertools.chain(
getattr(node.args, "posonlyargs", []) or [],
getattr(node.args, "args", []) or [],
getattr(node.args, "vararg", []) or [],
getattr(node.args, "kwonlyargs", []) or [],
getattr(node.args, "kwarg", []) or [],
)
}
}
return results
function = ast_parse(subject.type_comment, mode='func_type')
return signature_from_ast(subject, bound_method, function)
else:
return None
except (OSError, TypeError): # failed to load source code
@@ -81,19 +116,17 @@ def get_type_comment(obj: Any) -> Dict[str, Any]:
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:
explicit = function['explicit']
args = function['args']
type_sig = get_type_comment(obj, bound_method)
if type_sig:
sig = inspect.signature(obj, bound_method)
for i, param in enumerate(sig.parameters.values()):
if param.name not in obj.__annotations__:
type_hint = args[param.name if explicit else i]
annotation = ast_unparse(type_hint)
obj.__annotations__[param.name] = annotation
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'])
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

@@ -508,7 +508,7 @@ def test_autodoc_typehints_signature(app):
' :module: target.typehints',
'',
'',
'.. py:function:: missing_attr(c: None, a: str, b: Optional[str] = None) -> None',
'.. py:function:: missing_attr(c, a: str, b: Optional[str] = None) -> str',
' :module: target.typehints',
''
]