fix(decorator): align var args + keywords behaviour with native python (#2252)

* fix(decorator): align kwargs behaviour with native python

Previously, validate_arguments would strip the value if the keyword
was the same as the target kwargs argument.

Align with Python's behaviour.

* fix: also prevent overwriting variable args

Introduces tests for both, which compare against the native python implementation

Co-authored-by: Eric Jolibois <em.jolibois@gmail.com>

* mention about 'args' in the changelog too

* simplify var_kwargs pop

Co-authored-by: Eric Jolibois <em.jolibois@gmail.com>

Co-authored-by: Eric Jolibois <em.jolibois@gmail.com>
This commit is contained in:
Anthony King
2021-02-13 10:35:21 +00:00
committed by GitHub
parent b21da6f381
commit 2a87f7954a
3 changed files with 17 additions and 9 deletions
+2
View File
@@ -0,0 +1,2 @@
Fix `validate_decorator` so `**kwargs` doesn't exclude values when the keyword
has the same name as the `*args` or `**kwargs` names.
+6 -7
View File
@@ -161,8 +161,9 @@ class ValidatedFunction:
var_kwargs = {}
wrong_positional_args = []
non_var_fields = set(self.model.__fields__) - {self.v_args_name, self.v_kwargs_name}
for k, v in kwargs.items():
if k in self.model.__fields__:
if k in non_var_fields:
if k in self.positional_only_args:
wrong_positional_args.append(k)
values[k] = v
@@ -177,9 +178,7 @@ class ValidatedFunction:
def execute(self, m: BaseModel) -> Any:
d = {k: v for k, v in m._iter() if k in m.__fields_set__}
kwargs = d.pop(self.v_kwargs_name, None)
if kwargs:
d.update(kwargs)
var_kwargs = d.pop(self.v_kwargs_name, {})
if self.v_args_name in d:
args_: List[Any] = []
@@ -193,7 +192,7 @@ class ValidatedFunction:
in_kwargs = True
else:
args_.append(value)
return self.raw_function(*args_, **kwargs)
return self.raw_function(*args_, **kwargs, **var_kwargs)
elif self.positional_only_args:
args_ = []
kwargs = {}
@@ -202,9 +201,9 @@ class ValidatedFunction:
args_.append(value)
else:
kwargs[name] = value
return self.raw_function(*args_, **kwargs)
return self.raw_function(*args_, **kwargs, **var_kwargs)
else:
return self.raw_function(**d)
return self.raw_function(**d, **var_kwargs)
def create_model(self, fields: Dict[str, Any], takes_args: bool, takes_kwargs: bool, config: 'ConfigType') -> None:
pos_args = len(self.arg_mapping)
+9 -2
View File
@@ -110,15 +110,22 @@ def test_untyped():
assert foo(1, {'x': 2}, c='3', d='4') == "1, {'x': 2}, 3, 4"
def test_var_args_kwargs():
@validate_arguments
@pytest.mark.parametrize('validated', (True, False))
def test_var_args_kwargs(validated):
def foo(a, b, *args, d=3, **kwargs):
return f'a={a!r}, b={b!r}, args={args!r}, d={d!r}, kwargs={kwargs!r}'
if validated:
foo = validate_arguments(foo)
assert foo(1, 2) == 'a=1, b=2, args=(), d=3, kwargs={}'
assert foo(1, 2, 3, d=4) == 'a=1, b=2, args=(3,), d=4, kwargs={}'
assert foo(*[1, 2, 3], d=4) == 'a=1, b=2, args=(3,), d=4, kwargs={}'
assert foo(1, 2, args=(10, 11)) == "a=1, b=2, args=(), d=3, kwargs={'args': (10, 11)}"
assert foo(1, 2, 3, args=(10, 11)) == "a=1, b=2, args=(3,), d=3, kwargs={'args': (10, 11)}"
assert foo(1, 2, 3, e=10) == "a=1, b=2, args=(3,), d=3, kwargs={'e': 10}"
assert foo(1, 2, kwargs=4) == "a=1, b=2, args=(), d=3, kwargs={'kwargs': 4}"
assert foo(1, 2, kwargs=4, e=5) == "a=1, b=2, args=(), d=3, kwargs={'kwargs': 4, 'e': 5}"
@skip_pre_38