Skip to content

Commit f2baf1a

Browse files
pythongh-87106: Fix inspect.signature.bind handling of positional-only arguments with **kwargs
1 parent ecad802 commit f2baf1a

File tree

3 files changed

+29
-27
lines changed

3 files changed

+29
-27
lines changed

Lib/inspect.py

+14-12
Original file line numberDiff line numberDiff line change
@@ -3109,6 +3109,8 @@ def _bind(self, args, kwargs, *, partial=False):
31093109
parameters_ex = ()
31103110
arg_vals = iter(args)
31113111

3112+
pos_only_param_in_kwargs = []
3113+
31123114
while True:
31133115
# Let's iterate through the positional arguments and corresponding
31143116
# parameters
@@ -3129,10 +3131,9 @@ def _bind(self, args, kwargs, *, partial=False):
31293131
break
31303132
elif param.name in kwargs:
31313133
if param.kind == _POSITIONAL_ONLY:
3132-
msg = '{arg!r} parameter is positional only, ' \
3133-
'but was passed as a keyword'
3134-
msg = msg.format(arg=param.name)
3135-
raise TypeError(msg) from None
3134+
# Raise a TypeError once we are sure there is no
3135+
# **kwargs param later.
3136+
pos_only_param_in_kwargs.append(param)
31363137
parameters_ex = (param,)
31373138
break
31383139
elif (param.kind == _VAR_KEYWORD or
@@ -3215,20 +3216,21 @@ def _bind(self, args, kwargs, *, partial=False):
32153216

32163217
else:
32173218
if param.kind == _POSITIONAL_ONLY:
3218-
# This should never happen in case of a properly built
3219-
# Signature object (but let's have this check here
3220-
# to ensure correct behaviour just in case)
3221-
raise TypeError('{arg!r} parameter is positional only, '
3222-
'but was passed as a keyword'. \
3223-
format(arg=param.name))
3224-
3225-
arguments[param_name] = arg_val
3219+
# Restore the param in case there is a kwargs_param
3220+
kwargs[param_name] = arg_val
3221+
else:
3222+
arguments[param_name] = arg_val
32263223

32273224
if kwargs:
32283225
if kwargs_param is not None:
32293226
# Process our '**kwargs'-like parameter
32303227
arguments[kwargs_param.name] = kwargs
32313228
else:
3229+
if pos_only_param_in_kwargs:
3230+
raise TypeError('got some positional-only arguments passed as '
3231+
'keyword arguments: {arg!r} '. \
3232+
format(arg=', '.join(
3233+
param.name for param in pos_only_param_in_kwargs)))
32323234
raise TypeError(
32333235
'got an unexpected keyword argument {arg!r}'.format(
32343236
arg=next(iter(kwargs))))

Lib/test/test_inspect.py

+12-15
Original file line numberDiff line numberDiff line change
@@ -4155,18 +4155,9 @@ def test(a, *args, b, z=100, **kwargs):
41554155
self.assertEqual(ba.args, (10, 20))
41564156

41574157
def test_signature_bind_positional_only(self):
4158-
P = inspect.Parameter
4159-
4160-
def test(a_po, b_po, c_po=3, foo=42, *, bar=50, **kwargs):
4158+
def test(a_po, b_po, c_po=3, /, foo=42, *, bar=50, **kwargs):
41614159
return a_po, b_po, c_po, foo, bar, kwargs
41624160

4163-
sig = inspect.signature(test)
4164-
new_params = collections.OrderedDict(tuple(sig.parameters.items()))
4165-
for name in ('a_po', 'b_po', 'c_po'):
4166-
new_params[name] = new_params[name].replace(kind=P.POSITIONAL_ONLY)
4167-
new_sig = sig.replace(parameters=new_params.values())
4168-
test.__signature__ = new_sig
4169-
41704161
self.assertEqual(self.call(test, 1, 2, 4, 5, bar=6),
41714162
(1, 2, 4, 5, 6, {}))
41724163

@@ -4176,15 +4167,21 @@ def test(a_po, b_po, c_po=3, foo=42, *, bar=50, **kwargs):
41764167
self.assertEqual(self.call(test, 1, 2, foo=4, bar=5),
41774168
(1, 2, 3, 4, 5, {}))
41784169

4179-
with self.assertRaisesRegex(TypeError, "but was passed as a keyword"):
4180-
self.call(test, 1, 2, foo=4, bar=5, c_po=10)
4170+
self.assertEqual(self.call(test, 1, 2, foo=4, bar=5, c_po=10),
4171+
(1, 2, 3, 4, 5, {'c_po': 10}))
41814172

4182-
with self.assertRaisesRegex(TypeError, "parameter is positional only"):
4183-
self.call(test, 1, 2, c_po=4)
4173+
self.assertEqual(self.call(test, 1, 2, c_po=4),
4174+
(1, 2, 3, 42, 50, {'c_po': 4}))
41844175

4185-
with self.assertRaisesRegex(TypeError, "parameter is positional only"):
4176+
with self.assertRaisesRegex(TypeError, "missing 2 required positional arguments"):
41864177
self.call(test, a_po=1, b_po=2)
41874178

4179+
def test_without_var_kwargs(a_po, b_po, c_po=3, /, foo=42, *, bar=50):
4180+
return a_po, b_po, c_po, foo, bar
4181+
4182+
with self.assertRaisesRegex(TypeError, "positional-only arguments passed as keyword"):
4183+
self.call(test_without_var_kwargs, 1, 2, foo=4, bar=5, c_po=10)
4184+
41884185
def test_signature_bind_with_self_arg(self):
41894186
# Issue #17071: one of the parameters is named "self
41904187
def test(a, self, b):
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
Fixed handling in :meth:`inspect.signature.bind` of keyword arguments having
2+
the same name as positional-only arguments when a variadic keyword argument
3+
(e.g. `**kwargs`) is present.

0 commit comments

Comments
 (0)