kwargs inside the decorator are wrong
mxposed opened this issue · 7 comments
Hello,
I ran into a issue when I try to assess certain kwargs inside the decorator. In my situation I had one kwarg passed to the function, but it was inside args
inside the decorator. Am I doing something wrong or is this a bug or is this a feature?
The test below fails for me and prints args (True,) kwargs {}
def test_decorator_kwargs():
@decorator.decorator
def loader(func, *args, **kwargs):
print(f"args {args} kwargs {kwargs}")
return kwargs
@loader
def my_loader(test=False):
return test
kwargs = my_loader(test=True)
assert "test" in kwargs
assert kwargs["test"]
kwargs = my_loader(test=False)
assert "test" in kwargs
assert not kwargs["test"]
I'm using decorator 4.4.2 on python 3.7.1
hah, I'm having the opposite problem with #104
I think this behavior is correct in 4.4.2.
in your example func(test=False)
test
is a named positional argument, not a keyword argument.
I totally understand that those two things SHOULD be the same thing, but python used to (?) treat them differently.
nuts, but I think decorator v4 more strictly adhered to python's implementation (for better or worse)
If the decorated function uses a variable as a named positional variable, it isn't included in the kwargs dict.
def test_behavior(*args, **kwargs):
print(f"args: {args} \n kwargs:{kwargs}")
def test_behavior2(required, optional=None, **kwargs):
print(f"req: {required} \noptional={optional} \nkwargs:{kwargs}")
test_behavior()
args: ()
kwargs:{}
test_behavior("hi")
args: ('hi',)
kwargs:{}
test_behavior(named="hi")
args: ()
kwargs:{'named': 'hi'}
test_behavior2("hi")
req: hi
optional=None
kwargs:{}
test_behavior2("hi", "world")
req: hi
optional=world
kwargs:{}
test_behavior2("hi", optional="world" )
req: hi
optional=world
kwargs:{}
test_behavior2("hi", optional="world", something_else="weird")
req: hi
optional=world
kwargs:{'something_else': 'weird'}
@bauman you may be right in how python names them, but I'm not sure I agree semantically.
Consider this:
def test(x=1, y=2):
print(f"x={x}, y={y}")
While it is true that I cannot do test(10, x=20)
but I can do test(y=10, x=20)
.
Anyway, my usecase is this: the decorator needs to check an environment variable and already supplied named arguments, and if either of them has indicated that this is a test, then set the test
named argument if the underlying function has that named argument. Current behaviour makes it quite hard to check if the supplied named arguments had test
already, but maybe I just am doing it wrong. I suppose I can look at the inspection of the underlying function, get the position number of the named argument and then ask the args
for that value
I completely agree with you @mxposed
I don't think that the problem we're talking about is this maintainers problem though.
Python itself invented these three different types of arguments where positional and named positional are loosely used in the same args
and this other thing that semantically is a named argument is something different in a keyword arg to python.
perhaps we in the python community have been abusing this and should have chosen between positional arguments and keyword arguments and left the named positional defaultable arguments alone.
v5 is a move into what makes semantic sense, but departs from how python itself behaves.
If I had to pick common sense or "whatever the default behavior of python is" I suppose I'd still want to match python and take up my complaint in a PEP
I knew this was going to bite me. See my last comment in #39 and the discussion in #104. @mxposed should be happy with the behavior of version 5 of the decorator module. At the moment I am not sure if the behavior in v5 will stay as it is or will revert back to the semantic of v4. The plan is to wait a few days and see how it goes.
@mxposed
The behavior that you want is now available in decorator 5.0.5:
from decorator import decorator
def loader(func, *args, **kwargs):
print(f"{args=} {kwargs=}")
return kwargs
@decorator(loader, kwsyntax=True)
def my_loader(test=False):
return test
my_loader(test=True) # args=() kwargs={'test': True}
my_loader(test=False) # args=() kwargs={'test': False}
Notice that you can get what you want also with the old decorator module, but you need to use kw-only-arguments, i.e.
write my_loader as follows:
@loader
def my_loader(*, test=False):
return test
Happy Easter!
Thank you! Happy Easter!