mirror of
https://github.com/sphinx-doc/sphinx.git
synced 2025-02-25 18:55:22 -06:00
Merge pull request #7152 from gpotter2/elipsispatch
Fix #7146: support (...) in type hint comments (V2)
This commit is contained in:
commit
b80c7cd234
@ -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)
|
||||
|
||||
|
@ -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 "")
|
||||
|
||||
|
@ -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',
|
||||
''
|
||||
]
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user