diff --git a/Doc/library/inspect.rst b/Doc/library/inspect.rst index 196479eb7550e2..29bdae28b1fe26 100644 --- a/Doc/library/inspect.rst +++ b/Doc/library/inspect.rst @@ -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 ````. A custom *formatvalue* + function receives a placeholder object whose :func:`repr` is ```` + for these arguments. + .. note:: This function was inadvertently marked as deprecated in Python 3.5. diff --git a/Lib/inspect.py b/Lib/inspect.py index db9129d47db6ab..d06c8078ded870 100644 --- a/Lib/inspect.py +++ b/Lib/inspect.py @@ -1397,6 +1397,14 @@ def _formatannotation(annotation): return formatannotation(annotation, module) return _formatannotation +# A placeholder with as repr (using a string would have '' +# with quotes). This is used in formatargvalues function. +class _Deleted: + def __repr__(self): + return '' + +_deleted = _Deleted() +del _Deleted def formatargvalues(args, varargs, varkw, locals, formatarg=str, @@ -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): diff --git a/Lib/test/test_inspect/test_inspect.py b/Lib/test/test_inspect/test_inspect.py index 5a3aa3e43fb2b2..c5581041f044b1 100644 --- a/Lib/test/test_inspect/test_inspect.py +++ b/Lib/test/test_inspect/test_inspect.py @@ -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=, x=8, y=)') + + def test_formatargvalues_deleted_argument_kinds(self): + cases = ( + ((), "(x=None, a=2, k=4, *args=(3,), **kwargs={'z': 5})"), + (('x',), + "(x=, a=2, k=4, *args=(3,), **kwargs={'z': 5})"), + (('a',), + "(x=None, a=, k=4, *args=(3,), **kwargs={'z': 5})"), + (('k',), + "(x=None, a=2, k=, *args=(3,), **kwargs={'z': 5})"), + (('args',), + "(x=None, a=2, k=4, *args=, **kwargs={'z': 5})"), + (('kwargs',), + '(x=None, a=2, k=4, *args=(3,), **kwargs=)'), + (('x', 'a', 'k', 'args', 'kwargs'), + '(x=, a=, k=, ' + '*args=, **kwargs=)'), + ) + 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:, *ARGS:, **KWARGS:)') + def test_previous_frame(self): args, varargs, varkw, locals = inspect.getargvalues(mod.fr.f_back) self.assertEqual(args, ['a', 'b', 'c', 'd', 'e', 'f']) diff --git a/Misc/NEWS.d/next/Library/2023-01-26-09-36-24.gh-issue-61448.SOAkvt.rst b/Misc/NEWS.d/next/Library/2023-01-26-09-36-24.gh-issue-61448.SOAkvt.rst new file mode 100644 index 00000000000000..6ea99ba3310b7c --- /dev/null +++ b/Misc/NEWS.d/next/Library/2023-01-26-09-36-24.gh-issue-61448.SOAkvt.rst @@ -0,0 +1,3 @@ +Fix :func:`inspect.formatargvalues` raising :exc:`KeyError` for deleted +arguments, including ``*args`` and ``**kwargs``, by displaying ````. +Patch by Ezio Melotti and Furkan Onder.