forked from sphinx-doc/sphinx
-
Notifications
You must be signed in to change notification settings - Fork 1
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Add sphinx.pycode.function.Signature
- Loading branch information
Showing
2 changed files
with
215 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,118 @@ | ||
""" | ||
sphinx.pycode.function | ||
~~~~~~~~~~~~~~~~~~~~~~ | ||
Utilities parsing function definitions | ||
:copyright: Copyright 2007-2020 by the Sphinx team, see AUTHORS. | ||
:license: BSD, see LICENSE for details. | ||
""" | ||
|
||
import ast | ||
from collections import OrderedDict | ||
from inspect import Parameter | ||
from typing import cast | ||
from typing import Mapping | ||
|
||
|
||
def stringify(node: ast.AST) -> str: | ||
"""Stringify an AST node.""" | ||
if node is None: | ||
return None | ||
elif isinstance(node, ast.Attribute): | ||
return '%s.%s' % (stringify(node.value), node.attr) | ||
elif isinstance(node, ast.Bytes): | ||
return repr(node.s) | ||
elif isinstance(node, ast.Call): | ||
args = ([stringify(e) for e in node.args] + | ||
['%s=%s' % (k.arg, stringify(k.value)) for k in node.keywords]) | ||
return '%s(%s)' % (stringify(node.func), ', '.join(args)) | ||
elif isinstance(node, ast.Dict): | ||
keys = (stringify(k) for k in node.keys) | ||
values = (stringify(v) for v in node.values) | ||
items = (k + ': ' + v for k, v in zip(keys, values)) | ||
return '{' + ', '.join(items) + '}' | ||
elif isinstance(node, ast.Ellipsis): | ||
return '...' | ||
elif sys.version_info > (3, 6) and isinstance(node, ast.Constant): | ||
return repr(node.value) | ||
elif isinstance(node, ast.Index): | ||
return stringify(node.value) | ||
elif isinstance(node, ast.Lambda): | ||
return '<function <lambda>>' # TODO | ||
elif isinstance(node, ast.List): | ||
return '[' + ', '.join(stringify(e) for e in node.elts) + ']' | ||
elif isinstance(node, ast.Name): | ||
return node.id | ||
elif isinstance(node, ast.NameConstant): | ||
return repr(node.value) | ||
elif isinstance(node, ast.Num): | ||
return repr(node.n) | ||
elif isinstance(node, ast.Set): | ||
return "{" + ', '.join(stringify(e) for e in node.elts) + "}" | ||
elif isinstance(node, ast.Str): | ||
return repr(node.s) | ||
elif isinstance(node, ast.Subscript): | ||
return "%s[%s]" % (stringify(node.value), stringify(node.slice)) | ||
elif isinstance(node, ast.Tuple): | ||
return ', '.join(stringify(e) for e in node.elts) | ||
else: | ||
# TODO: f-string | ||
raise NotImplementedError('Unable to render %s instance' % type(node).__name__) | ||
|
||
|
||
class Signature: | ||
"""A Signature object represents the call signature of a function and its return annotation. | ||
This class provides ``inspect.Signature`` like interface for string based call signatures:: | ||
sig = Signature('(a, b, *args, **kwargs)') | ||
sig.parameters # => returns a dict for Parameters | ||
""" | ||
|
||
def __init__(self, signature: str) -> None: | ||
module = ast.parse('def func' + signature + ': pass') | ||
self.definition = cast(ast.FunctionDef, module.body[0]) | ||
|
||
@property | ||
def parameters(self) -> Mapping: | ||
args = self.definition.args | ||
params = [] | ||
|
||
if hasattr(args, "posonlyargs"): # for py38+ | ||
for arg in args.posonlyargs: # type: ignore | ||
annotation = stringify(arg.annotation) or Parameter.empty | ||
params.append(Parameter(arg.arg, Parameter.POSITIONAL_ONLY, | ||
annotation=annotation)) | ||
|
||
for i, arg in enumerate(args.args): | ||
if len(args.args) - i <= len(args.defaults): | ||
default = stringify(args.defaults[-len(args.args) + i]) | ||
else: | ||
default = Parameter.empty | ||
|
||
annotation = stringify(arg.annotation) or Parameter.empty | ||
params.append(Parameter(arg.arg, Parameter.POSITIONAL_OR_KEYWORD, | ||
default=default, annotation=annotation)) | ||
|
||
if args.vararg: | ||
annotation = stringify(args.vararg.annotation) or Parameter.empty | ||
params.append(Parameter(args.vararg.arg, Parameter.VAR_POSITIONAL, | ||
annotation=annotation)) | ||
|
||
for i, arg in enumerate(args.kwonlyargs): | ||
default = stringify(args.kw_defaults[i]) | ||
annotation = stringify(arg.annotation) or Parameter.empty | ||
params.append(Parameter(arg.arg, Parameter.KEYWORD_ONLY, default=default, | ||
annotation=annotation)) | ||
|
||
if args.kwarg: | ||
annotation = stringify(args.kwarg.annotation) or Parameter.empty | ||
params.append(Parameter(args.kwarg.arg, Parameter.VAR_KEYWORD, | ||
annotation=annotation)) | ||
|
||
return OrderedDict((p.name, p) for p in params) | ||
|
||
@property | ||
def return_annotation(self) -> str: | ||
return stringify(self.definition.returns) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,97 @@ | ||
""" | ||
test_pycode_function | ||
~~~~~~~~~~~~~~~~~~~~ | ||
Test pycode.function. | ||
:copyright: Copyright 2007-2016 by the Sphinx team, see AUTHORS. | ||
:license: BSD, see LICENSE for details. | ||
""" | ||
|
||
import sys | ||
from inspect import Parameter | ||
|
||
import pytest | ||
|
||
from sphinx.pycode.function import Signature | ||
|
||
|
||
def test_signature_basic(): | ||
sig = Signature('(a, b, *args, c=0, d="blah", **kwargs)') | ||
assert list(sig.parameters.keys()) == ['a', 'b', 'args', 'c', 'd', 'kwargs'] | ||
assert sig.parameters['a'].name == 'a' | ||
assert sig.parameters['a'].kind == Parameter.POSITIONAL_OR_KEYWORD | ||
assert sig.parameters['a'].default == Parameter.empty | ||
assert sig.parameters['a'].annotation == Parameter.empty | ||
assert sig.parameters['b'].name == 'b' | ||
assert sig.parameters['b'].kind == Parameter.POSITIONAL_OR_KEYWORD | ||
assert sig.parameters['b'].default == Parameter.empty | ||
assert sig.parameters['b'].annotation == Parameter.empty | ||
assert sig.parameters['args'].name == 'args' | ||
assert sig.parameters['args'].kind == Parameter.VAR_POSITIONAL | ||
assert sig.parameters['args'].default == Parameter.empty | ||
assert sig.parameters['args'].annotation == Parameter.empty | ||
assert sig.parameters['c'].name == 'c' | ||
assert sig.parameters['c'].kind == Parameter.KEYWORD_ONLY | ||
assert sig.parameters['c'].default == '0' | ||
assert sig.parameters['c'].annotation == Parameter.empty | ||
assert sig.parameters['d'].name == 'd' | ||
assert sig.parameters['d'].kind == Parameter.KEYWORD_ONLY | ||
assert sig.parameters['d'].default == "'blah'" | ||
assert sig.parameters['d'].annotation == Parameter.empty | ||
assert sig.parameters['kwargs'].name == 'kwargs' | ||
assert sig.parameters['kwargs'].kind == Parameter.VAR_KEYWORD | ||
assert sig.parameters['kwargs'].default == Parameter.empty | ||
assert sig.parameters['kwargs'].annotation == Parameter.empty | ||
assert sig.return_annotation is None | ||
|
||
|
||
def test_signature_default_values(): | ||
sig = Signature('(a=0, b=0.0, c="str", d=b"bytes", e=..., f=True, ' | ||
'g=[1, 2, 3], h={"a": 1}, i={1, 2, 3}, ' | ||
'j=lambda x, y: None, k=None, l=object(), m=foo.bar.CONSTANT)') | ||
assert sig.parameters['a'].default == '0' | ||
assert sig.parameters['b'].default == '0.0' | ||
assert sig.parameters['c'].default == "'str'" | ||
assert sig.parameters['d'].default == "b'bytes'" | ||
assert sig.parameters['e'].default == '...' | ||
assert sig.parameters['f'].default == 'True' | ||
assert sig.parameters['g'].default == '[1, 2, 3]' | ||
assert sig.parameters['h'].default == "{'a': 1}" | ||
assert sig.parameters['i'].default == '{1, 2, 3}' | ||
assert sig.parameters['j'].default == '<function <lambda>>' | ||
assert sig.parameters['k'].default == 'None' | ||
assert sig.parameters['l'].default == 'object()' | ||
assert sig.parameters['m'].default == 'foo.bar.CONSTANT' | ||
|
||
|
||
def test_signature_annotations(): | ||
sig = Signature('(a: int, *args: bytes, b: str = "blah", **kwargs: float) -> None') | ||
assert list(sig.parameters.keys()) == ['a', 'args', 'b', 'kwargs'] | ||
assert sig.parameters['a'].annotation == "int" | ||
assert sig.parameters['args'].annotation == "bytes" | ||
assert sig.parameters['b'].annotation == "str" | ||
assert sig.parameters['kwargs'].annotation == "float" | ||
assert sig.return_annotation == 'None' | ||
|
||
|
||
def test_signature_complex_annotations(): | ||
sig = Signature('() -> Tuple[str, int, ...]') | ||
assert sig.return_annotation == 'Tuple[str, int, ...]' | ||
|
||
sig = Signature('() -> Callable[[int, int], int]') | ||
assert sig.return_annotation == 'Callable[[int, int], int]' | ||
|
||
|
||
@pytest.mark.skipif(sys.version_info < (3, 8), | ||
reason='python-3.8 or above is required') | ||
def test_signature_positionaly_only_args(): | ||
sig = Signature('(a, /, b)') | ||
assert list(sig.parameters.keys()) == ['a', 'b'] | ||
assert sig.parameters['a'].kind == Parameter.POSITIONAL_ONLY | ||
assert sig.parameters['b'].kind == Parameter.POSITIONAL_OR_KEYWORD | ||
|
||
|
||
def test_signature_invalid(): | ||
with pytest.raises(SyntaxError): | ||
Signature('') |