-
Notifications
You must be signed in to change notification settings - Fork 24
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
TypeError: Test.__call__() got an unexpected keyword argument 'a'
#138
Comments
Interestingly, this does not seem to be working either, failing with the same message: from rich.traceback import install
install(show_locals=True)
from plum import dispatch
class Test:
@dispatch
def __call__(self, **kwargs):
pass
@dispatch
def __call__(self):
pass
test = Test()
test(a=1) |
Hey @sylvorg, what's going on is the following: Firstly, it is important to know that Plum ignores keyword arguments in determining whether functions are the same or not: from plum import dispatch
@dispatch
def f(x: int):
return 1
@dispatch
def f(x: int, **kw_args): # Overwrites the above, because keyword arguments are ignored in registering functions!
return 2
@dispatch
def f(x: int, *, a=1, **kw_args): # Overwrites above both two!
return 3
assert f(1) == 3
@dispatch
def f(x: int, *, b=1): # Overwrites all above methods! Now only keyword `b` is available!
return 4
f(1, a=1) # TypeError: f() got an unexpected keyword argument 'a' However, Plum does not ignore from plum import dispatch
@dispatch
def f(x: int):
return 1
@dispatch
def f(x: int, *args):
return 2
assert f(1) == 1
assert f(1, 1) == 2 Therefore, in your case, the following is happening: class Test:
@dispatch
def __call__(self):
pass
@dispatch
def __call__(self, *args, **kwargs): # New definition for `__call__`!
pass When you call In the other two cases, the second I hope that makes sense! |
Hmm... Would it be particularly difficult implementing support for keyword arguments? I would have thought that the keyword name makes the resolution easier, and any keyword names and their values not matching the ones already in the resolved functions will either go to a function with |
@sylvorg We've had a fair bit of discussion around keyword arguments. Very long story short is that supporting keyword arguments isn't entirely straightforward. Plum's design basically mimics how multiple dispatch in the Julia programming works, including the treatment of keyword arguments. |
Hmm... Does this mean none of my plum methods can use keywords arguments? At all? As they would always be overridden? |
@sylvorg Definitely not! It just means that which methods are considered the same (i.e. what would be a redefinition) isn't influenced by keyword arguments. For example, this should work fine: from plum import dispatch
@dispatch
def f(x: int, *, option1 = None):
return x
@dispatch
def f(x: str, *, option2 = None):
return x
Keyword arguments without @dispatch
def f(x: int, y: str="hey"):
... # Implementation is converted to @dispatch
def f(x: int):
y = "hey"
... # Implementation
@dispatch
def f(x: int, y: str):
... # Implementation |
Ah; so the names are matched in the case of keyword arguments, not the types. Got it. I just can't accept variable keyword arguments in a plum method, right? Those would be overridden, so just one of them or the last one would work. |
Actually, could you help me create a patch for my own version of plum that would redirect any unknown keyword arguments to a function with |
@sylvorg You should be able to use variable keyword arguments. Let me try to put together an example of what might be the desired behaviour. I’ll do that a little later |
Since keyword arguments are ignored in the dispatch, you won't be able to create versions of methods for specific keyword arguments. However, you could emulate this behaviour in a way like this: from plum import dispatch
@dispatch
def f(x, **kw_args):
if kw_args:
return f.invoke(object, object)(x, **kw_args)
print("Only one argument and no keywords!")
@dispatch
def f(*args, **kw_args):
print("Fallback:", args, kw_args)
I think this might be considerably hard to get right. If I simple solution like the above suffices, my advice would be to go with that. |
So basically I should just make sure that, if I use |
@sylvorg, hmm, if you truly want to achieve dispatch based on which keyword arguments are given, you will unfortunately need to emulate that behaviour. Perhaps another approach is possible, which has been proposed in other issues: def f(x, *, option=None):
return _f(x, option)
@dispatch
def _f(x, option: None):
...
@dispatch
def _f(x, option: str):
... The idea is that you convert keyword arguments into positional arguments in a fixed order by using a wrapper function, and then use Plum's dispatch for positional arguments. Could something like that be OK for your use case? |
Hello, and sorry for the extreme delay; I'm taking two summer courses right now! 😅 In trying to create my own version of def as_decorator():
return True
class NO_CONSTRAINT:
pass
class Function:
def __init__(self, func):
func.__signature__ = normalize_signature(signature(func))
self.__paramessages__ = []
self.__positional__ = dict()
self.__keywords__ = dict()
self.__var_positional__ = False
self.__var_keyword__ = False
for name, param in func.__signature__.parameters.items():
if param.kind is Parameter.VAR_POSITIONAL:
self.__var_positional__ = True
if param.kind is Parameter.VAR_KEYWORD:
self.__var_keyword__ = True
if param.kind in (
Parameter.POSITIONAL_ONLY,
Parameter.POSITIONAL_OR_KEYWORD,
):
self.__positional__[name] = param
if param.kind in (
Parameter.KEYWORD_ONLY,
Parameter.POSITIONAL_OR_KEYWORD,
):
self.__keywords__[name] = param
if param.default is Parameter.empty:
self.__paramessages__.append(f"{name}: {param.annotation.__name__}")
else:
self.__paramessages__.append(
f"{name}: {param.annotation.__name__} = {param.default}"
)
self.__constraint__ = NO_CONSTRAINT
self.__func__ = func
def __call__(self, *args, **kwargs):
return self.__func__(*args, **kwargs)
def __hash__(self):
return hash(self.__func__)
def __id__(self):
return id(self.__func__)
def __constrain__(self, param, arg):
if isinstance(param.annotation, typing.TypeVar):
if param.annotation.__constraints__:
for constraint in param.annotation.__constraints__:
if isinstance(arg, constraint, exact=True):
self.__constraint__ = constraint
def __score__(self, args, kwargs):
score = -1 if self.__var_positional__ or self.__var_keyword__ else 0
for index, (name, param) in enumerate(self.__positional__.items()):
try:
arg = args[index]
except IndexError:
score -= 1
else:
self.__constrain__(param, arg)
if isinstance(arg, param.annotation):
score += 1
else:
score -= 1
if len(args) > len(self.__positional__) and not self.__var_positional__:
score -= len(args) - len(self.__positional__)
for name, param in self.__keywords__.items():
if name in kwargs:
self.__constrain__(param, kwargs[name])
if isinstance(kwargs[name], param.annotation):
score += 1
else:
score -= 1
else:
score -= 1
for name in kwargs:
if name not in self.__keywords__ and not self.__var_keyword__:
score -= 1
return score
def __getattr__(self, attr):
return getattr(self.__func__, attr)
def __repr__(self):
return f"{self.__func__.__name__}({', '.join(self.__paramessages__)}) -> {self.__func__.__signature__.return_annotation.__name__}"
class Treads(list):
def __init__(self, func):
super().__init__()
self.append(Function(func))
def __call__(self, *args, **kwargs):
if as_decorator() and len(args) == 1 and isinstance(args[0], abc.Callable):
self.append(Function(args[0]))
return self
scores = defaultdict(list)
var_positional_scores = defaultdict(list)
var_keyword_scores = defaultdict(list)
var_both_scores = defaultdict(list)
var_no_scores = defaultdict(list)
for func in self:
score = func.__score__(args, kwargs)
scores[score].append(func)
if func.__var_positional__ and func.__var_keyword__:
var_both_scores[score].append(func)
elif func.__var_positional__:
var_positional_scores[score].append(func)
elif func.__var_keyword__:
var_keyword_scores[score].append(func)
elif not (func.__positional__ or func.__keywords__):
var_no_scores[score].append(func)
message_prefix = lambda funcs: f"multiple callables from {funcs} match the"
message = lambda funcs: f"{message_prefix(funcs)} arguments {args}"
# TODO: What happens with functions with:
# - no arguments
# - no keyword arguments
# - only arguments
# - only keyword arguments
# - both arguments and keyword arguments
funcs = scores[max(scores)]
if len(funcs) == 1:
pass
elif args and kwargs and var_both_scores:
funcs = var_both_scores[max(var_both_scores)]
if len(funcs) > 1:
raise TypeError(f"{message(funcs)} and keyword arguments {kwargs}")
elif args and var_positional_scores:
funcs = var_positional_scores[max(var_positional_scores)]
if len(funcs) > 1:
raise TypeError(message(funcs))
elif kwargs and var_keyword_scores:
funcs = var_keyword_scores[max(var_keyword_scores)]
if len(funcs) > 1:
raise TypeError(f"{message_prefix(funcs)} keyword arguments {kwargs}")
elif not (args or kwargs) and var_no_scores:
funcs = var_no_scores[max(var_no_scores)]
if len(funcs) > 1:
raise TypeError(f"{message_prefix(funcs)} missing arguments")
else:
raise TypeError(f"{message(funcs)} and keyword arguments {kwargs}")
func = funcs[0]
result = func(*args, **kwargs)
return_annotation = func.__signature__.return_annotation
message = (
f"callable {func} returned {result} of type {type(result)}"
f", which is not of type {return_annotation}"
)
if isinstance(return_annotation, typing.TypeVar) and not isinstance(
result, func.__constraint__
):
raise TypeError(f"{message}, specifically {func.__constraint__}")
if not isinstance(result, return_annotation):
raise TypeError(message)
return result
@Treads
def f(x: int):
return 1
@f
def f(x: int, **kw_args):
return 2
@f
def f(x: int, *, a=1, **kw_args):
return 3
@f
def f(x: int, *, b=1):
return 4
assert f(1) == 1
assert f(1, z=1) == 2
assert f(1, a=1) == 3
assert f(1, b=1) == 4
@Treads
def f(si: T) -> T:
return str(si)
try:
f(0)
except TypeError:
assert True
else:
assert False Sorry for the long code, and again for the even longer delay! |
Hey @sylvorg! Not a problem, of course. Hope the summer courses are interesting. :) Which courses are you taking? Thanks for sharing your proof of concept! That looks interesting. Could you explain the principle of what it's doing? Are you assigning some kind of score and then choosing the method with the based score? |
Thanks for asking! I'm taking Introductory Psychology and Environment and Globalization, the latter of which is kinda stressing me out with a 2000 word essay with proper references due on the 17th. 😹 Ah, well. I've got some time to complete it.
No problem, and that's exactly what I'm doing; I assume
I think that should work, though I'm still a little confused on how |
Good luck with the essay! You got this. :) Hmm, that's an interesting approach! Plum currently does not use a scoring system. The algorithm is quite simple: determine all methods which are applicable to the given arguments, and eliminate a method if its type are all strict subtypes of another method. If there is one method left, success. If there are none left, no method could be determined. If more than one is left, raise an ambiguity error. I'll have to think about your scoring system a bit. |
Hello!
With the following code:
I get the following traceback:
It doesn't seem to be happening with the following blocks either:
Thank you kindly for the help!
The text was updated successfully, but these errors were encountered: