From 566790c56673e577d1242863216a7f952eaf3b7d Mon Sep 17 00:00:00 2001 From: sobolevn Date: Tue, 5 Sep 2023 13:23:37 +0300 Subject: [PATCH 1/7] gh-108901: Deprecate `inspect.getargs`, use `Signature.from_code` instead --- Doc/library/inspect.rst | 10 ++ Lib/inspect.py | 110 ++++++++++++------ Lib/test/test_inspect.py | 54 +++++++++ ...-09-05-13-23-06.gh-issue-108901.2KcZab.rst | 2 + 4 files changed, 141 insertions(+), 35 deletions(-) create mode 100644 Misc/NEWS.d/next/Library/2023-09-05-13-23-06.gh-issue-108901.2KcZab.rst diff --git a/Doc/library/inspect.rst b/Doc/library/inspect.rst index 7884308a333020..14213e747f7902 100644 --- a/Doc/library/inspect.rst +++ b/Doc/library/inspect.rst @@ -765,6 +765,16 @@ function. .. versionadded:: 3.10 ``globalns`` and ``localns`` parameters. + .. classmethod:: Signature.from_code(co) + + Return a :class:`Signature` (or its subclass) object + for a given :class:`code object ` ``co``. + + Since code objects do not know anything + about default values or annotations, + they will always be ommited from the signature. + + .. versionadded:: 3.13 .. class:: Parameter(name, kind, *, default=Parameter.empty, annotation=Parameter.empty) diff --git a/Lib/inspect.py b/Lib/inspect.py index c8211833dd0831..10beaa11ed472b 100644 --- a/Lib/inspect.py +++ b/Lib/inspect.py @@ -1364,6 +1364,13 @@ def getargs(co): 'args' is the list of argument names. Keyword-only arguments are appended. 'varargs' and 'varkw' are the names of the * and ** arguments or None.""" + import warnings + warnings.warn( + 'getargs is deprecated since Python3.13, ' + 'use Signature.from_code instead', + DeprecationWarning, + ) + if not iscode(co): raise TypeError('{!r} is not a code object'.format(co)) @@ -2384,52 +2391,34 @@ def p(name_node, default_node, default=empty): return cls(parameters, return_annotation=cls.empty) -def _signature_from_builtin(cls, func, skip_bound_arg=True): +def _signature_from_code(cls, + func_code, + globals=None, + locals=None, + eval_str=False, + is_duck_function=False, + func=None): """Private helper function to get signature for - builtin callables. + code objects. """ - - if not _signature_is_builtin(func): - raise TypeError("{!r} is not a Python builtin " - "function".format(func)) - - s = getattr(func, "__text_signature__", None) - if not s: - raise ValueError("no signature found for builtin {!r}".format(func)) - - return _signature_fromstr(cls, func, s, skip_bound_arg) - - -def _signature_from_function(cls, func, skip_bound_arg=True, - globals=None, locals=None, eval_str=False): - """Private helper: constructs Signature for the given python function.""" - - is_duck_function = False - if not isfunction(func): - if _signature_is_functionlike(func): - is_duck_function = True - else: - # If it's not a pure Python function, and not a duck type - # of pure function: - raise TypeError('{!r} is not a Python function'.format(func)) - - s = getattr(func, "__text_signature__", None) - if s: - return _signature_fromstr(cls, func, s, skip_bound_arg) - 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: + # `func` can be `None` when we get a signature from just a `CodeObject` + annotations = {} + defaults = None + kwdefaults = None if defaults: pos_default_count = len(defaults) @@ -2495,6 +2484,46 @@ def _signature_from_function(cls, func, skip_bound_arg=True, __validate_parameters__=is_duck_function) +def _signature_from_builtin(cls, func, skip_bound_arg=True): + """Private helper function to get signature for + builtin callables. + """ + + if not _signature_is_builtin(func): + raise TypeError("{!r} is not a Python builtin " + "function".format(func)) + + s = getattr(func, "__text_signature__", None) + if not s: + raise ValueError("no signature found for builtin {!r}".format(func)) + + return _signature_fromstr(cls, func, s, skip_bound_arg) + + +def _signature_from_function(cls, func, skip_bound_arg=True, + globals=None, locals=None, eval_str=False): + """Private helper: constructs Signature for the given python function.""" + + is_duck_function = False + if not isfunction(func): + if _signature_is_functionlike(func): + is_duck_function = True + else: + # If it's not a pure Python function, and not a duck type + # of pure function: + raise TypeError('{!r} is not a Python function'.format(func)) + + 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_callable(obj, *, follow_wrapper_chains=True, skip_bound_arg=True, @@ -3107,6 +3136,17 @@ def from_callable(cls, obj, *, follow_wrapper_chains=follow_wrapped, globals=globals, locals=locals, eval_str=eval_str) + @classmethod + def from_code(cls, co): + """Constructs Signature for the given code object. + + Signatures created from code objects cannot know about annotations + or default values. + """ + if not iscode(co): + raise TypeError('{!r} is not a code object'.format(co)) + return _signature_from_code(cls, co) + @property def parameters(self): return self._parameters diff --git a/Lib/test/test_inspect.py b/Lib/test/test_inspect.py index 78ef817906b2aa..dae49433e5b950 100644 --- a/Lib/test/test_inspect.py +++ b/Lib/test/test_inspect.py @@ -435,6 +435,25 @@ class ConcreteGrandchild(ClassExample): self.assertEqual(isabstract_checks, [True, True, False]) +class TestGetArgs(unittest.TestCase): + def test_getargs_is_deprecated(self): + # We are only interested in the deprecation warning, + # because `getargs` is: + # - undocumented + # - untested + # - deprecated + # - has modern alternative + # - incorrect anyways. + def func(): ... + + msg = ( + 'getargs is deprecated since Python3.13, ' + 'use Signature.from_code instead' + ) + with self.assertWarnsRegex(DeprecationWarning, msg): + inspect.getargs(func.__code__) + + class TestInterpreterStack(IsTestBase): def __init__(self, *args, **kwargs): unittest.TestCase.__init__(self, *args, **kwargs) @@ -3674,6 +3693,41 @@ def test_signature_on_noncallable_mocks(self): with self.assertRaises(TypeError): inspect.signature(mock) + def test_signature_on_code_objects(self): + def func1( + a, b: int, + /, + c, d: str = 'a', + *args: int, + e, f: bool = True, + **kwargs: str, + ) -> float: + ... + + def func2(a: str, b: int = 0, /): ... + def func3(): ... + def func4(*a, **k): ... + def func5(*, kw=False): ... + + known_sigs = { + func1: '(a, b, /, c, d, *args, e, f, **kwargs)', + func2: '(a, b, /)', + func3: '()', + func4: '(*a, **k)', + func5: '(*, kw)', + } + + for test_func, expected_sig in known_sigs.items(): + with self.subTest(test_func=test_func, expected_sig=expected_sig): + self.assertEqual( + str(inspect.Signature.from_code(test_func.__code__)), + expected_sig, + ) + + def test_signature_on_non_code_objects(self): + with self.assertRaisesRegex(TypeError, '1 is not a code object'): + inspect.Signature.from_code(1) + def test_signature_equality(self): def foo(a, *, b:int) -> float: pass self.assertFalse(inspect.signature(foo) == 42) diff --git a/Misc/NEWS.d/next/Library/2023-09-05-13-23-06.gh-issue-108901.2KcZab.rst b/Misc/NEWS.d/next/Library/2023-09-05-13-23-06.gh-issue-108901.2KcZab.rst new file mode 100644 index 00000000000000..81b79f5b5fb065 --- /dev/null +++ b/Misc/NEWS.d/next/Library/2023-09-05-13-23-06.gh-issue-108901.2KcZab.rst @@ -0,0 +1,2 @@ +Deprecate ``inspect.getargs``, use :meth:`inspect.Signature.from_code` +instead. From 20875cbd9c56ca443f34ffeb673966d19e1e226e Mon Sep 17 00:00:00 2001 From: sobolevn Date: Tue, 5 Sep 2023 17:25:30 +0300 Subject: [PATCH 2/7] Adjust stacklevel --- Lib/inspect.py | 1 + 1 file changed, 1 insertion(+) diff --git a/Lib/inspect.py b/Lib/inspect.py index 10beaa11ed472b..697f73f6b5f607 100644 --- a/Lib/inspect.py +++ b/Lib/inspect.py @@ -1369,6 +1369,7 @@ def getargs(co): 'getargs is deprecated since Python3.13, ' 'use Signature.from_code instead', DeprecationWarning, + stacklevel=2, ) if not iscode(co): From 604f25bfc47c756c9226677d11f69278f136828d Mon Sep 17 00:00:00 2001 From: sobolevn Date: Tue, 5 Sep 2023 19:57:06 +0300 Subject: [PATCH 3/7] Do not deprecate `getargs()` yet --- Lib/inspect.py | 8 -------- Lib/test/test_inspect.py | 19 ------------------- 2 files changed, 27 deletions(-) diff --git a/Lib/inspect.py b/Lib/inspect.py index 697f73f6b5f607..f0d98168a5cceb 100644 --- a/Lib/inspect.py +++ b/Lib/inspect.py @@ -1364,14 +1364,6 @@ def getargs(co): 'args' is the list of argument names. Keyword-only arguments are appended. 'varargs' and 'varkw' are the names of the * and ** arguments or None.""" - import warnings - warnings.warn( - 'getargs is deprecated since Python3.13, ' - 'use Signature.from_code instead', - DeprecationWarning, - stacklevel=2, - ) - if not iscode(co): raise TypeError('{!r} is not a code object'.format(co)) diff --git a/Lib/test/test_inspect.py b/Lib/test/test_inspect.py index dae49433e5b950..c08961f777a017 100644 --- a/Lib/test/test_inspect.py +++ b/Lib/test/test_inspect.py @@ -435,25 +435,6 @@ class ConcreteGrandchild(ClassExample): self.assertEqual(isabstract_checks, [True, True, False]) -class TestGetArgs(unittest.TestCase): - def test_getargs_is_deprecated(self): - # We are only interested in the deprecation warning, - # because `getargs` is: - # - undocumented - # - untested - # - deprecated - # - has modern alternative - # - incorrect anyways. - def func(): ... - - msg = ( - 'getargs is deprecated since Python3.13, ' - 'use Signature.from_code instead' - ) - with self.assertWarnsRegex(DeprecationWarning, msg): - inspect.getargs(func.__code__) - - class TestInterpreterStack(IsTestBase): def __init__(self, *args, **kwargs): unittest.TestCase.__init__(self, *args, **kwargs) From d69623b15b1f3909b269b20eecb9ca2db6425a5c Mon Sep 17 00:00:00 2001 From: sobolevn Date: Tue, 5 Sep 2023 19:58:42 +0300 Subject: [PATCH 4/7] Change NEWS --- .../Library/2023-09-05-13-23-06.gh-issue-108901.2KcZab.rst | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Misc/NEWS.d/next/Library/2023-09-05-13-23-06.gh-issue-108901.2KcZab.rst b/Misc/NEWS.d/next/Library/2023-09-05-13-23-06.gh-issue-108901.2KcZab.rst index 81b79f5b5fb065..438adfb47847b3 100644 --- a/Misc/NEWS.d/next/Library/2023-09-05-13-23-06.gh-issue-108901.2KcZab.rst +++ b/Misc/NEWS.d/next/Library/2023-09-05-13-23-06.gh-issue-108901.2KcZab.rst @@ -1,2 +1,2 @@ -Deprecate ``inspect.getargs``, use :meth:`inspect.Signature.from_code` -instead. +Add :meth:`inspect.Signature.from_code` to be able +to construct :class:`inspect.Signature` objects from :class:`types.CodeType`. From b3e7978d27a48258f287c6a77b9a394b07225507 Mon Sep 17 00:00:00 2001 From: sobolevn Date: Tue, 5 Sep 2023 20:51:18 +0300 Subject: [PATCH 5/7] Address review --- Doc/library/inspect.rst | 4 +- Lib/inspect.py | 95 ++++++++++++++++++++-------------------- Lib/test/test_inspect.py | 8 ++-- 3 files changed, 55 insertions(+), 52 deletions(-) diff --git a/Doc/library/inspect.rst b/Doc/library/inspect.rst index 14213e747f7902..93a8917ee8893b 100644 --- a/Doc/library/inspect.rst +++ b/Doc/library/inspect.rst @@ -772,7 +772,9 @@ function. Since code objects do not know anything about default values or annotations, - they will always be ommited from the signature. + they will always be omitted from the signature. + It is recommended to use :meth:`Signature.from_callable` + when function object is available, it does not have these limitations. .. versionadded:: 3.13 diff --git a/Lib/inspect.py b/Lib/inspect.py index f0d98168a5cceb..cf4ca097a9e205 100644 --- a/Lib/inspect.py +++ b/Lib/inspect.py @@ -2384,13 +2384,53 @@ def p(name_node, default_node, default=empty): return cls(parameters, return_annotation=cls.empty) +def _signature_from_builtin(cls, func, skip_bound_arg=True): + """Private helper function to get signature for + builtin callables. + """ + + if not _signature_is_builtin(func): + raise TypeError("{!r} is not a Python builtin " + "function".format(func)) + + s = getattr(func, "__text_signature__", None) + if not s: + raise ValueError("no signature found for builtin {!r}".format(func)) + + return _signature_fromstr(cls, func, s, skip_bound_arg) + + +def _signature_from_function(cls, func, skip_bound_arg=True, + globals=None, locals=None, eval_str=False): + """Private helper: constructs Signature for the given python function.""" + + is_duck_function = False + if not isfunction(func): + if _signature_is_functionlike(func): + is_duck_function = True + else: + # If it's not a pure Python function, and not a duck type + # of pure function: + raise TypeError('{!r} is not a Python function'.format(func)) + + 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): + func_code, + globals=None, + locals=None, + eval_str=False, + is_duck_function=False, + func=None): """Private helper function to get signature for code objects. """ @@ -2408,7 +2448,6 @@ def _signature_from_code(cls, defaults = func.__defaults__ kwdefaults = func.__kwdefaults__ else: - # `func` can be `None` when we get a signature from just a `CodeObject` annotations = {} defaults = None kwdefaults = None @@ -2477,46 +2516,6 @@ def _signature_from_code(cls, __validate_parameters__=is_duck_function) -def _signature_from_builtin(cls, func, skip_bound_arg=True): - """Private helper function to get signature for - builtin callables. - """ - - if not _signature_is_builtin(func): - raise TypeError("{!r} is not a Python builtin " - "function".format(func)) - - s = getattr(func, "__text_signature__", None) - if not s: - raise ValueError("no signature found for builtin {!r}".format(func)) - - return _signature_fromstr(cls, func, s, skip_bound_arg) - - -def _signature_from_function(cls, func, skip_bound_arg=True, - globals=None, locals=None, eval_str=False): - """Private helper: constructs Signature for the given python function.""" - - is_duck_function = False - if not isfunction(func): - if _signature_is_functionlike(func): - is_duck_function = True - else: - # If it's not a pure Python function, and not a duck type - # of pure function: - raise TypeError('{!r} is not a Python function'.format(func)) - - 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_callable(obj, *, follow_wrapper_chains=True, skip_bound_arg=True, @@ -3137,7 +3136,7 @@ def from_code(cls, co): or default values. """ if not iscode(co): - raise TypeError('{!r} is not a code object'.format(co)) + raise TypeError(f'code object is expected, got {type(co)}') return _signature_from_code(cls, co) @property diff --git a/Lib/test/test_inspect.py b/Lib/test/test_inspect.py index c08961f777a017..3057dde9ce1ebe 100644 --- a/Lib/test/test_inspect.py +++ b/Lib/test/test_inspect.py @@ -3674,7 +3674,7 @@ def test_signature_on_noncallable_mocks(self): with self.assertRaises(TypeError): inspect.signature(mock) - def test_signature_on_code_objects(self): + def test_signature_from_code(self): def func1( a, b: int, /, @@ -3705,8 +3705,10 @@ def func5(*, kw=False): ... expected_sig, ) - def test_signature_on_non_code_objects(self): - with self.assertRaisesRegex(TypeError, '1 is not a code object'): + with self.assertRaisesRegex( + TypeError, + 'code object is expected, got int', + ): inspect.Signature.from_code(1) def test_signature_equality(self): From efdb6177e27ef6f44e4aeb455a6d710fc77e0784 Mon Sep 17 00:00:00 2001 From: sobolevn Date: Tue, 5 Sep 2023 20:56:56 +0300 Subject: [PATCH 6/7] Fix test --- Lib/inspect.py | 2 +- Lib/test/test_inspect.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Lib/inspect.py b/Lib/inspect.py index cf4ca097a9e205..a453be3d164143 100644 --- a/Lib/inspect.py +++ b/Lib/inspect.py @@ -3136,7 +3136,7 @@ def from_code(cls, co): or default values. """ if not iscode(co): - raise TypeError(f'code object is expected, got {type(co)}') + raise TypeError(f'code object is expected, got {type(co)!r}') return _signature_from_code(cls, co) @property diff --git a/Lib/test/test_inspect.py b/Lib/test/test_inspect.py index 3057dde9ce1ebe..029a383bbf4f5c 100644 --- a/Lib/test/test_inspect.py +++ b/Lib/test/test_inspect.py @@ -3707,7 +3707,7 @@ def func5(*, kw=False): ... with self.assertRaisesRegex( TypeError, - 'code object is expected, got int', + "code object is expected, got ", ): inspect.Signature.from_code(1) From 77da9c643cb0595d069be94e75973c4fa7b82048 Mon Sep 17 00:00:00 2001 From: sobolevn Date: Fri, 8 Sep 2023 14:09:50 +0300 Subject: [PATCH 7/7] Address review --- Doc/library/inspect.rst | 14 ++++++++------ Lib/inspect.py | 19 +++++++++---------- Lib/test/test_inspect.py | 2 +- 3 files changed, 18 insertions(+), 17 deletions(-) diff --git a/Doc/library/inspect.rst b/Doc/library/inspect.rst index 93a8917ee8893b..458f8d4d3f4816 100644 --- a/Doc/library/inspect.rst +++ b/Doc/library/inspect.rst @@ -768,13 +768,15 @@ function. .. classmethod:: Signature.from_code(co) Return a :class:`Signature` (or its subclass) object - for a given :class:`code object ` ``co``. + for a given :class:`code object ` *co*. - Since code objects do not know anything - about default values or annotations, - they will always be omitted from the signature. - It is recommended to use :meth:`Signature.from_callable` - when function object is available, it does not have these limitations. + .. note:: + + Default values and annotations + will not be included in the signature, + since code objects have no knowledge of such things. + It is recommended to use :meth:`Signature.from_callable` + when a function object is available, it does not have these limitations. .. versionadded:: 3.13 diff --git a/Lib/inspect.py b/Lib/inspect.py index a453be3d164143..a8c176869236ce 100644 --- a/Lib/inspect.py +++ b/Lib/inspect.py @@ -2416,12 +2416,13 @@ 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) + 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, @@ -2431,9 +2432,7 @@ def _signature_from_code(cls, eval_str=False, is_duck_function=False, func=None): - """Private helper function to get signature for - code objects. - """ + """Private helper function to get signature for code objects.""" Parameter = cls._parameter_cls # Parameter information. @@ -3136,7 +3135,7 @@ def from_code(cls, co): or default values. """ if not iscode(co): - raise TypeError(f'code object is expected, got {type(co)!r}') + raise TypeError(f'code object was expected, got {type(co).__name__!r}') return _signature_from_code(cls, co) @property diff --git a/Lib/test/test_inspect.py b/Lib/test/test_inspect.py index 029a383bbf4f5c..d9df28b38297f6 100644 --- a/Lib/test/test_inspect.py +++ b/Lib/test/test_inspect.py @@ -3707,7 +3707,7 @@ def func5(*, kw=False): ... with self.assertRaisesRegex( TypeError, - "code object is expected, got ", + "code object was expected, got 'int'", ): inspect.Signature.from_code(1)