diff --git a/Lib/inspect.py b/Lib/inspect.py index 3683f8c3fd53308..bceed35a53abba1 100644 --- a/Lib/inspect.py +++ b/Lib/inspect.py @@ -2348,6 +2348,8 @@ def _signature_from_function(cls, func, skip_bound_arg=True, return _signature_fromstr(cls, func, s, skip_bound_arg) Parameter = cls._parameter_cls + if Parameter is Signature._parameter_cls and not is_duck_function: + Parameter = Parameter._from_code # Parameter information. func_code = func.__code__ @@ -2746,6 +2748,18 @@ def __init__(self, name, kind, *, default=_empty, annotation=_empty): self._name = name + @classmethod + def _from_code(cls, name, kind, *, default=_empty, annotation=_empty): + # Fast path for Python functions: only the name needs validation. + if iskeyword(name) or not name.isidentifier(): + return cls(name, kind, default=default, annotation=annotation) + self = object.__new__(cls) + self._name = name + self._kind = kind + self._default = default + self._annotation = annotation + return self + def __reduce__(self): return (type(self), (self._name, self._kind), diff --git a/Lib/test/test_inspect/test_inspect.py b/Lib/test/test_inspect/test_inspect.py index df5843abfcb8753..7c6f3324af94c2e 100644 --- a/Lib/test/test_inspect/test_inspect.py +++ b/Lib/test/test_inspect/test_inspect.py @@ -3556,6 +3556,48 @@ def __init__(self, marker): self.assertEqual(str(inspect.signature(funclike)), '(marker)') + @cpython_only + def test_signature_functionlike_invalid_names(self): + # The code object of a function-like object is not guaranteed + # to have valid parameter names, so they must be validated. + def func(a, b): + pass + + class funclike: + __name__ = func.__name__ + __code__ = func.__code__.replace(co_varnames=('a', '$b')) + __annotations__ = {} + __defaults__ = None + __kwdefaults__ = None + + def __call__(self, *args): + pass + + with self.assertRaisesRegex(ValueError, + 'is not a valid parameter name'): + inspect.signature(funclike()) + + def test_signature_parameter_cls_subclass(self): + # A Signature subclass can override _parameter_cls with a + # Parameter subclass that has its own constructor. + class MyParameter(inspect.Parameter): + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + self.extra = 'spam' + + class MySignature(inspect.Signature): + _parameter_cls = MyParameter + + def f(a, /, b=1, *args, c, d=2, **kwargs): + pass + + sig = MySignature.from_callable(f) + self.assertEqual(len(sig.parameters), 6) + for param in sig.parameters.values(): + self.assertIs(type(param), MyParameter) + self.assertEqual(param.extra, 'spam') + self.assertEqual(sig, inspect.signature(f)) + def test_signature_on_method(self): class Test: def __init__(*args): @@ -5656,6 +5698,20 @@ def test_signature_parameter_implicit(self): self.assertEqual(param.kind, inspect.Parameter.POSITIONAL_ONLY) self.assertEqual(param.name, 'implicit0') + @cpython_only + def test_signature_from_code_unusual_names(self): + def f(a, b): pass + f.__code__ = f.__code__.replace(co_varnames=('.0', 'b')) + sig = inspect.signature(f) + self.assertEqual(list(sig.parameters), ['implicit0', 'b']) + self.assertEqual(sig.parameters['implicit0'].kind, + inspect.Parameter.POSITIONAL_ONLY) + + f.__code__ = f.__code__.replace(co_varnames=('if', 'b')) + with self.assertRaisesRegex(ValueError, + 'is not a valid parameter name'): + inspect.signature(f) + def test_signature_parameter_immutability(self): p = inspect.Parameter('spam', kind=inspect.Parameter.KEYWORD_ONLY) diff --git a/Misc/NEWS.d/next/Library/2026-09-13-21-45-00.gh-issue-150816.pQ3kZs.rst b/Misc/NEWS.d/next/Library/2026-09-13-21-45-00.gh-issue-150816.pQ3kZs.rst new file mode 100644 index 000000000000000..4f67195f45fc6dc --- /dev/null +++ b/Misc/NEWS.d/next/Library/2026-09-13-21-45-00.gh-issue-150816.pQ3kZs.rst @@ -0,0 +1,2 @@ +Speed up :func:`inspect.signature` for Python functions by skipping +redundant validation when creating :class:`inspect.Parameter` objects.