Skip to content

Commit 8f11248

Browse files
committed
gh-107831: Fix inspect.getcallargs accepting positional-only args by keyword
getcallargs() built the set of acceptable keyword names from getfullargspec's 'args', which includes positional-only parameters, so a positional-only parameter could be filled by keyword without error -- unlike an actual call or Signature.bind(). Exclude positional-only names (derived from the code object's co_posonlyargcount) from keyword matching and raise the same TypeError the interpreter does.
1 parent 30aeeb3 commit 8f11248

3 files changed

Lines changed: 34 additions & 1 deletion

File tree

Lib/inspect.py

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1466,16 +1466,26 @@ def getcallargs(func, /, *positional, **named):
14661466
num_args = len(args)
14671467
num_defaults = len(defaults) if defaults else 0
14681468

1469+
# Positional-only parameters cannot be filled by keyword, matching the
1470+
# behaviour of an actual call and Signature.bind() (gh-107831).
1471+
func_obj = func.__func__ if ismethod(func) else func
1472+
code = getattr(func_obj, '__code__', None)
1473+
posonlyargs = set(args[:code.co_posonlyargcount]) if code is not None else set()
1474+
14691475
n = min(num_pos, num_args)
14701476
for i in range(n):
14711477
arg2value[args[i]] = positional[i]
14721478
if varargs:
14731479
arg2value[varargs] = tuple(positional[n:])
1474-
possible_kwargs = set(args + kwonlyargs)
1480+
possible_kwargs = set(args + kwonlyargs) - posonlyargs
14751481
if varkw:
14761482
arg2value[varkw] = {}
1483+
posonly_passed_as_kwarg = []
14771484
for kw, value in named.items():
14781485
if kw not in possible_kwargs:
1486+
if kw in posonlyargs and not varkw:
1487+
posonly_passed_as_kwarg.append(kw)
1488+
continue
14791489
if not varkw:
14801490
raise TypeError("%s() got an unexpected keyword argument %r" %
14811491
(f_name, kw))
@@ -1485,6 +1495,10 @@ def getcallargs(func, /, *positional, **named):
14851495
raise TypeError("%s() got multiple values for argument %r" %
14861496
(f_name, kw))
14871497
arg2value[kw] = value
1498+
if posonly_passed_as_kwarg:
1499+
raise TypeError(
1500+
"%s() got some positional-only arguments passed as keyword "
1501+
"arguments: %r" % (f_name, ', '.join(posonly_passed_as_kwarg)))
14881502
if num_pos > num_args and not varargs:
14891503
_too_many(f_name, args, kwonlyargs, varargs, num_defaults,
14901504
num_pos, arg2value)

Lib/test/test_inspect/test_inspect.py

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2266,6 +2266,22 @@ def test_plain(self):
22662266
self.assertEqualCallArgs(f, '2, **collections.UserDict(b=3)')
22672267
self.assertEqualCallArgs(f, 'b=2, **collections.UserDict(a=3)')
22682268

2269+
def test_positional_only(self):
2270+
# gh-107831: positional-only parameters cannot be filled by keyword,
2271+
# so getcallargs must match a real call (raise rather than bind).
2272+
f = self.makeCallable('a, b, /, c, d=1, *, e')
2273+
self.assertEqualCallArgs(f, '1, 2, 3, e=4')
2274+
self.assertEqualCallArgs(f, '1, 2, c=3, e=4')
2275+
self.assertEqualCallArgs(f, '1, 2, 3, 4, e=5')
2276+
self.assertEqualException(f, 'a=1, b=2, c=3, e=4')
2277+
self.assertEqualException(f, '1, b=2, c=3, e=4')
2278+
# With **kwargs the positional-only name is absorbed into it and the
2279+
# parameter itself is left unfilled (a missing-argument error).
2280+
g = self.makeCallable('a, /, **kw')
2281+
self.assertEqualCallArgs(g, '1')
2282+
self.assertEqualCallArgs(g, '1, x=2')
2283+
self.assertEqualException(g, 'a=1')
2284+
22692285
def test_varargs(self):
22702286
f = self.makeCallable('a, b=1, *c')
22712287
self.assertEqualCallArgs(f, '2')
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
:func:`inspect.getcallargs` now raises :exc:`TypeError` when a positional-only
2+
parameter is passed by keyword, consistent with calling the function directly
3+
and with :meth:`inspect.Signature.bind`.

0 commit comments

Comments
 (0)