Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 14 additions & 0 deletions Lib/inspect.py
Original file line number Diff line number Diff line change
Expand Up @@ -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__
Expand Down Expand Up @@ -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),
Expand Down
56 changes: 56 additions & 0 deletions Lib/test/test_inspect/test_inspect.py
Original file line number Diff line number Diff line change
Expand Up @@ -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):
Expand Down Expand Up @@ -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)

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
Speed up :func:`inspect.signature` for Python functions by skipping
redundant validation when creating :class:`inspect.Parameter` objects.
Loading