Skip to content

gh-108901: Deprecate inspect.getargvalues and inspect.formatargvalues, provide modern alternative #112639

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 6 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
16 changes: 16 additions & 0 deletions Doc/library/inspect.rst
Original file line number Diff line number Diff line change
Expand Up @@ -817,6 +817,12 @@ 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 frame object.

.. versionadded:: 3.13


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

Expand Down Expand Up @@ -1107,6 +1113,11 @@ Classes and functions
are the names of the ``*`` and ``**`` arguments or ``None``. *locals* is the
locals dictionary of the given frame.

.. deprecated-removed:: 3.13 3.15
Use :meth:`Signature.from_frame` instead.
For Python version older than 3.13 use
`inspect313 <https://pypi.org/project/inspect313/>`_ PyPI package.

.. note::
This function was inadvertently marked as deprecated in Python 3.5.

Expand All @@ -1117,6 +1128,11 @@ Classes and functions
:func:`getargvalues`. The format\* arguments are the corresponding optional
formatting functions that are called to turn names and values into strings.

.. deprecated-removed:: 3.13 3.15
Use :meth:`Signature.from_frame` instead.
For Python version older than 3.13 use
`inspect313 <https://pypi.org/project/inspect313/>`_ PyPI package.

.. note::
This function was inadvertently marked as deprecated in Python 3.5.

Expand Down
12 changes: 10 additions & 2 deletions Doc/whatsnew/3.13.rst
Original file line number Diff line number Diff line change
Expand Up @@ -982,6 +982,11 @@ Pending Removal in Python 3.15
and was only useful for Jython support.
(Contributed by Nikita Sobolev in :gh:`116349`.)

* :func:`inspect.getargvalues` and :func:`inspect.formatargvalues`
are deprecated and slated for removal in 3.15;
use :meth:`inspect.Signature.from_frame` instead.
(Contributed by Nikita Sobolev in :gh:`108901`.)

Pending Removal in Python 3.16
------------------------------

Expand Down Expand Up @@ -1400,8 +1405,11 @@ CPython bytecode changes
Porting to Python 3.13
======================

This section lists previously described changes and other bugfixes
that may require changes to your code.
* :func:`inspect.getargvalues` and :func:`inspect.formatargvalues`
are deprecated. For Python 3.13+ use
new :meth:`inspect.Signature.from_frame` API.
For older versions, use `inspect313 <https://pypi.org/project/inspect313/>`_
PyPI package which is a backport of this API for Pythons from 3.8 to 3.12.

Changes in the Python API
-------------------------
Expand Down
41 changes: 41 additions & 0 deletions Lib/inspect.py
Original file line number Diff line number Diff line change
Expand Up @@ -1486,6 +1486,15 @@ def getargvalues(frame):
'args' is a list of the argument names.
'varargs' and 'varkw' are the names of the * and ** arguments or None.
'locals' is the locals dictionary of the given frame."""
import warnings
warnings._deprecated(
"getargvalues",
(
'{name!r} is deprecated and slated for removal in Python {remove}; '
'use `inspect.Singature.from_frame` instead'
),
remove=(3, 15),
)
args, varargs, varkw = getargs(frame.f_code)
return ArgInfo(args, varargs, varkw, frame.f_locals)

Expand Down Expand Up @@ -1521,6 +1530,15 @@ def formatargvalues(args, varargs, varkw, locals,
next four arguments are the corresponding optional formatting functions
that are called to turn names and values into strings. The ninth
argument is an optional function to format the sequence of arguments."""
import warnings
warnings._deprecated(
"formatargvalues",
(
'{name!r} is deprecated and slated for removal in Python {remove}; '
'use `inspect.Singature.__str__` instead'
),
remove=(3, 15),
)
def convert(name, locals=locals,
formatarg=formatarg, formatvalue=formatvalue):
return formatarg(name) + formatvalue(locals[name])
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."""
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 = []
for name in arg_names[:pos_count]:
if frame.f_locals and name in frame.f_locals:
defaults.append(frame.f_locals[name])
Comment on lines +3123 to +3125
Copy link
Member

Choose a reason for hiding this comment

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

It seems like you can move if frame.f_locals: out of this loop and the one below.


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

func = types.FunctionType(func_code, {})
func.__defaults__ = tuple(defaults)
func.__kwdefaults__ = kwdefaults
return cls.from_callable(func)

@property
def parameters(self):
return self._parameters
Expand Down
82 changes: 76 additions & 6 deletions Lib/test/test_inspect/test_inspect.py
Original file line number Diff line number Diff line change
Expand Up @@ -472,6 +472,16 @@ def __init__(self, *args, **kwargs):

git.abuse(7, 8, 9)

def assertDeprecated(self, name):
import re
return self.assertWarnsRegex(
DeprecationWarning,
re.escape(
f"{name!r} is deprecated and slated "
"for removal in Python 3.15",
),
)

def test_abuse_done(self):
self.istest(inspect.istraceback, 'git.ex.__traceback__')
self.istest(inspect.isframe, 'mod.fr')
Expand Down Expand Up @@ -518,21 +528,45 @@ def test_trace(self):
self.assertEqual(frame3.positions, dis.Positions(18, 18, 8, 13))

def test_frame(self):
args, varargs, varkw, locals = inspect.getargvalues(mod.fr)
with self.assertDeprecated('getargvalues'):
args, varargs, varkw, locals = inspect.getargvalues(mod.fr)
self.assertEqual(args, ['x', 'y'])
self.assertEqual(varargs, None)
self.assertEqual(varkw, None)
self.assertEqual(locals, {'x': 11, 'p': 11, 'y': 14})
self.assertEqual(inspect.formatargvalues(args, varargs, varkw, locals),
'(x=11, y=14)')
with self.assertDeprecated('formatargvalues'):
format = inspect.formatargvalues(args, varargs, varkw, locals)
self.assertEqual(format, '(x=11, y=14)')

def test_previous_frame(self):
args, varargs, varkw, locals = inspect.getargvalues(mod.fr.f_back)
with self.assertDeprecated('getargvalues'):
args, varargs, varkw, locals = inspect.getargvalues(mod.fr.f_back)
self.assertEqual(args, ['a', 'b', 'c', 'd', 'e', 'f'])
self.assertEqual(varargs, 'g')
self.assertEqual(varkw, 'h')
self.assertEqual(inspect.formatargvalues(args, varargs, varkw, locals),
'(a=7, b=8, c=9, d=3, e=4, f=5, *g=(), **h={})')
with self.assertDeprecated('formatargvalues'):
format = inspect.formatargvalues(args, varargs, varkw, locals)
self.assertEqual(format,
'(a=7, b=8, c=9, d=3, e=4, f=5, *g=(), **h={})')

def test_frame_with_argument_override(self):
# This tests shows that the current implementation of `getargvalues`:
# 1. Does not render `/` correctly
# 2. Uses not real default values, but can also show redefined values
def inner(a=1, /, c=5, *, b=2):
global fr
a = 3
fr = inspect.currentframe()
b = 4

inner()
with self.assertDeprecated('getargvalues'):
args, varargs, varkw, locals = inspect.getargvalues(fr)
with self.assertDeprecated('formatargvalues'):
format = inspect.formatargvalues(args, varargs, varkw, locals)
self.assertEqual(format,
'(a=3, c=5, b=4)')


class GetSourceBase(unittest.TestCase):
# Subclasses must override.
Expand Down Expand Up @@ -4623,6 +4657,42 @@ class D2(D1):
self.assertEqual(inspect.signature(D2), inspect.signature(D1))


class TestSignatureFromFrame(unittest.TestCase):
def test_signature_from_frame(self):
def inner(a=1, /, b=2, *e, c: int = 3, d, **f) -> None:
global fr
Copy link
Member

Choose a reason for hiding this comment

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

Using a global sounds like a bad idea. You can use a "nonlocal" instead.

Sometimes, I use a mutable type instead, which is more or less the same:

ns = {}
def func():
    ns['name'] = value

fr = inspect.currentframe()

inner(d=4)
self.assertEqual(str(inspect.Signature.from_frame(fr)),
'(a=1, /, b=2, *e, c=3, d=4, **f)')

def inner(a, /, b, *e, c: int = 3, d, **f) -> None:
global fr
fr = inspect.currentframe()

inner(1, 2, d=4)
self.assertEqual(str(inspect.Signature.from_frame(fr)),
'(a=1, /, b=2, *e, c=3, d=4, **f)')

def test_signature_from_frame_defaults_change(self):
def inner(a=1, /, c=5, *, b=2):
global fr
a = 3
fr = inspect.currentframe()
b = 4

inner()
self.assertEqual(str(inspect.Signature.from_frame(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)')


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 @@
Deprecate :func:`inspect.getargvalues` and :func:`inspect.formatargvalues`,
slate it for removal in 3.15; instead use
:meth:`inspect.Signature.from_frame`.