diff --git a/Doc/library/inspect.rst b/Doc/library/inspect.rst index ed8d705da3b0b5..3c85faa1bc6057 100644 --- a/Doc/library/inspect.rst +++ b/Doc/library/inspect.rst @@ -817,6 +817,19 @@ function. .. versionchanged:: 3.10 The *globals*, *locals*, and *eval_str* parameters were added. + .. classmethod:: Signature.from_frame(frame) + + Return a :class:`Signature` (or its subclass) object for a given + :ref:`frame object `. + + Notice that it is impossible to get signatures + with defaults or annotations from frames, + because annotations are stored + in a function inside ``__defaults__``, ``__kwdefaults__``, + and ``__annotations__`` attributes. + + .. versionadded:: 3.13 + .. class:: Parameter(name, kind, *, default=Parameter.empty, annotation=Parameter.empty) diff --git a/Doc/whatsnew/3.13.rst b/Doc/whatsnew/3.13.rst index 51939909000960..9574ed2e1864b8 100644 --- a/Doc/whatsnew/3.13.rst +++ b/Doc/whatsnew/3.13.rst @@ -361,6 +361,11 @@ and only logged in :ref:`Python Development Mode ` or on :ref:`Python built on debug mode `. (Contributed by Victor Stinner in :gh:`62948`.) +inspect +------- + +* Add :meth:`inspect.Signature.from_frame` to get signatures from frame objects. + ipaddress --------- diff --git a/Lib/inspect.py b/Lib/inspect.py index 8a2b2c96e993b5..865fd401dc158e 100644 --- a/Lib/inspect.py +++ b/Lib/inspect.py @@ -2419,16 +2419,34 @@ def _signature_from_function(cls, func, skip_bound_arg=True, Parameter = cls._parameter_cls # Parameter information. - func_code = func.__code__ + annotations = get_annotations( + func, + globals=globals, + locals=locals, + eval_str=eval_str, + ) + return _signature_from_code( + func.__code__, + annotations=annotations, + defaults=func.__defaults__, + kwdefaults=func.__kwdefaults__, + cls=cls, + is_duck_function=is_duck_function, + ) + + +def _signature_from_code( + func_code, + *, + annotations, defaults, kwdefaults, + cls, is_duck_function, +): pos_count = func_code.co_argcount arg_names = func_code.co_varnames posonly_count = func_code.co_posonlyargcount positional = arg_names[:pos_count] keyword_only_count = func_code.co_kwonlyargcount keyword_only = arg_names[pos_count:pos_count + keyword_only_count] - annotations = get_annotations(func, globals=globals, locals=locals, eval_str=eval_str) - defaults = func.__defaults__ - kwdefaults = func.__kwdefaults__ if defaults: pos_default_count = len(defaults) @@ -3093,6 +3111,29 @@ def from_callable(cls, obj, *, follow_wrapper_chains=follow_wrapped, globals=globals, locals=locals, eval_str=eval_str) + @classmethod + def from_frame(cls, frame): + """ + Constructs Signature from a given frame object. + + Notice that it is impossible to get signatures + with defaults or annotations from frames, + because annotations are stored + in a function inside ``__defaults__``, ``__kwdefaults__``, + and ``__annotations__`` attributes. + """ + if not isframe(frame): + raise TypeError(f'Frame object expected, got: {type(frame)}') + + return _signature_from_code( + frame.f_code, + annotations={}, + defaults=(), + kwdefaults={}, + cls=cls, + is_duck_function=False, + ) + @property def parameters(self): return self._parameters diff --git a/Lib/test/test_inspect/test_inspect.py b/Lib/test/test_inspect/test_inspect.py index 52cf68b93b85fa..3af5731939e5bb 100644 --- a/Lib/test/test_inspect/test_inspect.py +++ b/Lib/test/test_inspect/test_inspect.py @@ -4623,6 +4623,104 @@ class D2(D1): self.assertEqual(inspect.signature(D2), inspect.signature(D1)) +class TestSignatureFromFrame(unittest.TestCase): + def test_from_frame(self): + ns = {} + def inner(a, /, b, *e, c: int = 3, d, **f) -> None: + ns['fr'] = inspect.currentframe() + + inner(1, 2, d=4) + self.assertEqual(str(inspect.Signature.from_frame(ns['fr'])), + '(a, /, b, *e, c, d, **f)') + + def test_from_frame_with_pos_only_defaults(self): + ns = {} + def inner(a=1, /, b=2, *e, c: int = 3, d, **f) -> None: + ns['fr'] = inspect.currentframe() + + inner(d=4) + self.assertEqual(str(inspect.Signature.from_frame(ns['fr'])), + '(a, /, b, *e, c, d, **f)') + + def test_from_frame_no_locals(self): + ns = {} + def inner(): + ns['fr'] = inspect.currentframe() + + inner() + self.assertEqual(str(inspect.Signature.from_frame(ns['fr'])), + '()') + + def test_from_frame_no_pos(self): + ns = {} + def inner(*, a, b=2, **c): + ns['fr'] = inspect.currentframe() + + inner(a=1) + self.assertEqual(str(inspect.Signature.from_frame(ns['fr'])), + '(*, a, b, **c)') + + def test_from_frame_no_kw(self): + ns = {} + def inner(a, /, b, *c): + ns['fr'] = inspect.currentframe() + + inner(1, 2) + self.assertEqual(str(inspect.Signature.from_frame(ns['fr'])), + '(a, /, b, *c)') + + def test_from_frame_with_nonlocal(self): + fr = None + def inner(a, /, b, *c): + nonlocal fr + fr = inspect.currentframe() + + inner(1, 2) + self.assertEqual(str(inspect.Signature.from_frame(fr)), + '(a, /, b, *c)') + + def test_clear_frame(self): + ns = {} + def inner(a=1, /, c=5, *, b=2): + ns['fr'] = inspect.currentframe() + + inner() + ns['fr'].clear() + self.assertEqual(str(inspect.Signature.from_frame(ns['fr'])), + '(a, /, c, *, b)') + + def test_from_method_frame(self): + ns = {} + class _A: + def inner(self, a, *, b): + ns['fr'] = inspect.currentframe() + + _A().inner(1, b=2) + self.assertEqual(str(inspect.Signature.from_frame(ns['fr'])), + '(self, a, *, b)') + + def test_from_frame_defaults_change(self): + ns = {} + def inner(a=1, /, c=5, *, b=2): + a = 3 + ns['fr'] = inspect.currentframe() + b = 4 + + inner() + self.assertEqual(str(inspect.Signature.from_frame(ns['fr'])), + '(a, /, c, *, b)') + + def test_from_frame_mod(self): + self.assertEqual(str(inspect.Signature.from_frame(mod.fr)), + '(x, y)') + self.assertEqual(str(inspect.Signature.from_frame(mod.fr.f_back)), + '(a, /, b, c, d, e, f, *g, **h)') + + def test_from_not_frame(self): + with self.assertRaisesRegex(TypeError, 'Frame object expected'): + inspect.Signature.from_frame(lambda: ...) + + class TestParameterObject(unittest.TestCase): def test_signature_parameter_kinds(self): P = inspect.Parameter diff --git a/Misc/NEWS.d/next/Library/2024-03-09-12-55-44.gh-issue-108901.s9VClL.rst b/Misc/NEWS.d/next/Library/2024-03-09-12-55-44.gh-issue-108901.s9VClL.rst new file mode 100644 index 00000000000000..14f38d74083511 --- /dev/null +++ b/Misc/NEWS.d/next/Library/2024-03-09-12-55-44.gh-issue-108901.s9VClL.rst @@ -0,0 +1,2 @@ +Add :meth:`inspect.Signature.from_frame` to get a signature +from a frame object.