mirror of
https://github.com/sphinx-doc/sphinx.git
synced 2025-02-25 18:55:22 -06:00
refactor: Support suppressed type_comment (refs: #7152)
This commit is contained in:
@@ -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)
|
||||
|
||||
|
@@ -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',
|
||||
''
|
||||
]
|
||||
|
Reference in New Issue
Block a user