From ad7389bb20d437b3c04f57a0923012bdd6380e7d Mon Sep 17 00:00:00 2001 From: sobolevn Date: Wed, 22 Nov 2023 17:55:03 +0300 Subject: [PATCH 1/4] wip --- Doc/library/inspect.rst | 14 ++++++ Lib/inspect.py | 70 ++++++++++++++++++++++++--- Lib/test/test_inspect/test_inspect.py | 52 ++++++++++++++++++++ 3 files changed, 130 insertions(+), 6 deletions(-) diff --git a/Doc/library/inspect.rst b/Doc/library/inspect.rst index b463c0b6d0e402..2253a0800d387e 100644 --- a/Doc/library/inspect.rst +++ b/Doc/library/inspect.rst @@ -773,6 +773,12 @@ function. .. versionadded:: 3.10 ``globalns`` and ``localns`` parameters. + .. 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) @@ -1055,6 +1061,10 @@ 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`` PyPI package. + .. note:: This function was inadvertently marked as deprecated in Python 3.5. @@ -1065,6 +1075,10 @@ 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`` PyPI package. + .. note:: This function was inadvertently marked as deprecated in Python 3.5. diff --git a/Lib/inspect.py b/Lib/inspect.py index aaa22bef896602..7e7741356c569c 100644 --- a/Lib/inspect.py +++ b/Lib/inspect.py @@ -1489,6 +1489,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) @@ -1524,6 +1533,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]) @@ -2241,7 +2259,6 @@ def _signature_strip_non_python_syntax(signature): clean_signature = ''.join(text).strip().replace("\n", "") return clean_signature, self_parameter - def _signature_fromstr(cls, obj, s, skip_bound_arg=True): """Private helper to parse content of '__text_signature__' and return a Signature based on it. @@ -2416,20 +2433,53 @@ def _signature_from_function(cls, func, skip_bound_arg=True, s = getattr(func, "__text_signature__", None) if s: return _signature_fromstr(cls, func, s, skip_bound_arg) - + return _signature_from_code(cls, + func.__code__, + globals=globals, + locals=locals, + eval_str=eval_str, + is_duck_function=is_duck_function, + func=func) + +def _signature_from_code(cls, + func_code, + *, + globals=None, + locals=None, + eval_str=False, + is_duck_function=False, + func=None, + compute_defaults=False): + """Private helper: function to get signature from code objects.""" Parameter = cls._parameter_cls # Parameter information. - func_code = func.__code__ 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 func is not None: + annotations = get_annotations(func, globals=globals, locals=locals, eval_str=eval_str) + defaults = func.__defaults__ + kwdefaults = func.__kwdefaults__ + else: + annotations = {} + if compute_defaults: + defaults = [] + for name in positional: + if name in locals: + defaults.append(locals[name]) + defaults = tuple(defaults) + + kwdefaults = {} + for name in keyword_only: + if name in locals: + kwdefaults.update({name: locals[name]}) + else: + defaults = None + kwdefaults = None if defaults: pos_default_count = len(defaults) @@ -3109,6 +3159,14 @@ 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 the given frame object.""" + return _signature_from_code(cls, frame.f_code, + locals=frame.f_locals, + compute_defaults=True, + is_duck_function=True) + @property def parameters(self): return self._parameters diff --git a/Lib/test/test_inspect/test_inspect.py b/Lib/test/test_inspect/test_inspect.py index becbb0498bbb3f..cbabaac8865455 100644 --- a/Lib/test/test_inspect/test_inspect.py +++ b/Lib/test/test_inspect/test_inspect.py @@ -505,6 +505,22 @@ def test_previous_frame(self): self.assertEqual(inspect.formatargvalues(args, varargs, varkw, locals), '(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() + args, varargs, varkw, locals = inspect.getargvalues(fr) + self.assertEqual(inspect.formatargvalues(args, varargs, varkw, locals), + '(a=3, c=5, b=4)') + + class GetSourceBase(unittest.TestCase): # Subclasses must override. fodderModule = None @@ -4137,6 +4153,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 + 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 From 0edd93a5217fa7bf89444e57df7175c8089cd5fd Mon Sep 17 00:00:00 2001 From: sobolevn Date: Sun, 3 Dec 2023 10:40:28 +0300 Subject: [PATCH 2/4] Simplify the implementation --- Lib/inspect.py | 68 ++++++++++----------------- Lib/test/test_inspect/test_inspect.py | 34 ++++++++++---- 2 files changed, 51 insertions(+), 51 deletions(-) diff --git a/Lib/inspect.py b/Lib/inspect.py index f14434f5b8ceb6..2e9c8ef3edf6c9 100644 --- a/Lib/inspect.py +++ b/Lib/inspect.py @@ -2433,53 +2433,20 @@ def _signature_from_function(cls, func, skip_bound_arg=True, s = getattr(func, "__text_signature__", None) if s: return _signature_fromstr(cls, func, s, skip_bound_arg) - return _signature_from_code(cls, - func.__code__, - globals=globals, - locals=locals, - eval_str=eval_str, - is_duck_function=is_duck_function, - func=func) - -def _signature_from_code(cls, - func_code, - *, - globals=None, - locals=None, - eval_str=False, - is_duck_function=False, - func=None, - compute_defaults=False): - """Private helper: function to get signature from code objects.""" + Parameter = cls._parameter_cls # Parameter information. + func_code = func.__code__ 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] - if func is not None: - annotations = get_annotations(func, globals=globals, locals=locals, eval_str=eval_str) - defaults = func.__defaults__ - kwdefaults = func.__kwdefaults__ - else: - annotations = {} - if compute_defaults: - defaults = [] - for name in positional: - if name in locals: - defaults.append(locals[name]) - defaults = tuple(defaults) - - kwdefaults = {} - for name in keyword_only: - if name in locals: - kwdefaults.update({name: locals[name]}) - else: - defaults = None - kwdefaults = None + 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) @@ -3161,11 +3128,26 @@ def from_callable(cls, obj, *, @classmethod def from_frame(cls, frame): - """Constructs Signature from the given frame object.""" - return _signature_from_code(cls, frame.f_code, - locals=frame.f_locals, - compute_defaults=True, - is_duck_function=True) + """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]) + + 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): diff --git a/Lib/test/test_inspect/test_inspect.py b/Lib/test/test_inspect/test_inspect.py index a24049779b47c0..d4d4123772db96 100644 --- a/Lib/test/test_inspect/test_inspect.py +++ b/Lib/test/test_inspect/test_inspect.py @@ -443,6 +443,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') @@ -489,21 +499,26 @@ 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`: @@ -516,8 +531,11 @@ def inner(a=1, /, c=5, *, b=2): b = 4 inner() - args, varargs, varkw, locals = inspect.getargvalues(fr) - self.assertEqual(inspect.formatargvalues(args, varargs, varkw, locals), + 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)') From b8d651cabcc5714d166abb17e4e74bd0466100dd Mon Sep 17 00:00:00 2001 From: sobolevn Date: Sun, 3 Dec 2023 10:48:25 +0300 Subject: [PATCH 3/4] Add news --- Doc/whatsnew/3.13.rst | 5 +++++ .../Library/2023-12-03-10-46-38.gh-issue-108901.lJujFe.rst | 3 +++ 2 files changed, 8 insertions(+) create mode 100644 Misc/NEWS.d/next/Library/2023-12-03-10-46-38.gh-issue-108901.lJujFe.rst diff --git a/Doc/whatsnew/3.13.rst b/Doc/whatsnew/3.13.rst index 372e4a45468e68..6886f859976b92 100644 --- a/Doc/whatsnew/3.13.rst +++ b/Doc/whatsnew/3.13.rst @@ -626,6 +626,11 @@ Pending Removal in Python 3.15 All arguments will be removed from :func:`threading.RLock` in Python 3.15. (Contributed by Nikita Sobolev in :gh:`102029`.) +* :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 ------------------------------ diff --git a/Misc/NEWS.d/next/Library/2023-12-03-10-46-38.gh-issue-108901.lJujFe.rst b/Misc/NEWS.d/next/Library/2023-12-03-10-46-38.gh-issue-108901.lJujFe.rst new file mode 100644 index 00000000000000..11558a72282291 --- /dev/null +++ b/Misc/NEWS.d/next/Library/2023-12-03-10-46-38.gh-issue-108901.lJujFe.rst @@ -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`. From 1c475268efab5f80c6d0f0ac0175f5167e5c0c71 Mon Sep 17 00:00:00 2001 From: sobolevn Date: Sat, 9 Mar 2024 11:00:44 +0300 Subject: [PATCH 4/4] Ready to be reviewed --- Doc/library/inspect.rst | 6 ++++-- Doc/whatsnew/3.13.rst | 7 +++++-- Lib/inspect.py | 1 + 3 files changed, 10 insertions(+), 4 deletions(-) diff --git a/Doc/library/inspect.rst b/Doc/library/inspect.rst index 9192b890fd8a16..b44b0234d48917 100644 --- a/Doc/library/inspect.rst +++ b/Doc/library/inspect.rst @@ -1115,7 +1115,8 @@ Classes and functions .. deprecated-removed:: 3.13 3.15 Use :meth:`Signature.from_frame` instead. - For Python version older than 3.13 use ``inspect313`` PyPI package. + For Python version older than 3.13 use + `inspect313 `_ PyPI package. .. note:: This function was inadvertently marked as deprecated in Python 3.5. @@ -1129,7 +1130,8 @@ Classes and functions .. deprecated-removed:: 3.13 3.15 Use :meth:`Signature.from_frame` instead. - For Python version older than 3.13 use ``inspect313`` PyPI package. + For Python version older than 3.13 use + `inspect313 `_ PyPI package. .. note:: This function was inadvertently marked as deprecated in Python 3.5. diff --git a/Doc/whatsnew/3.13.rst b/Doc/whatsnew/3.13.rst index d92d8d846d743c..bb6c5084c2b466 100644 --- a/Doc/whatsnew/3.13.rst +++ b/Doc/whatsnew/3.13.rst @@ -1405,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 `_ + PyPI package which is a backport of this API for Pythons from 3.8 to 3.12. Changes in the Python API ------------------------- diff --git a/Lib/inspect.py b/Lib/inspect.py index 978313f7e0f91c..b48d9bfb3eb3f4 100644 --- a/Lib/inspect.py +++ b/Lib/inspect.py @@ -2258,6 +2258,7 @@ def _signature_strip_non_python_syntax(signature): clean_signature = ''.join(text).strip().replace("\n", "") return clean_signature, self_parameter + def _signature_fromstr(cls, obj, s, skip_bound_arg=True): """Private helper to parse content of '__text_signature__' and return a Signature based on it.