Skip to content
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

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

Open
wants to merge 7 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 5 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
14 changes: 14 additions & 0 deletions Doc/library/inspect.rst
Original file line number Diff line number Diff line change
Expand Up @@ -817,6 +817,20 @@ 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 annotations from frames,
because annotations are stored
in function inside :attr:`~function.__annotations__` attribute.
Also note that default values are populated from frame's variables,
not real function's default values.

.. versionadded:: 3.13
Copy link
Contributor

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
6 changes: 6 additions & 0 deletions Doc/whatsnew/3.13.rst
Original file line number Diff line number Diff line change
Expand Up @@ -361,6 +361,12 @@ 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
and to replace the old :func:`inspect.getargvalues` API.

ipaddress
---------

Expand Down
66 changes: 62 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,46 @@ 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 annotations from frames,
because annotations are stored
in function inside ``__annotations__`` attribute.
Also note that default values are populated from frame's variables,
not real function's default values.
"""
if not isframe(frame):
raise TypeError(f'Frame object expected, got: {type(frame)}')

func_code = frame.f_code
pos_count = func_code.co_argcount
arg_names = func_code.co_varnames
keyword_only_count = func_code.co_kwonlyargcount

defaults = []
kwdefaults = {}
if frame.f_locals:
for name in arg_names[:pos_count]:
if name in frame.f_locals:
defaults.append(frame.f_locals[name])

for name in arg_names[pos_count : pos_count + keyword_only_count]:
if name in frame.f_locals:
kwdefaults.update({name: frame.f_locals[name]})

return _signature_from_code(
func_code,
annotations={},
defaults=defaults,
kwdefaults=kwdefaults,
cls=cls,
is_duck_function=False,
)

@property
def parameters(self):
return self._parameters
Expand Down
90 changes: 90 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,96 @@ 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=1, /, b=2, *e, c=3, d=4, **f)')
sobolevn marked this conversation as resolved.
Show resolved Hide resolved

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=1, /, b=2, *e, c=3, d=4, **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=1, b=2, **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=1, /, b=2, *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=1, /, b=2, *c)')

def test_from_method_frame(self):
ns = {}
class _A:
def inner(self, a, *, b):
ns['fr'] = inspect.currentframe()
def __repr__(self):
return '_A'

_A().inner(1, b=2)
self.assertEqual(str(inspect.Signature.from_frame(ns['fr'])),
'(self=_A, a=1, *, b=2)')

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=3, /, c=5, *, b=4)')

def test_from_frame_mod(self):
self.assertEqual(str(inspect.Signature.from_frame(mod.fr)),
'(x=11, y=14)')
self.assertEqual(str(inspect.Signature.from_frame(mod.fr.f_back)),
'(a=7, /, b=8, c=9, d=3, e=4, f=5, *g, **h)')

def test_from_not_frame(self):
with self.assertRaisesRegex(TypeError, 'Frame object expected'):
inspect.Signature.from_frame(lambda: ...)

vstinner marked this conversation as resolved.
Show resolved Hide resolved

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.
Loading