You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

162 lines
6.4 KiB

import itertools
from functools import partial
from inspect import BoundArguments
from inspect import Parameter
from inspect import Signature
from typing import Any
from typing import Callable
class SignatureAdapter(Signature):
method: Callable[..., Any]
@classmethod
def wrap(cls, method):
"""Build a wrapper that adapts the received arguments to the inner ``method`` signature"""
sig = cls.from_callable(method)
sig.method = method
sig.__name__ = (
method.func.__name__ if isinstance(method, partial) else method.__name__
)
return sig
def __call__(self, *args: Any, **kwargs: Any) -> Any:
ba = self.bind_expected(*args, **kwargs)
return self.method(*ba.args, **ba.kwargs)
@classmethod
def from_callable(cls, method):
if hasattr(method, "__signature__"):
sig = method.__signature__
return SignatureAdapter(
sig.parameters.values(),
return_annotation=sig.return_annotation,
)
return super().from_callable(method)
def bind_expected(self, *args: Any, **kwargs: Any) -> BoundArguments: # noqa: C901
"""Get a BoundArguments object, that maps the passed `args`
and `kwargs` to the function's signature. It avoids to raise `TypeError`
trying to fill all the required arguments and ignoring the unknown ones.
Adapted from the internal `inspect.Signature._bind`.
"""
arguments = {}
parameters = iter(self.parameters.values())
arg_vals = iter(args)
parameters_ex: Any = ()
kwargs_param = None
while True:
# Let's iterate through the positional arguments and corresponding
# parameters
try:
arg_val = next(arg_vals)
except StopIteration:
# No more positional arguments
try:
param = next(parameters)
except StopIteration:
# No more parameters. That's it. Just need to check that
# we have no `kwargs` after this while loop
break
else:
if param.kind == Parameter.VAR_POSITIONAL:
# That's OK, just empty *args. Let's start parsing
# kwargs
break
elif param.name in kwargs:
if param.kind == Parameter.POSITIONAL_ONLY:
msg = (
"{arg!r} parameter is positional only, "
"but was passed as a keyword"
)
msg = msg.format(arg=param.name)
raise TypeError(msg) from None
parameters_ex = (param,)
break
elif (
param.kind == Parameter.VAR_KEYWORD
or param.default is not Parameter.empty
):
# That's fine too - we have a default value for this
# parameter. So, lets start parsing `kwargs`, starting
# with the current parameter
parameters_ex = (param,)
break
else:
# No default, not VAR_KEYWORD, not VAR_POSITIONAL,
# not in `kwargs`
parameters_ex = (param,)
break
else:
# We have a positional argument to process
try:
param = next(parameters)
except StopIteration:
# raise TypeError('too many positional arguments') from None
break
else:
if param.kind == Parameter.VAR_KEYWORD:
# Memorize that we have a '**kwargs'-like parameter
kwargs_param = param
break
if param.kind == Parameter.KEYWORD_ONLY:
# Looks like we have no parameter for this positional
# argument
# 'too many positional arguments' forgiven
break
if param.kind == Parameter.VAR_POSITIONAL:
# We have an '*args'-like argument, let's fill it with
# all positional arguments we have left and move on to
# the next phase
values = [arg_val]
values.extend(arg_vals)
arguments[param.name] = tuple(values)
break
if param.name in kwargs and param.kind != Parameter.POSITIONAL_ONLY:
arguments[param.name] = kwargs.pop(param.name)
else:
arguments[param.name] = arg_val
# Now, we iterate through the remaining parameters to process
# keyword arguments
for param in itertools.chain(parameters_ex, parameters):
if param.kind == Parameter.VAR_KEYWORD:
# Memorize that we have a '**kwargs'-like parameter
kwargs_param = param
continue
if param.kind == Parameter.VAR_POSITIONAL:
# Named arguments don't refer to '*args'-like parameters.
# We only arrive here if the positional arguments ended
# before reaching the last parameter before *args.
continue
param_name = param.name
try:
arg_val = kwargs.pop(param_name)
except KeyError:
# We have no value for this parameter. It's fine though,
# if it has a default value, or it is an '*args'-like
# parameter, left alone by the processing of positional
# arguments.
pass
else:
arguments[param_name] = arg_val #
if kwargs:
if kwargs_param is not None:
# Process our '**kwargs'-like parameter
arguments[kwargs_param.name] = kwargs # type: ignore [assignment]
else:
# 'ignoring we got an unexpected keyword argument'
pass
return BoundArguments(self, arguments) # type: ignore [arg-type]