diff options
author | Yury Selivanov <yselivanov@sprymix.com> | 2014-04-08 15:28:02 (GMT) |
---|---|---|
committer | Yury Selivanov <yselivanov@sprymix.com> | 2014-04-08 15:28:02 (GMT) |
commit | 0fceaf45e2f6695685785e18852902740210a128 (patch) | |
tree | 091cf01bb32ba468714abd33b2415ecadbb9a198 /Lib/inspect.py | |
parent | 7ddf3eba90576f51c14b8da0df2970589761b78e (diff) | |
download | cpython-0fceaf45e2f6695685785e18852902740210a128.zip cpython-0fceaf45e2f6695685785e18852902740210a128.tar.gz cpython-0fceaf45e2f6695685785e18852902740210a128.tar.bz2 |
inspect.signautre: Fix functools.partial support. Issue #21117
Diffstat (limited to 'Lib/inspect.py')
-rw-r--r-- | Lib/inspect.py | 135 |
1 files changed, 62 insertions, 73 deletions
diff --git a/Lib/inspect.py b/Lib/inspect.py index 9f9a600..4c3e33d 100644 --- a/Lib/inspect.py +++ b/Lib/inspect.py @@ -1511,7 +1511,8 @@ def _signature_get_partial(wrapped_sig, partial, extra_args=()): # look like after applying a 'functools.partial' object (or alike) # on it. - new_params = OrderedDict(wrapped_sig.parameters.items()) + old_params = wrapped_sig.parameters + new_params = OrderedDict(old_params.items()) partial_args = partial.args or () partial_keywords = partial.keywords or {} @@ -1525,32 +1526,57 @@ def _signature_get_partial(wrapped_sig, partial, extra_args=()): msg = 'partial object {!r} has incorrect arguments'.format(partial) raise ValueError(msg) from ex - for arg_name, arg_value in ba.arguments.items(): - param = new_params[arg_name] - if arg_name in partial_keywords: - # We set a new default value, because the following code - # is correct: - # - # >>> def foo(a): print(a) - # >>> print(partial(partial(foo, a=10), a=20)()) - # 20 - # >>> print(partial(partial(foo, a=10), a=20)(a=30)) - # 30 - # - # So, with 'partial' objects, passing a keyword argument is - # like setting a new default value for the corresponding - # parameter - # - # We also mark this parameter with '_partial_kwarg' - # flag. Later, in '_bind', the 'default' value of this - # parameter will be added to 'kwargs', to simulate - # the 'functools.partial' real call. - new_params[arg_name] = param.replace(default=arg_value, - _partial_kwarg=True) - - elif (param.kind not in (_VAR_KEYWORD, _VAR_POSITIONAL) and - not param._partial_kwarg): - new_params.pop(arg_name) + + transform_to_kwonly = False + for param_name, param in old_params.items(): + try: + arg_value = ba.arguments[param_name] + except KeyError: + pass + else: + if param.kind is _POSITIONAL_ONLY: + # If positional-only parameter is bound by partial, + # it effectively disappears from the signature + new_params.pop(param_name) + continue + + if param.kind is _POSITIONAL_OR_KEYWORD: + if param_name in partial_keywords: + # This means that this parameter, and all parameters + # after it should be keyword-only (and var-positional + # should be removed). Here's why. Consider the following + # function: + # foo(a, b, *args, c): + # pass + # + # "partial(foo, a='spam')" will have the following + # signature: "(*, a='spam', b, c)". Because attempting + # to call that partial with "(10, 20)" arguments will + # raise a TypeError, saying that "a" argument received + # multiple values. + transform_to_kwonly = True + # Set the new default value + new_params[param_name] = param.replace(default=arg_value) + else: + # was passed as a positional argument + new_params.pop(param.name) + continue + + if param.kind is _KEYWORD_ONLY: + # Set the new default value + new_params[param_name] = param.replace(default=arg_value) + + if transform_to_kwonly: + assert param.kind is not _POSITIONAL_ONLY + + if param.kind is _POSITIONAL_OR_KEYWORD: + new_param = new_params[param_name].replace(kind=_KEYWORD_ONLY) + new_params[param_name] = new_param + new_params.move_to_end(param_name) + elif param.kind in (_KEYWORD_ONLY, _VAR_KEYWORD): + new_params.move_to_end(param_name) + elif param.kind is _VAR_POSITIONAL: + new_params.pop(param.name) return wrapped_sig.replace(parameters=new_params.values()) @@ -2069,7 +2095,7 @@ class Parameter: `Parameter.KEYWORD_ONLY`, `Parameter.VAR_KEYWORD`. ''' - __slots__ = ('_name', '_kind', '_default', '_annotation', '_partial_kwarg') + __slots__ = ('_name', '_kind', '_default', '_annotation') POSITIONAL_ONLY = _POSITIONAL_ONLY POSITIONAL_OR_KEYWORD = _POSITIONAL_OR_KEYWORD @@ -2079,8 +2105,7 @@ class Parameter: empty = _empty - def __init__(self, name, kind, *, default=_empty, annotation=_empty, - _partial_kwarg=False): + def __init__(self, name, kind, *, default=_empty, annotation=_empty): if kind not in (_POSITIONAL_ONLY, _POSITIONAL_OR_KEYWORD, _VAR_POSITIONAL, _KEYWORD_ONLY, _VAR_KEYWORD): @@ -2105,8 +2130,6 @@ class Parameter: self._name = name - self._partial_kwarg = _partial_kwarg - @property def name(self): return self._name @@ -2123,8 +2146,8 @@ class Parameter: def kind(self): return self._kind - def replace(self, *, name=_void, kind=_void, annotation=_void, - default=_void, _partial_kwarg=_void): + def replace(self, *, name=_void, kind=_void, + annotation=_void, default=_void): '''Creates a customized copy of the Parameter.''' if name is _void: @@ -2139,11 +2162,7 @@ class Parameter: if default is _void: default = self._default - if _partial_kwarg is _void: - _partial_kwarg = self._partial_kwarg - - return type(self)(name, kind, default=default, annotation=annotation, - _partial_kwarg=_partial_kwarg) + return type(self)(name, kind, default=default, annotation=annotation) def __str__(self): kind = self.kind @@ -2169,17 +2188,6 @@ class Parameter: id(self), self.name) def __eq__(self, other): - # NB: We deliberately do not compare '_partial_kwarg' attributes - # here. Imagine we have a following situation: - # - # def foo(a, b=1): pass - # def bar(a, b): pass - # bar2 = functools.partial(bar, b=1) - # - # For the above scenario, signatures for `foo` and `bar2` should - # be equal. '_partial_kwarg' attribute is an internal flag, to - # distinguish between keyword parameters with defaults and - # keyword parameters which got their defaults from functools.partial return (issubclass(other.__class__, Parameter) and self._name == other._name and self._kind == other._kind and @@ -2219,12 +2227,7 @@ class BoundArguments: def args(self): args = [] for param_name, param in self._signature.parameters.items(): - if (param.kind in (_VAR_KEYWORD, _KEYWORD_ONLY) or - param._partial_kwarg): - # Keyword arguments mapped by 'functools.partial' - # (Parameter._partial_kwarg is True) are mapped - # in 'BoundArguments.kwargs', along with VAR_KEYWORD & - # KEYWORD_ONLY + if param.kind in (_VAR_KEYWORD, _KEYWORD_ONLY): break try: @@ -2249,8 +2252,7 @@ class BoundArguments: kwargs_started = False for param_name, param in self._signature.parameters.items(): if not kwargs_started: - if (param.kind in (_VAR_KEYWORD, _KEYWORD_ONLY) or - param._partial_kwarg): + if param.kind in (_VAR_KEYWORD, _KEYWORD_ONLY): kwargs_started = True else: if param_name not in self.arguments: @@ -2332,18 +2334,14 @@ class Signature: name = param.name if kind < top_kind: - msg = 'wrong parameter order: {} before {}' + msg = 'wrong parameter order: {!r} before {!r}' msg = msg.format(top_kind, kind) raise ValueError(msg) elif kind > top_kind: kind_defaults = False top_kind = kind - if (kind in (_POSITIONAL_ONLY, _POSITIONAL_OR_KEYWORD) and - not param._partial_kwarg): - # If we have a positional-only or positional-or-keyword - # parameter, that does not have its default value set - # by 'functools.partial' or other "partial" signature: + if kind in (_POSITIONAL_ONLY, _POSITIONAL_OR_KEYWORD): if param.default is _empty: if kind_defaults: # No default for this parameter, but the @@ -2518,15 +2516,6 @@ class Signature: parameters_ex = () arg_vals = iter(args) - if partial: - # Support for binding arguments to 'functools.partial' objects. - # See 'functools.partial' case in 'signature()' implementation - # for details. - for param_name, param in self.parameters.items(): - if (param._partial_kwarg and param_name not in kwargs): - # Simulating 'functools.partial' behavior - kwargs[param_name] = param.default - while True: # Let's iterate through the positional arguments and corresponding # parameters |