Skip to content

Commit 3f73ca2

Browse files
committed
inspect.signautre: Fix functools.partial support. Issue #21117
1 parent 1d1d95b commit 3f73ca2

File tree

3 files changed

+147
-98
lines changed

3 files changed

+147
-98
lines changed

Lib/inspect.py

+63-76
Original file line numberDiff line numberDiff line change
@@ -1518,7 +1518,8 @@ def _signature_get_partial(wrapped_sig, partial, extra_args=()):
15181518
on it.
15191519
"""
15201520

1521-
new_params = OrderedDict(wrapped_sig.parameters.items())
1521+
old_params = wrapped_sig.parameters
1522+
new_params = OrderedDict(old_params.items())
15221523

15231524
partial_args = partial.args or ()
15241525
partial_keywords = partial.keywords or {}
@@ -1532,32 +1533,57 @@ def _signature_get_partial(wrapped_sig, partial, extra_args=()):
15321533
msg = 'partial object {!r} has incorrect arguments'.format(partial)
15331534
raise ValueError(msg) from ex
15341535

1535-
for arg_name, arg_value in ba.arguments.items():
1536-
param = new_params[arg_name]
1537-
if arg_name in partial_keywords:
1538-
# We set a new default value, because the following code
1539-
# is correct:
1540-
#
1541-
# >>> def foo(a): print(a)
1542-
# >>> print(partial(partial(foo, a=10), a=20)())
1543-
# 20
1544-
# >>> print(partial(partial(foo, a=10), a=20)(a=30))
1545-
# 30
1546-
#
1547-
# So, with 'partial' objects, passing a keyword argument is
1548-
# like setting a new default value for the corresponding
1549-
# parameter
1550-
#
1551-
# We also mark this parameter with '_partial_kwarg'
1552-
# flag. Later, in '_bind', the 'default' value of this
1553-
# parameter will be added to 'kwargs', to simulate
1554-
# the 'functools.partial' real call.
1555-
new_params[arg_name] = param.replace(default=arg_value,
1556-
_partial_kwarg=True)
1557-
1558-
elif (param.kind not in (_VAR_KEYWORD, _VAR_POSITIONAL) and
1559-
not param._partial_kwarg):
1560-
new_params.pop(arg_name)
1536+
1537+
transform_to_kwonly = False
1538+
for param_name, param in old_params.items():
1539+
try:
1540+
arg_value = ba.arguments[param_name]
1541+
except KeyError:
1542+
pass
1543+
else:
1544+
if param.kind is _POSITIONAL_ONLY:
1545+
# If positional-only parameter is bound by partial,
1546+
# it effectively disappears from the signature
1547+
new_params.pop(param_name)
1548+
continue
1549+
1550+
if param.kind is _POSITIONAL_OR_KEYWORD:
1551+
if param_name in partial_keywords:
1552+
# This means that this parameter, and all parameters
1553+
# after it should be keyword-only (and var-positional
1554+
# should be removed). Here's why. Consider the following
1555+
# function:
1556+
# foo(a, b, *args, c):
1557+
# pass
1558+
#
1559+
# "partial(foo, a='spam')" will have the following
1560+
# signature: "(*, a='spam', b, c)". Because attempting
1561+
# to call that partial with "(10, 20)" arguments will
1562+
# raise a TypeError, saying that "a" argument received
1563+
# multiple values.
1564+
transform_to_kwonly = True
1565+
# Set the new default value
1566+
new_params[param_name] = param.replace(default=arg_value)
1567+
else:
1568+
# was passed as a positional argument
1569+
new_params.pop(param.name)
1570+
continue
1571+
1572+
if param.kind is _KEYWORD_ONLY:
1573+
# Set the new default value
1574+
new_params[param_name] = param.replace(default=arg_value)
1575+
1576+
if transform_to_kwonly:
1577+
assert param.kind is not _POSITIONAL_ONLY
1578+
1579+
if param.kind is _POSITIONAL_OR_KEYWORD:
1580+
new_param = new_params[param_name].replace(kind=_KEYWORD_ONLY)
1581+
new_params[param_name] = new_param
1582+
new_params.move_to_end(param_name)
1583+
elif param.kind in (_KEYWORD_ONLY, _VAR_KEYWORD):
1584+
new_params.move_to_end(param_name)
1585+
elif param.kind is _VAR_POSITIONAL:
1586+
new_params.pop(param.name)
15611587

15621588
return wrapped_sig.replace(parameters=new_params.values())
15631589

@@ -2103,7 +2129,7 @@ class Parameter:
21032129
`Parameter.KEYWORD_ONLY`, `Parameter.VAR_KEYWORD`.
21042130
"""
21052131

2106-
__slots__ = ('_name', '_kind', '_default', '_annotation', '_partial_kwarg')
2132+
__slots__ = ('_name', '_kind', '_default', '_annotation')
21072133

21082134
POSITIONAL_ONLY = _POSITIONAL_ONLY
21092135
POSITIONAL_OR_KEYWORD = _POSITIONAL_OR_KEYWORD
@@ -2113,8 +2139,7 @@ class Parameter:
21132139

21142140
empty = _empty
21152141

2116-
def __init__(self, name, kind, *, default=_empty, annotation=_empty,
2117-
_partial_kwarg=False):
2142+
def __init__(self, name, kind, *, default=_empty, annotation=_empty):
21182143

21192144
if kind not in (_POSITIONAL_ONLY, _POSITIONAL_OR_KEYWORD,
21202145
_VAR_POSITIONAL, _KEYWORD_ONLY, _VAR_KEYWORD):
@@ -2139,17 +2164,13 @@ def __init__(self, name, kind, *, default=_empty, annotation=_empty,
21392164

21402165
self._name = name
21412166

2142-
self._partial_kwarg = _partial_kwarg
2143-
21442167
def __reduce__(self):
21452168
return (type(self),
21462169
(self._name, self._kind),
2147-
{'_partial_kwarg': self._partial_kwarg,
2148-
'_default': self._default,
2170+
{'_default': self._default,
21492171
'_annotation': self._annotation})
21502172

21512173
def __setstate__(self, state):
2152-
self._partial_kwarg = state['_partial_kwarg']
21532174
self._default = state['_default']
21542175
self._annotation = state['_annotation']
21552176

@@ -2169,8 +2190,8 @@ def annotation(self):
21692190
def kind(self):
21702191
return self._kind
21712192

2172-
def replace(self, *, name=_void, kind=_void, annotation=_void,
2173-
default=_void, _partial_kwarg=_void):
2193+
def replace(self, *, name=_void, kind=_void,
2194+
annotation=_void, default=_void):
21742195
"""Creates a customized copy of the Parameter."""
21752196

21762197
if name is _void:
@@ -2185,11 +2206,7 @@ def replace(self, *, name=_void, kind=_void, annotation=_void,
21852206
if default is _void:
21862207
default = self._default
21872208

2188-
if _partial_kwarg is _void:
2189-
_partial_kwarg = self._partial_kwarg
2190-
2191-
return type(self)(name, kind, default=default, annotation=annotation,
2192-
_partial_kwarg=_partial_kwarg)
2209+
return type(self)(name, kind, default=default, annotation=annotation)
21932210

21942211
def __str__(self):
21952212
kind = self.kind
@@ -2215,17 +2232,6 @@ def __repr__(self):
22152232
id(self), self)
22162233

22172234
def __eq__(self, other):
2218-
# NB: We deliberately do not compare '_partial_kwarg' attributes
2219-
# here. Imagine we have a following situation:
2220-
#
2221-
# def foo(a, b=1): pass
2222-
# def bar(a, b): pass
2223-
# bar2 = functools.partial(bar, b=1)
2224-
#
2225-
# For the above scenario, signatures for `foo` and `bar2` should
2226-
# be equal. '_partial_kwarg' attribute is an internal flag, to
2227-
# distinguish between keyword parameters with defaults and
2228-
# keyword parameters which got their defaults from functools.partial
22292235
return (issubclass(other.__class__, Parameter) and
22302236
self._name == other._name and
22312237
self._kind == other._kind and
@@ -2265,12 +2271,7 @@ def signature(self):
22652271
def args(self):
22662272
args = []
22672273
for param_name, param in self._signature.parameters.items():
2268-
if (param.kind in (_VAR_KEYWORD, _KEYWORD_ONLY) or
2269-
param._partial_kwarg):
2270-
# Keyword arguments mapped by 'functools.partial'
2271-
# (Parameter._partial_kwarg is True) are mapped
2272-
# in 'BoundArguments.kwargs', along with VAR_KEYWORD &
2273-
# KEYWORD_ONLY
2274+
if param.kind in (_VAR_KEYWORD, _KEYWORD_ONLY):
22742275
break
22752276

22762277
try:
@@ -2295,8 +2296,7 @@ def kwargs(self):
22952296
kwargs_started = False
22962297
for param_name, param in self._signature.parameters.items():
22972298
if not kwargs_started:
2298-
if (param.kind in (_VAR_KEYWORD, _KEYWORD_ONLY) or
2299-
param._partial_kwarg):
2299+
if param.kind in (_VAR_KEYWORD, _KEYWORD_ONLY):
23002300
kwargs_started = True
23012301
else:
23022302
if param_name not in self.arguments:
@@ -2378,18 +2378,14 @@ def __init__(self, parameters=None, *, return_annotation=_empty,
23782378
name = param.name
23792379

23802380
if kind < top_kind:
2381-
msg = 'wrong parameter order: {} before {}'
2381+
msg = 'wrong parameter order: {!r} before {!r}'
23822382
msg = msg.format(top_kind, kind)
23832383
raise ValueError(msg)
23842384
elif kind > top_kind:
23852385
kind_defaults = False
23862386
top_kind = kind
23872387

2388-
if (kind in (_POSITIONAL_ONLY, _POSITIONAL_OR_KEYWORD) and
2389-
not param._partial_kwarg):
2390-
# If we have a positional-only or positional-or-keyword
2391-
# parameter, that does not have its default value set
2392-
# by 'functools.partial' or other "partial" signature:
2388+
if kind in (_POSITIONAL_ONLY, _POSITIONAL_OR_KEYWORD):
23932389
if param.default is _empty:
23942390
if kind_defaults:
23952391
# No default for this parameter, but the
@@ -2570,15 +2566,6 @@ def _bind(self, args, kwargs, *, partial=False):
25702566
parameters_ex = ()
25712567
arg_vals = iter(args)
25722568

2573-
if partial:
2574-
# Support for binding arguments to 'functools.partial' objects.
2575-
# See 'functools.partial' case in 'signature()' implementation
2576-
# for details.
2577-
for param_name, param in self.parameters.items():
2578-
if (param._partial_kwarg and param_name not in kwargs):
2579-
# Simulating 'functools.partial' behavior
2580-
kwargs[param_name] = param.default
2581-
25822569
while True:
25832570
# Let's iterate through the positional arguments and corresponding
25842571
# parameters

0 commit comments

Comments
 (0)