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
16 changes: 15 additions & 1 deletion Lib/inspect.py
Original file line number Diff line number Diff line change
Expand Up @@ -1466,16 +1466,26 @@ def getcallargs(func, /, *positional, **named):
num_args = len(args)
num_defaults = len(defaults) if defaults else 0

# Positional-only parameters cannot be filled by keyword, matching the
# behaviour of an actual call and Signature.bind() (gh-107831).
func_obj = func.__func__ if ismethod(func) else func
code = getattr(func_obj, '__code__', None)
posonlyargs = set(args[:code.co_posonlyargcount]) if code is not None else set()

n = min(num_pos, num_args)
for i in range(n):
arg2value[args[i]] = positional[i]
if varargs:
arg2value[varargs] = tuple(positional[n:])
possible_kwargs = set(args + kwonlyargs)
possible_kwargs = set(args + kwonlyargs) - posonlyargs
if varkw:
arg2value[varkw] = {}
posonly_passed_as_kwarg = []
for kw, value in named.items():
if kw not in possible_kwargs:
if kw in posonlyargs and not varkw:
posonly_passed_as_kwarg.append(kw)
continue
if not varkw:
raise TypeError("%s() got an unexpected keyword argument %r" %
(f_name, kw))
Expand All @@ -1485,6 +1495,10 @@ def getcallargs(func, /, *positional, **named):
raise TypeError("%s() got multiple values for argument %r" %
(f_name, kw))
arg2value[kw] = value
if posonly_passed_as_kwarg:
raise TypeError(
"%s() got some positional-only arguments passed as keyword "
"arguments: %r" % (f_name, ', '.join(posonly_passed_as_kwarg)))
if num_pos > num_args and not varargs:
_too_many(f_name, args, kwonlyargs, varargs, num_defaults,
num_pos, arg2value)
Expand Down
16 changes: 16 additions & 0 deletions Lib/test/test_inspect/test_inspect.py
Original file line number Diff line number Diff line change
Expand Up @@ -2266,6 +2266,22 @@ def test_plain(self):
self.assertEqualCallArgs(f, '2, **collections.UserDict(b=3)')
self.assertEqualCallArgs(f, 'b=2, **collections.UserDict(a=3)')

def test_positional_only(self):
# gh-107831: positional-only parameters cannot be filled by keyword,
# so getcallargs must match a real call (raise rather than bind).
f = self.makeCallable('a, b, /, c, d=1, *, e')
self.assertEqualCallArgs(f, '1, 2, 3, e=4')
self.assertEqualCallArgs(f, '1, 2, c=3, e=4')
self.assertEqualCallArgs(f, '1, 2, 3, 4, e=5')
self.assertEqualException(f, 'a=1, b=2, c=3, e=4')
self.assertEqualException(f, '1, b=2, c=3, e=4')
# With **kwargs the positional-only name is absorbed into it and the
# parameter itself is left unfilled (a missing-argument error).
g = self.makeCallable('a, /, **kw')
self.assertEqualCallArgs(g, '1')
self.assertEqualCallArgs(g, '1, x=2')
self.assertEqualException(g, 'a=1')

def test_varargs(self):
f = self.makeCallable('a, b=1, *c')
self.assertEqualCallArgs(f, '2')
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
:func:`inspect.getcallargs` now raises :exc:`TypeError` when a positional-only
parameter is passed by keyword, consistent with calling the function directly
and with :meth:`inspect.Signature.bind`.
Loading