Skip to content

gh-108901: Add inspect.Signature.from_frame #116537

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 7 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 13 additions & 0 deletions Doc/library/inspect.rst
Original file line number Diff line number Diff line change
Expand Up @@ -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 <frame-objects>`.

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
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
.. versionadded:: 3.13
.. versionadded:: next



.. class:: Parameter(name, kind, *, default=Parameter.empty, annotation=Parameter.empty)

Expand Down
5 changes: 5 additions & 0 deletions Doc/whatsnew/3.13.rst
Original file line number Diff line number Diff line change
Expand Up @@ -361,6 +361,11 @@ and only logged in :ref:`Python Development Mode <devmode>` or on :ref:`Python
built on debug mode <debug-build>`.
(Contributed by Victor Stinner in :gh:`62948`.)

inspect
-------

* Add :meth:`inspect.Signature.from_frame` to get signatures from frame objects.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Don't we need the "Contributed by" part? (to have the issue number)


ipaddress
---------

Expand Down
49 changes: 45 additions & 4 deletions Lib/inspect.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -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
Expand Down
98 changes: 98 additions & 0 deletions Lib/test/test_inspect/test_inspect.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
Add :meth:`inspect.Signature.from_frame` to get a signature
from a frame object.