Skip to content
5 changes: 5 additions & 0 deletions Doc/library/inspect.rst
Original file line number Diff line number Diff line change
Expand Up @@ -1347,6 +1347,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.

Arguments whose names are absent from *locals*, including ``*`` and ``**``
arguments, are displayed with a value of ``<deleted>``. A custom *formatvalue*
function receives a placeholder object whose :func:`repr` is ``<deleted>``
for these arguments.

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

Expand Down
19 changes: 16 additions & 3 deletions Lib/inspect.py
Original file line number Diff line number Diff line change
Expand Up @@ -1397,6 +1397,14 @@ def _formatannotation(annotation):
return formatannotation(annotation, module)
return _formatannotation

# A placeholder with <deleted> as repr (using a string would have '<deleted>'
# with quotes). This is used in formatargvalues function.
class _Deleted:
def __repr__(self):
return '<deleted>'

_deleted = _Deleted()
del _Deleted

def formatargvalues(args, varargs, varkw, locals,
formatarg=str,
Expand All @@ -1411,14 +1419,19 @@ def formatargvalues(args, varargs, varkw, locals,
argument is an optional function to format the sequence of arguments."""
def convert(name, locals=locals,
formatarg=formatarg, formatvalue=formatvalue):
return formatarg(name) + formatvalue(locals[name])
formatted_name = formatarg(name)
try:
value = locals[name]
except KeyError:
value = _deleted
return formatted_name + formatvalue(value)
specs = []
for i in range(len(args)):
specs.append(convert(args[i]))
if varargs:
specs.append(formatvarargs(varargs) + formatvalue(locals[varargs]))
specs.append(convert(varargs, formatarg=formatvarargs))
if varkw:
specs.append(formatvarkw(varkw) + formatvalue(locals[varkw]))
specs.append(convert(varkw, formatarg=formatvarkw))
return '(' + ', '.join(specs) + ')'

def _missing_arguments(f_name, argnames, pos, values):
Expand Down
84 changes: 84 additions & 0 deletions Lib/test/test_inspect/test_inspect.py
Original file line number Diff line number Diff line change
Expand Up @@ -595,6 +595,90 @@ def test_frame(self):
self.assertEqual(inspect.formatargvalues(args, varargs, varkw, locals),
'(x=11, y=14)')

def return_current_frame(x):
return inspect.currentframe()

self.assertEqual(inspect.formatargvalues(
*inspect.getargvalues(return_current_frame(5))), '(x=5)')
self.assertEqual(inspect.formatargvalues(
*inspect.getargvalues(return_current_frame(9))), '(x=9)')

# See issue-61448
def test_formatargvalues_deleted_args(self):
def fun(a, b, x, y):
del b, y
return inspect.currentframe()

args = inspect.getargvalues(fun(2, 4, 8, 16))
self.assertEqual(inspect.formatargvalues(*args),
'(a=2, b=<deleted>, x=8, y=<deleted>)')

def test_formatargvalues_deleted_argument_kinds(self):
cases = (
((), "(x=None, a=2, k=4, *args=(3,), **kwargs={'z': 5})"),
(('x',),
"(x=<deleted>, a=2, k=4, *args=(3,), **kwargs={'z': 5})"),
(('a',),
"(x=None, a=<deleted>, k=4, *args=(3,), **kwargs={'z': 5})"),
(('k',),
"(x=None, a=2, k=<deleted>, *args=(3,), **kwargs={'z': 5})"),
(('args',),
"(x=None, a=2, k=4, *args=<deleted>, **kwargs={'z': 5})"),
(('kwargs',),
'(x=None, a=2, k=4, *args=(3,), **kwargs=<deleted>)'),
(('x', 'a', 'k', 'args', 'kwargs'),
'(x=<deleted>, a=<deleted>, k=<deleted>, '
'*args=<deleted>, **kwargs=<deleted>)'),
)
for deleted, expected in cases:
with self.subTest(deleted=deleted):
def fun(x, /, a, *args, k, **kwargs):
if 'x' in deleted:
del x
if 'a' in deleted:
del a
if 'k' in deleted:
del k
if 'args' in deleted:
del args
if 'kwargs' in deleted:
del kwargs
return inspect.currentframe()

frame = fun(None, 2, 3, k=4, z=5)
try:
self.assertEqual(inspect.formatargvalues(
*inspect.getargvalues(frame)), expected)
finally:
frame.clear()

def test_formatargvalues_mapping_lookup(self):
class LookupDict(dict):
def __getitem__(self, name):
return super().__getitem__(name) + 1

for args, varargs, varkw, name in (
(['x'], None, None, 'x'),
([], 'args', None, '*args'),
([], None, 'kwargs', '**kwargs'),
):
with self.subTest(name=name):
values = collections.defaultdict(lambda: 42)
self.assertEqual(inspect.formatargvalues(
args, varargs, varkw, values), f'({name}=42)')
values = LookupDict({name.lstrip('*'): 5})
self.assertEqual(inspect.formatargvalues(
args, varargs, varkw, values), f'({name}=6)')

def test_formatargvalues_deleted_custom_formatters(self):
self.assertEqual(inspect.formatargvalues(
['x'], 'args', 'kwargs', {},
formatarg=lambda name: name.upper(),
formatvarargs=lambda name: '*' + name.upper(),
formatvarkw=lambda name: '**' + name.upper(),
formatvalue=lambda value: ':' + repr(value)),
'(X:<deleted>, *ARGS:<deleted>, **KWARGS:<deleted>)')

def test_previous_frame(self):
args, varargs, varkw, locals = inspect.getargvalues(mod.fr.f_back)
self.assertEqual(args, ['a', 'b', 'c', 'd', 'e', 'f'])
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
Fix :func:`inspect.formatargvalues` raising :exc:`KeyError` for deleted
arguments, including ``*args`` and ``**kwargs``, by displaying ``<deleted>``.
Patch by Ezio Melotti and Furkan Onder.
Loading