diff --git a/changes/2251-cybojenix.md b/changes/2251-cybojenix.md new file mode 100644 index 0000000..c41d5e8 --- /dev/null +++ b/changes/2251-cybojenix.md @@ -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. \ No newline at end of file diff --git a/pydantic/decorator.py b/pydantic/decorator.py index 98bc4e2..85d04c6 100644 --- a/pydantic/decorator.py +++ b/pydantic/decorator.py @@ -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) diff --git a/tests/test_decorator.py b/tests/test_decorator.py index ed7efd3..fa668d5 100644 --- a/tests/test_decorator.py +++ b/tests/test_decorator.py @@ -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