Skip to content

Commit

Permalink
pythongh-108901: Add inspect.Signature.from_frame
Browse files Browse the repository at this point in the history
  • Loading branch information
sobolevn committed Mar 9, 2024
1 parent 03f86b1 commit e0df986
Show file tree
Hide file tree
Showing 4 changed files with 148 additions and 4 deletions.
7 changes: 7 additions & 0 deletions Doc/library/inspect.rst
Original file line number Diff line number Diff line change
Expand Up @@ -817,6 +817,13 @@ 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>`.

.. versionadded:: 3.13


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

Expand Down
52 changes: 48 additions & 4 deletions Lib/inspect.py
Original file line number Diff line number Diff line change
Expand Up @@ -2419,16 +2419,29 @@ 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 +3106,37 @@ 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."""
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_signature_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)')

def test_signature_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_signature_from_frame_no_locals(self):
ns = {}
def inner():
ns['fr'] = inspect.currentframe()

inner()
self.assertEqual(str(inspect.Signature.from_frame(ns['fr'])),
'()')

def test_signature_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_signature_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_signature_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_signature_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_signature_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_signature_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_signature_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,3 @@
Add :meth:`inspect.Signature.from_frame` to get a singature from a frame
object, it will be used as a new alternative for older
:func:`inspect.getargvalues` and :func:`inspect.formatargvalues`.

0 comments on commit e0df986

Please sign in to comment.