diff --git a/Doc/library/inspect.rst b/Doc/library/inspect.rst index 2a71201a80b2cb..0b1bcb71c55edf 100644 --- a/Doc/library/inspect.rst +++ b/Doc/library/inspect.rst @@ -550,7 +550,7 @@ The Signature object represents the call signature of a callable object and its return annotation. To retrieve a Signature object, use the :func:`signature` function. -.. function:: signature(callable, \*, follow_wrapped=True) +.. function:: signature(callable_or_text, \*, follow_wrapped=True) Return a :class:`Signature` object for the given ``callable``:: @@ -570,7 +570,8 @@ function. Accepts a wide range of Python callables, from plain functions and classes to - :func:`functools.partial` objects. + :func:`functools.partial` objects. Text (string) signature are also + supported. See :func:`Signature.from_text`. Raises :exc:`ValueError` if no signature can be provided, and :exc:`TypeError` if that type of object is not supported. @@ -584,6 +585,9 @@ function. ``callable`` specifically (``callable.__wrapped__`` will not be used to unwrap decorated callables.) + .. versionchanged:: 3.9 + Supports converting signature strings into :class:`Signature` objects. + .. note:: Some callables may not be introspectable in certain implementations of @@ -677,6 +681,14 @@ function. .. versionadded:: 3.5 + .. classmethod:: Signature.from_text(text) + + Return a :class:`Signature` (or its subclass) object for a given string. + + For any signature object, ``str(sig)`` will round-trip through + ``from_text()``. + + .. versionadded:: 3.9 .. class:: Parameter(name, kind, \*, default=Parameter.empty, annotation=Parameter.empty) diff --git a/Lib/inspect.py b/Lib/inspect.py index 99a580bd2f2350..86d055f2b2ee45 100644 --- a/Lib/inspect.py +++ b/Lib/inspect.py @@ -1955,18 +1955,32 @@ def _signature_strip_non_python_syntax(signature): return clean_signature, self_parameter, last_positional_only -def _signature_fromstr(cls, obj, s, skip_bound_arg=True): +def _signature_fromstr(cls, obj, text, skip_bound_arg=True): """Private helper to parse content of '__text_signature__' and return a Signature based on it. """ + modname = getattr(obj, '__module__', None) + _self = getattr(obj, '__self__', None) + try: + return _signature_from_text(cls, text, modname, _self, skip_bound_arg) + except ValueError as exc: + if getattr(exc, '_syntax', False): + raise ValueError("{!r} builtin has invalid signature".format(obj)) + raise + + +def _signature_from_text(cls, text, module_name=None, _self=None, skip_bound_arg=True): + """Private helper to parse a signature string in Argument Clinic's + extended signature format. + """ # Lazy import ast because it's relatively heavy and # it's not used for other than this function. import ast Parameter = cls._parameter_cls - clean_signature, self_parameter, last_positional_only = \ - _signature_strip_non_python_syntax(s) + (clean_signature, self_parameter, last_positional_only + ) = _signature_strip_non_python_syntax(text) program = "def foo" + clean_signature + ": pass" @@ -1976,7 +1990,9 @@ def _signature_fromstr(cls, obj, s, skip_bound_arg=True): module = None if not isinstance(module, ast.Module): - raise ValueError("{!r} builtin has invalid signature".format(obj)) + exc = ValueError(f"invalid signature {text!r}") + exc._syntax = True + raise exc f = module.body[0] @@ -1986,7 +2002,6 @@ def _signature_fromstr(cls, obj, s, skip_bound_arg=True): module = None module_dict = {} - module_name = getattr(obj, '__module__', None) if module_name: module = sys.modules.get(module_name, None) if module: @@ -2080,7 +2095,6 @@ def p(name_node, default_node, default=empty): # - We don't strip first bound argument if # skip_bound_arg is False. assert parameters - _self = getattr(obj, '__self__', None) self_isbound = _self is not None self_ismodule = ismodule(_self) if self_isbound and (self_ismodule or skip_bound_arg): @@ -2812,6 +2826,24 @@ def __init__(self, parameters=None, *, return_annotation=_empty, self._parameters = types.MappingProxyType(params) self._return_annotation = return_annotation + @classmethod + def from_text(cls, text, *, skip_bound_arg=True): + """Constructs Signature for the given string. + + The expected format matches what Signature.__str__() returns. + For example: + + func() + func(a, b) + func(a: int, b: str) + func(a, b=None, /, c, d=None, *args, e, f=None, **kwargs) + + For the most part this will round-trip with Signature.__str__(). + """ + # See Argument Clinic's extended signature format (PEP 436). + return _signature_from_text(cls, text, + skip_bound_arg=skip_bound_arg) + @classmethod def from_function(cls, func): """Constructs Signature for the given python function. @@ -3090,7 +3122,10 @@ def __str__(self): def signature(obj, *, follow_wrapped=True): """Get a signature object for the passed callable.""" - return Signature.from_callable(obj, follow_wrapped=follow_wrapped) + if isinstance(obj, str): + return Signature.from_text(obj) + else: + return Signature.from_callable(obj, follow_wrapped=follow_wrapped) def _main(): diff --git a/Lib/test/test_inspect.py b/Lib/test/test_inspect.py index 1cd4ea28939d8a..029c669eb3fd06 100644 --- a/Lib/test/test_inspect.py +++ b/Lib/test/test_inspect.py @@ -3166,6 +3166,112 @@ class MySignature(inspect.Signature): pass sig = MySignature.from_callable(_pickle.Pickler) self.assertTrue(isinstance(sig, MySignature)) + def test_signature_from_text(self): + def expect(params, returns=None): + for i, param in enumerate(params): + kind = None + kwargs = None + if isinstance(param, str): + name = param + elif len(param) == 1: + name, = param + elif len(param) == 2: + name, kind = param + else: + name, kind, kwargs = param + if kind is None: + kind = inspect.Parameter.POSITIONAL_OR_KEYWORD + params[i] = inspect.Parameter(name, kind, **kwargs or {}) + if returns: + return inspect.Signature(params, return_annotation=returns) + else: + return inspect.Signature(params) + tests = [ + # (text, expected, expected-no-skip) + ('()', + expect([]), + None), + ('(a, b)', + expect(['a', 'b']), + None), + ('(a, b=2, /, c=3, *args, d=4, e, **kwargs)', + expect([ + ('a', inspect.Parameter.POSITIONAL_ONLY), + ('b', inspect.Parameter.POSITIONAL_ONLY, {'default': 2}), + ('c', None, {'default': 3}), + ('args', inspect.Parameter.VAR_POSITIONAL), + ('d', inspect.Parameter.KEYWORD_ONLY, {'default': 4}), + ('e', inspect.Parameter.KEYWORD_ONLY, ), + ('kwargs', inspect.Parameter.VAR_KEYWORD), + ]), + None), + ('(a, b: int, c=3, *, d: Int = 4)', + ValueError('Annotations are not currently supported'), + # XXX Uncomment once annotations *are* supported. + #expect([ + # ('a',), + # ('b', None, {'annotation': int}), + # ('c', None, {'default': 3}), + # ('d', inspect.Parameter.KEYWORD_ONLY, {'default': 4, 'annotation': 'Int'}), + # ]), + None), + ('(*args, **kwargs)', + expect([ + ('args', inspect.Parameter.VAR_POSITIONAL), + ('kwargs', inspect.Parameter.VAR_KEYWORD), + ]), + None), + ('func()', + expect([]), + None), + + # builtins + (pow, None, None), + (range, None, None), + (__import__, None, None), + ] + for text, expected, expected_no_skip in tests: + if isinstance(text, str): + obj = None + else: + obj = text + text = getattr(obj, '__text_signature__', None) + if not text: + continue + if text.startswith('($module, '): + text = '(' + text[9:] + if expected is None: + expected = inspect.signature(obj) + if expected_no_skip is None: + expected_no_skip = expected + with self.subTest(text if obj is None else f'<{obj.__name__}> {text}'): + if isinstance(expected, Exception): + with self.assertRaises(type(expected)) as cm: + inspect.Signature.from_text(text, skip_bound_arg=True) + self.assertEqual(str(cm.exception), str(expected)) + else: + sig = inspect.signature(text) + sig_default = inspect.Signature.from_text(text) + sig_skip = inspect.Signature.from_text(text, skip_bound_arg=True) + + self.assertEqual(sig, expected) + self.assertEqual(sig_default, expected) + self.assertEqual(sig_skip, expected) + + if isinstance(expected_no_skip, Exception): + with self.assertRaises(type(expected_no_skip)) as cm: + inspect.Signature.from_text(text, skip_bound_arg=False) + self.assertEqual(str(cm.exception), str(expected_no_skip)) + else: + sig_no_skip = inspect.Signature.from_text(text, skip_bound_arg=False) + # round trip + sig_roundtrip = inspect.Signature.from_text( + str(sig_no_skip), + skip_bound_arg=False) + + self.assertEqual(sig_no_skip, expected) + self.assertEqual(sig_roundtrip, sig_no_skip) + def test_signature_definition_order_preserved_on_kwonly(self): for fn in signatures_with_lexicographic_keyword_only_parameters(): signature = inspect.signature(fn) diff --git a/Misc/NEWS.d/next/Library/2019-07-03-18-13-37.bpo-37497.wcVo12.rst b/Misc/NEWS.d/next/Library/2019-07-03-18-13-37.bpo-37497.wcVo12.rst new file mode 100644 index 00000000000000..0bff2b8324eadf --- /dev/null +++ b/Misc/NEWS.d/next/Library/2019-07-03-18-13-37.bpo-37497.wcVo12.rst @@ -0,0 +1 @@ +Add inspect.Signature.from_text().