summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorTakeshi KOMIYA <i.tkomiya@gmail.com>2020-01-05 00:00:26 +0900
committerTakeshi KOMIYA <i.tkomiya@gmail.com>2020-02-16 01:43:13 +0900
commit822625d14c0cad6607553079b31930ff9e2de717 (patch)
treeb3fd20327344576c4594208e4ccff7829a4277c1
parent8576e97e34a9b39189cb4e8188407002cdab1a27 (diff)
downloadsphinx-git-822625d14c0cad6607553079b31930ff9e2de717.tar.gz
Add sphinx.util.inspect:signature_from_str()
-rw-r--r--sphinx/util/inspect.py49
-rw-r--r--tests/test_util_inspect.py85
2 files changed, 134 insertions, 0 deletions
diff --git a/sphinx/util/inspect.py b/sphinx/util/inspect.py
index 80cac97d9..7c977daf1 100644
--- a/sphinx/util/inspect.py
+++ b/sphinx/util/inspect.py
@@ -21,8 +21,11 @@ from inspect import ( # NOQA
)
from io import StringIO
from typing import Any, Callable, Mapping, List, Tuple
+from typing import cast
from sphinx.deprecation import RemovedInSphinx40Warning, RemovedInSphinx50Warning
+from sphinx.pycode.ast import ast # for py35-37
+from sphinx.pycode.ast import unparse as ast_unparse
from sphinx.util import logging
from sphinx.util.typing import stringify as stringify_annotation
@@ -429,6 +432,52 @@ def stringify_signature(sig: inspect.Signature, show_annotation: bool = True,
return '(%s) -> %s' % (', '.join(args), annotation)
+def signature_from_str(signature: str) -> inspect.Signature:
+ """Create a Signature object from string."""
+ module = ast.parse('def func' + signature + ': pass')
+ definition = cast(ast.FunctionDef, module.body[0]) # type: ignore
+
+ # parameters
+ args = definition.args
+ params = []
+
+ if hasattr(args, "posonlyargs"):
+ for arg in args.posonlyargs: # type: ignore
+ annotation = ast_unparse(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 = ast_unparse(args.defaults[-len(args.args) + i])
+ else:
+ default = Parameter.empty
+
+ annotation = ast_unparse(arg.annotation) or Parameter.empty
+ params.append(Parameter(arg.arg, Parameter.POSITIONAL_OR_KEYWORD,
+ default=default, annotation=annotation))
+
+ if args.vararg:
+ annotation = ast_unparse(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 = ast_unparse(args.kw_defaults[i])
+ annotation = ast_unparse(arg.annotation) or Parameter.empty
+ params.append(Parameter(arg.arg, Parameter.KEYWORD_ONLY, default=default,
+ annotation=annotation))
+
+ if args.kwarg:
+ annotation = ast_unparse(args.kwarg.annotation) or Parameter.empty
+ params.append(Parameter(args.kwarg.arg, Parameter.VAR_KEYWORD,
+ annotation=annotation))
+
+ return_annotation = ast_unparse(definition.returns) or Parameter.empty
+
+ return inspect.Signature(params, return_annotation=return_annotation)
+
+
class Signature:
"""The Signature object represents the call signature of a callable object and
its return annotation.
diff --git a/tests/test_util_inspect.py b/tests/test_util_inspect.py
index 34844c9bf..d6676de8f 100644
--- a/tests/test_util_inspect.py
+++ b/tests/test_util_inspect.py
@@ -13,6 +13,7 @@ import datetime
import functools
import sys
import types
+from inspect import Parameter
import pytest
@@ -309,6 +310,90 @@ def test_signature_annotations_py38(app):
assert stringify_signature(sig) == '(a, b, /)'
+def test_signature_from_str_basic():
+ signature = '(a, b, *args, c=0, d="blah", **kwargs)'
+ sig = inspect.signature_from_str(signature)
+ 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 == Parameter.empty
+
+
+def test_signature_from_str_default_values():
+ 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)')
+ sig = inspect.signature_from_str(signature)
+ 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_from_str_annotations():
+ signature = '(a: int, *args: bytes, b: str = "blah", **kwargs: float) -> None'
+ sig = inspect.signature_from_str(signature)
+ 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_from_str_complex_annotations():
+ sig = inspect.signature_from_str('() -> Tuple[str, int, ...]')
+ assert sig.return_annotation == 'Tuple[str, int, ...]'
+
+ sig = inspect.signature_from_str('() -> 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_from_str_positionaly_only_args():
+ sig = inspect.signature_from_str('(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_from_str_invalid():
+ with pytest.raises(SyntaxError):
+ inspect.signature_from_str('')
+
+
def test_safe_getattr_with_default():
class Foo:
def __getattr__(self, item):