From 5969925a46c89afde97cbe14049204110718ccca Mon Sep 17 00:00:00 2001 From: Viicos <65306057+Viicos@users.noreply.github.com> Date: Fri, 13 Sep 2024 16:10:09 +0200 Subject: [PATCH 1/6] Add support for `TypedDict` for `**kwargs` --- python/pydantic_core/core_schema.py | 44 +++++++++++++++- src/validators/mod.rs | 3 ++ src/validators/var_kwargs.rs | 80 +++++++++++++++++++++++++++++ 3 files changed, 126 insertions(+), 1 deletion(-) create mode 100644 src/validators/var_kwargs.rs diff --git a/python/pydantic_core/core_schema.py b/python/pydantic_core/core_schema.py index 8635b63a5..b52880219 100644 --- a/python/pydantic_core/core_schema.py +++ b/python/pydantic_core/core_schema.py @@ -10,7 +10,7 @@ from collections.abc import Mapping from datetime import date, datetime, time, timedelta from decimal import Decimal -from typing import TYPE_CHECKING, Any, Callable, Dict, Hashable, List, Pattern, Set, Tuple, Type, Union +from typing import TYPE_CHECKING, Any, Callable, Dict, Hashable, List, Pattern, Set, Tuple, Type, Union, overload from typing_extensions import deprecated @@ -3372,6 +3372,48 @@ def arguments_parameter( return _dict_not_none(name=name, schema=schema, mode=mode, alias=alias) +class VarKwargsSchema(TypedDict): + type: Literal['var_kwargs'] + mode: Literal['single', 'typed_dict'] + schema: CoreSchema + + +@overload +def var_kwargs_schema( + *, + mode: Literal['single'], + schema: CoreSchema, +) -> VarKwargsSchema: ... + + +@overload +def var_kwargs_schema( + *, + mode: Literal['typed_dict'], + schema: TypedDictSchema, +) -> VarKwargsSchema: ... + + +def var_kwargs_schema( + *, + mode: Literal['single', 'typed_dict'], + schema: CoreSchema, +) -> VarKwargsSchema: + """Returns a schema describing the variadic keyword arguments of a callable. + + Args: + mode: The validation mode to use. If `'single'`, every value of the keyword arguments will + be validated against the core schema from the `schema` argument. If `'typed_dict'`, the + `schema` argument must be a [`typed_dict_schema`][pydantic_core.core_schema.typed_dict_schema]. + """ + + return _dict_not_none( + type='var_kwargs', + mode=mode, + schema=schema, + ) + + class ArgumentsSchema(TypedDict, total=False): type: Required[Literal['arguments']] arguments_schema: Required[List[ArgumentsParameter]] diff --git a/src/validators/mod.rs b/src/validators/mod.rs index 5f88d5dc8..b1aa47f9d 100644 --- a/src/validators/mod.rs +++ b/src/validators/mod.rs @@ -61,6 +61,7 @@ mod union; mod url; mod uuid; mod validation_state; +mod var_kwargs; mod with_default; pub use self::validation_state::{Exactness, ValidationState}; @@ -561,6 +562,7 @@ pub fn build_validator( callable::CallableValidator, // arguments arguments::ArgumentsValidator, + var_kwargs::VarKwargsValidator, // default value with_default::WithDefaultValidator, // chain validators @@ -716,6 +718,7 @@ pub enum CombinedValidator { Callable(callable::CallableValidator), // arguments Arguments(arguments::ArgumentsValidator), + VarKwargs(var_kwargs::VarKwargsValidator), // default value WithDefault(with_default::WithDefaultValidator), // chain validators diff --git a/src/validators/var_kwargs.rs b/src/validators/var_kwargs.rs new file mode 100644 index 000000000..d97b4ef74 --- /dev/null +++ b/src/validators/var_kwargs.rs @@ -0,0 +1,80 @@ +use std::str::FromStr; + +use pyo3::intern; +use pyo3::prelude::*; +use pyo3::types::{PyDict, PyString}; + +use crate::build_tools::py_schema_err; +use crate::errors::ValResult; +use crate::input::Input; +use crate::tools::SchemaDict; + +use super::validation_state::ValidationState; +use super::{build_validator, BuildValidator, CombinedValidator, DefinitionsBuilder, Validator}; + +#[derive(Debug)] +enum VarKwargsMode { + Single, + TypedDict, +} + +impl FromStr for VarKwargsMode { + type Err = PyErr; + + fn from_str(s: &str) -> Result { + match s { + "single" => Ok(Self::Single), + "typed_dict" => Ok(Self::TypedDict), + s => py_schema_err!("Invalid var_kwargs mode: `{}`, expected `single` or `typed_dict`", s), + } + } +} + +#[derive(Debug)] +pub struct VarKwargsValidator { + mode: VarKwargsMode, + validator: Box, +} + +impl BuildValidator for VarKwargsValidator { + const EXPECTED_TYPE: &'static str = "var_kwargs"; + + fn build( + schema: &Bound<'_, PyDict>, + config: Option<&Bound<'_, PyDict>>, + definitions: &mut DefinitionsBuilder, + ) -> PyResult { + let py = schema.py(); + + let py_mode: Bound = schema.get_as_req(intern!(py, "mode"))?; + let mode = VarKwargsMode::from_str(py_mode.to_string().as_str())?; + + let validator_schema: Bound = schema.get_as_req(intern!(py, "schema"))?; + + Ok(Self { + mode, + validator: Box::new(build_validator(&validator_schema, config, definitions)?), + } + .into()) + } +} + +impl_py_gc_traverse!(VarKwargsValidator { mode, validator }); + +impl Validator for VarKwargsValidator { + fn validate<'py>( + &self, + py: Python<'py>, + input: &(impl Input<'py> + ?Sized), + state: &mut ValidationState<'_, 'py>, + ) -> ValResult { + match self.mode { + VarKwargsMode::Single => { + // TODO + } + VarKwargsMode::TypedDict => { + // TODO + } + } + } +} From 1149fe4512d682657c4beeb13129e28db5b3a22e Mon Sep 17 00:00:00 2001 From: Viicos <65306057+Viicos@users.noreply.github.com> Date: Mon, 16 Sep 2024 16:06:39 +0200 Subject: [PATCH 2/6] Flatten everything under the arguments validator --- python/pydantic_core/core_schema.py | 49 ++----------- src/validators/arguments.rs | 109 ++++++++++++++++++++++------ src/validators/mod.rs | 3 - src/validators/var_kwargs.rs | 80 -------------------- tests/validators/test_arguments.py | 57 ++++++++++++++- 5 files changed, 152 insertions(+), 146 deletions(-) delete mode 100644 src/validators/var_kwargs.rs diff --git a/python/pydantic_core/core_schema.py b/python/pydantic_core/core_schema.py index b52880219..ab15d6835 100644 --- a/python/pydantic_core/core_schema.py +++ b/python/pydantic_core/core_schema.py @@ -10,7 +10,7 @@ from collections.abc import Mapping from datetime import date, datetime, time, timedelta from decimal import Decimal -from typing import TYPE_CHECKING, Any, Callable, Dict, Hashable, List, Pattern, Set, Tuple, Type, Union, overload +from typing import TYPE_CHECKING, Any, Callable, Dict, Hashable, List, Pattern, Set, Tuple, Type, Union from typing_extensions import deprecated @@ -3372,46 +3372,7 @@ def arguments_parameter( return _dict_not_none(name=name, schema=schema, mode=mode, alias=alias) -class VarKwargsSchema(TypedDict): - type: Literal['var_kwargs'] - mode: Literal['single', 'typed_dict'] - schema: CoreSchema - - -@overload -def var_kwargs_schema( - *, - mode: Literal['single'], - schema: CoreSchema, -) -> VarKwargsSchema: ... - - -@overload -def var_kwargs_schema( - *, - mode: Literal['typed_dict'], - schema: TypedDictSchema, -) -> VarKwargsSchema: ... - - -def var_kwargs_schema( - *, - mode: Literal['single', 'typed_dict'], - schema: CoreSchema, -) -> VarKwargsSchema: - """Returns a schema describing the variadic keyword arguments of a callable. - - Args: - mode: The validation mode to use. If `'single'`, every value of the keyword arguments will - be validated against the core schema from the `schema` argument. If `'typed_dict'`, the - `schema` argument must be a [`typed_dict_schema`][pydantic_core.core_schema.typed_dict_schema]. - """ - - return _dict_not_none( - type='var_kwargs', - mode=mode, - schema=schema, - ) +VarKwargsMode: TypeAlias = Literal['single', 'unpacked-typed-dict'] class ArgumentsSchema(TypedDict, total=False): @@ -3419,6 +3380,7 @@ class ArgumentsSchema(TypedDict, total=False): arguments_schema: Required[List[ArgumentsParameter]] populate_by_name: bool var_args_schema: CoreSchema + var_kwargs_mode: VarKwargsMode var_kwargs_schema: CoreSchema ref: str metadata: Dict[str, Any] @@ -3430,6 +3392,7 @@ def arguments_schema( *, populate_by_name: bool | None = None, var_args_schema: CoreSchema | None = None, + var_kwargs_mode: VarKwargsMode | None = None, var_kwargs_schema: CoreSchema | None = None, ref: str | None = None, metadata: Dict[str, Any] | None = None, @@ -3456,6 +3419,9 @@ def arguments_schema( arguments: The arguments to use for the arguments schema populate_by_name: Whether to populate by name var_args_schema: The variable args schema to use for the arguments schema + var_kwargs_mode: The validation mode to use for variadic keyword arguments. If `'single'`, every value of the + keyword arguments will be validated against the `var_kwargs_schema` schema. If `'unpacked-typed-dict'`, + the `schema` argument must be a [`typed_dict_schema`][pydantic_core.core_schema.typed_dict_schema] var_kwargs_schema: The variable kwargs schema to use for the arguments schema ref: optional unique identifier of the schema, used to reference the schema in other places metadata: Any other information you want to include with the schema, not used by pydantic-core @@ -3466,6 +3432,7 @@ def arguments_schema( arguments_schema=arguments, populate_by_name=populate_by_name, var_args_schema=var_args_schema, + var_kwargs_mode=var_kwargs_mode, var_kwargs_schema=var_kwargs_schema, ref=ref, metadata=metadata, diff --git a/src/validators/arguments.rs b/src/validators/arguments.rs index 075bc009e..5f0fb4972 100644 --- a/src/validators/arguments.rs +++ b/src/validators/arguments.rs @@ -1,3 +1,5 @@ +use std::str::FromStr; + use pyo3::intern; use pyo3::prelude::*; use pyo3::types::{PyDict, PyList, PyString, PyTuple}; @@ -15,6 +17,27 @@ use crate::tools::SchemaDict; use super::validation_state::ValidationState; use super::{build_validator, BuildValidator, CombinedValidator, DefinitionsBuilder, Validator}; +#[derive(Debug, PartialEq)] +enum VarKwargsMode { + Single, + UnpackedTypedDict, +} + +impl FromStr for VarKwargsMode { + type Err = PyErr; + + fn from_str(s: &str) -> Result { + match s { + "single" => Ok(Self::Single), + "unpacked-typed-dict" => Ok(Self::UnpackedTypedDict), + s => py_schema_err!( + "Invalid var_kwargs mode: `{}`, expected `single` or `unpacked-typed-dict`", + s + ), + } + } +} + #[derive(Debug)] struct Parameter { positional: bool, @@ -29,6 +52,7 @@ pub struct ArgumentsValidator { parameters: Vec, positional_params_count: usize, var_args_validator: Option>, + var_kwargs_mode: VarKwargsMode, var_kwargs_validator: Option>, loc_by_alias: bool, extra: ExtraBehavior, @@ -117,6 +141,22 @@ impl BuildValidator for ArgumentsValidator { }); } + let py_var_kwargs_mode: Bound = match schema.get_as(intern!(py, "var_kwargs_mode"))? { + Some(v) => v, + None => PyString::new_bound(py, "single"), + }; + let var_kwargs_mode = VarKwargsMode::from_str(py_var_kwargs_mode.to_string().as_str())?; + let var_kwargs_validator = match schema.get_item(intern!(py, "var_kwargs_schema"))? { + Some(v) => Some(Box::new(build_validator(&v, config, definitions)?)), + None => None, + }; + + if var_kwargs_mode == VarKwargsMode::UnpackedTypedDict && var_kwargs_validator.is_none() { + return py_schema_err!( + "`var_kwargs_schema` must be specified when `var_kwargs_mode` is `'unpacked-typed-dict'`" + ); + } + Ok(Self { parameters, positional_params_count, @@ -124,10 +164,8 @@ impl BuildValidator for ArgumentsValidator { Some(v) => Some(Box::new(build_validator(&v, config, definitions)?)), None => None, }, - var_kwargs_validator: match schema.get_item(intern!(py, "var_kwargs_schema"))? { - Some(v) => Some(Box::new(build_validator(&v, config, definitions)?)), - None => None, - }, + var_kwargs_mode, + var_kwargs_validator, loc_by_alias: config.get_as(intern!(py, "loc_by_alias"))?.unwrap_or(true), extra: ExtraBehavior::from_schema_or_config(py, schema, config, ExtraBehavior::Forbid)?, } @@ -258,6 +296,8 @@ impl Validator for ArgumentsValidator { // if there are kwargs check any that haven't been processed yet if let Some(kwargs) = args.kwargs() { if kwargs.len() > used_kwargs.len() { + let remaining_kwargs = PyDict::new_bound(py); + for result in kwargs.iter() { let (raw_key, value) = result?; let either_str = match raw_key @@ -278,28 +318,55 @@ impl Validator for ArgumentsValidator { Err(err) => return Err(err), }; if !used_kwargs.contains(either_str.as_cow()?.as_ref()) { - match self.var_kwargs_validator { - Some(ref validator) => match validator.validate(py, value.borrow_input(), state) { - Ok(value) => { - output_kwargs.set_item(either_str.as_py_string(py, state.cache_str()), value)?; - } - Err(ValError::LineErrors(line_errors)) => { - for err in line_errors { - errors.push(err.with_outer_location(raw_key.clone())); + match self.var_kwargs_mode { + VarKwargsMode::Single => match self.var_kwargs_validator { + Some(ref validator) => match validator.validate(py, value.borrow_input(), state) { + Ok(value) => { + output_kwargs + .set_item(either_str.as_py_string(py, state.cache_str()), value)?; + } + Err(ValError::LineErrors(line_errors)) => { + for err in line_errors { + errors.push(err.with_outer_location(raw_key.clone())); + } + } + Err(err) => return Err(err), + }, + None => { + if let ExtraBehavior::Forbid = self.extra { + errors.push(ValLineError::new_with_loc( + ErrorTypeDefaults::UnexpectedKeywordArgument, + value, + raw_key.clone(), + )); } } - Err(err) => return Err(err), }, - None => { - if let ExtraBehavior::Forbid = self.extra { - errors.push(ValLineError::new_with_loc( - ErrorTypeDefaults::UnexpectedKeywordArgument, - value, - raw_key.clone(), - )); - } + VarKwargsMode::UnpackedTypedDict => { + // Save to the remaining kwargs, we will validate as a single dict: + remaining_kwargs.set_item(either_str.as_py_string(py, state.cache_str()), value)?; + } + } + } + } + + if self.var_kwargs_mode == VarKwargsMode::UnpackedTypedDict { + // `var_kwargs_validator` is guaranteed to be `Some`: + match self + .var_kwargs_validator + .as_ref() + .unwrap() + .validate(py, remaining_kwargs.as_any(), state) + { + Ok(value) => { + output_kwargs.update(value.downcast_bound::(py).unwrap().as_mapping())?; + } + Err(ValError::LineErrors(line_errors)) => { + for error in line_errors { + errors.push(error); } } + Err(err) => return Err(err), } } } diff --git a/src/validators/mod.rs b/src/validators/mod.rs index b1aa47f9d..5f88d5dc8 100644 --- a/src/validators/mod.rs +++ b/src/validators/mod.rs @@ -61,7 +61,6 @@ mod union; mod url; mod uuid; mod validation_state; -mod var_kwargs; mod with_default; pub use self::validation_state::{Exactness, ValidationState}; @@ -562,7 +561,6 @@ pub fn build_validator( callable::CallableValidator, // arguments arguments::ArgumentsValidator, - var_kwargs::VarKwargsValidator, // default value with_default::WithDefaultValidator, // chain validators @@ -718,7 +716,6 @@ pub enum CombinedValidator { Callable(callable::CallableValidator), // arguments Arguments(arguments::ArgumentsValidator), - VarKwargs(var_kwargs::VarKwargsValidator), // default value WithDefault(with_default::WithDefaultValidator), // chain validators diff --git a/src/validators/var_kwargs.rs b/src/validators/var_kwargs.rs deleted file mode 100644 index d97b4ef74..000000000 --- a/src/validators/var_kwargs.rs +++ /dev/null @@ -1,80 +0,0 @@ -use std::str::FromStr; - -use pyo3::intern; -use pyo3::prelude::*; -use pyo3::types::{PyDict, PyString}; - -use crate::build_tools::py_schema_err; -use crate::errors::ValResult; -use crate::input::Input; -use crate::tools::SchemaDict; - -use super::validation_state::ValidationState; -use super::{build_validator, BuildValidator, CombinedValidator, DefinitionsBuilder, Validator}; - -#[derive(Debug)] -enum VarKwargsMode { - Single, - TypedDict, -} - -impl FromStr for VarKwargsMode { - type Err = PyErr; - - fn from_str(s: &str) -> Result { - match s { - "single" => Ok(Self::Single), - "typed_dict" => Ok(Self::TypedDict), - s => py_schema_err!("Invalid var_kwargs mode: `{}`, expected `single` or `typed_dict`", s), - } - } -} - -#[derive(Debug)] -pub struct VarKwargsValidator { - mode: VarKwargsMode, - validator: Box, -} - -impl BuildValidator for VarKwargsValidator { - const EXPECTED_TYPE: &'static str = "var_kwargs"; - - fn build( - schema: &Bound<'_, PyDict>, - config: Option<&Bound<'_, PyDict>>, - definitions: &mut DefinitionsBuilder, - ) -> PyResult { - let py = schema.py(); - - let py_mode: Bound = schema.get_as_req(intern!(py, "mode"))?; - let mode = VarKwargsMode::from_str(py_mode.to_string().as_str())?; - - let validator_schema: Bound = schema.get_as_req(intern!(py, "schema"))?; - - Ok(Self { - mode, - validator: Box::new(build_validator(&validator_schema, config, definitions)?), - } - .into()) - } -} - -impl_py_gc_traverse!(VarKwargsValidator { mode, validator }); - -impl Validator for VarKwargsValidator { - fn validate<'py>( - &self, - py: Python<'py>, - input: &(impl Input<'py> + ?Sized), - state: &mut ValidationState<'_, 'py>, - ) -> ValResult { - match self.mode { - VarKwargsMode::Single => { - // TODO - } - VarKwargsMode::TypedDict => { - // TODO - } - } - } -} diff --git a/tests/validators/test_arguments.py b/tests/validators/test_arguments.py index 915f05878..a988e48a0 100644 --- a/tests/validators/test_arguments.py +++ b/tests/validators/test_arguments.py @@ -769,6 +769,19 @@ def test_build_non_default_follows(): ) +def test_build_missing_var_kwargs(): + with pytest.raises( + SchemaError, match="`var_kwargs_schema` must be specified when `var_kwargs_mode` is `'unpacked-typed-dict'`" + ): + SchemaValidator( + { + 'type': 'arguments', + 'arguments_schema': [], + 'var_kwargs_mode': 'unpacked-typed-dict', + } + ) + + @pytest.mark.parametrize( 'input_value,expected', [ @@ -778,7 +791,7 @@ def test_build_non_default_follows(): ], ids=repr, ) -def test_kwargs(py_and_json: PyAndJson, input_value, expected): +def test_kwargs_single(py_and_json: PyAndJson, input_value, expected): v = py_and_json( { 'type': 'arguments', @@ -796,6 +809,48 @@ def test_kwargs(py_and_json: PyAndJson, input_value, expected): assert v.validate_test(input_value) == expected +@pytest.mark.parametrize( + 'input_value,expected', + [ + [ArgsKwargs((), {'x': 1}), ((), {'x': 1})], + [ArgsKwargs((), {'x': 1.0}), Err('x\n Input should be a valid integer [type=int_type,')], + [ArgsKwargs((), {'x': 1, 'z': 'str'}), ((), {'x': 1, 'y': 'str'})], + [ArgsKwargs((), {'x': 1, 'y': 'str'}), Err('y\n Extra inputs are not permitted [type=extra_forbidden,')], + ], +) +def test_kwargs_typed_dict(py_and_json: PyAndJson, input_value, expected): + v = py_and_json( + { + 'type': 'arguments', + 'arguments_schema': [], + 'var_kwargs_mode': 'unpacked-typed-dict', + 'var_kwargs_schema': { + 'type': 'typed-dict', + 'fields': { + 'x': { + 'type': 'typed-dict-field', + 'schema': {'type': 'int', 'strict': True}, + 'required': True, + }, + 'y': { + 'type': 'typed-dict-field', + 'schema': {'type': 'str'}, + 'required': False, + 'validation_alias': 'z', + }, + }, + 'config': {'extra_fields_behavior': 'forbid'}, + }, + } + ) + + if isinstance(expected, Err): + with pytest.raises(ValidationError, match=re.escape(expected.message)): + v.validate_test(input_value) + else: + assert v.validate_test(input_value) == expected + + @pytest.mark.parametrize( 'input_value,expected', [ From b9dbb34f709d1487242bd2ccdc0c88b1068283b5 Mon Sep 17 00:00:00 2001 From: Viicos <65306057+Viicos@users.noreply.github.com> Date: Tue, 17 Sep 2024 20:46:15 +0200 Subject: [PATCH 3/6] Feedback Co-authored-by: Sydney Runkle <54324534+sydney-runkle@users.noreply.github.com> --- src/validators/arguments.rs | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/validators/arguments.rs b/src/validators/arguments.rs index 5f0fb4972..54acea480 100644 --- a/src/validators/arguments.rs +++ b/src/validators/arguments.rs @@ -141,11 +141,11 @@ impl BuildValidator for ArgumentsValidator { }); } - let py_var_kwargs_mode: Bound = match schema.get_as(intern!(py, "var_kwargs_mode"))? { - Some(v) => v, - None => PyString::new_bound(py, "single"), - }; - let var_kwargs_mode = VarKwargsMode::from_str(py_var_kwargs_mode.to_string().as_str())?; + let py_var_kwargs_mode: Bound = schema + .get_as(intern!(py, "var_kwargs_mode"))? + .unwrap_or_else(|| PyString::new_bound(py, "single")); + + let var_kwargs_mode = VarKwargsMode::from_str(py_var_kwargs_mode.to_str()?)?; let var_kwargs_validator = match schema.get_item(intern!(py, "var_kwargs_schema"))? { Some(v) => Some(Box::new(build_validator(&v, config, definitions)?)), None => None, From a5206e5667b708509e7dad65c088b25ddd06ef2a Mon Sep 17 00:00:00 2001 From: Viicos <65306057+Viicos@users.noreply.github.com> Date: Wed, 18 Sep 2024 14:24:14 +0200 Subject: [PATCH 4/6] Feedback Co-authored-by: David Hewitt --- python/pydantic_core/core_schema.py | 4 ++-- src/validators/arguments.rs | 14 ++++++-------- tests/validators/test_arguments.py | 2 +- 3 files changed, 9 insertions(+), 11 deletions(-) diff --git a/python/pydantic_core/core_schema.py b/python/pydantic_core/core_schema.py index ab15d6835..9ad3eaccd 100644 --- a/python/pydantic_core/core_schema.py +++ b/python/pydantic_core/core_schema.py @@ -3372,7 +3372,7 @@ def arguments_parameter( return _dict_not_none(name=name, schema=schema, mode=mode, alias=alias) -VarKwargsMode: TypeAlias = Literal['single', 'unpacked-typed-dict'] +VarKwargsMode: TypeAlias = Literal['uniform', 'unpacked-typed-dict'] class ArgumentsSchema(TypedDict, total=False): @@ -3419,7 +3419,7 @@ def arguments_schema( arguments: The arguments to use for the arguments schema populate_by_name: Whether to populate by name var_args_schema: The variable args schema to use for the arguments schema - var_kwargs_mode: The validation mode to use for variadic keyword arguments. If `'single'`, every value of the + var_kwargs_mode: The validation mode to use for variadic keyword arguments. If `'uniform'`, every value of the keyword arguments will be validated against the `var_kwargs_schema` schema. If `'unpacked-typed-dict'`, the `schema` argument must be a [`typed_dict_schema`][pydantic_core.core_schema.typed_dict_schema] var_kwargs_schema: The variable kwargs schema to use for the arguments schema diff --git a/src/validators/arguments.rs b/src/validators/arguments.rs index 54acea480..fc01ac869 100644 --- a/src/validators/arguments.rs +++ b/src/validators/arguments.rs @@ -19,7 +19,7 @@ use super::{build_validator, BuildValidator, CombinedValidator, DefinitionsBuild #[derive(Debug, PartialEq)] enum VarKwargsMode { - Single, + Uniform, UnpackedTypedDict, } @@ -28,10 +28,10 @@ impl FromStr for VarKwargsMode { fn from_str(s: &str) -> Result { match s { - "single" => Ok(Self::Single), + "uniform" => Ok(Self::Uniform), "unpacked-typed-dict" => Ok(Self::UnpackedTypedDict), s => py_schema_err!( - "Invalid var_kwargs mode: `{}`, expected `single` or `unpacked-typed-dict`", + "Invalid var_kwargs mode: `{}`, expected `uniform` or `unpacked-typed-dict`", s ), } @@ -319,8 +319,8 @@ impl Validator for ArgumentsValidator { }; if !used_kwargs.contains(either_str.as_cow()?.as_ref()) { match self.var_kwargs_mode { - VarKwargsMode::Single => match self.var_kwargs_validator { - Some(ref validator) => match validator.validate(py, value.borrow_input(), state) { + VarKwargsMode::Uniform => match &self.var_kwargs_validator { + Some(validator) => match validator.validate(py, value.borrow_input(), state) { Ok(value) => { output_kwargs .set_item(either_str.as_py_string(py, state.cache_str()), value)?; @@ -362,9 +362,7 @@ impl Validator for ArgumentsValidator { output_kwargs.update(value.downcast_bound::(py).unwrap().as_mapping())?; } Err(ValError::LineErrors(line_errors)) => { - for error in line_errors { - errors.push(error); - } + errors.extend(line_errors); } Err(err) => return Err(err), } diff --git a/tests/validators/test_arguments.py b/tests/validators/test_arguments.py index a988e48a0..1fb17dcee 100644 --- a/tests/validators/test_arguments.py +++ b/tests/validators/test_arguments.py @@ -791,7 +791,7 @@ def test_build_missing_var_kwargs(): ], ids=repr, ) -def test_kwargs_single(py_and_json: PyAndJson, input_value, expected): +def test_kwargs_uniform(py_and_json: PyAndJson, input_value, expected): v = py_and_json( { 'type': 'arguments', From 99d33033890f6b88d9a0f3e825c0f8d9309e3dad Mon Sep 17 00:00:00 2001 From: Viicos <65306057+Viicos@users.noreply.github.com> Date: Fri, 20 Sep 2024 18:03:39 +0200 Subject: [PATCH 5/6] Always validate if in unpack typed dict mode --- src/validators/arguments.rs | 39 +++++++++++++++++++------------------ 1 file changed, 20 insertions(+), 19 deletions(-) diff --git a/src/validators/arguments.rs b/src/validators/arguments.rs index fc01ac869..22f870c56 100644 --- a/src/validators/arguments.rs +++ b/src/validators/arguments.rs @@ -143,7 +143,7 @@ impl BuildValidator for ArgumentsValidator { let py_var_kwargs_mode: Bound = schema .get_as(intern!(py, "var_kwargs_mode"))? - .unwrap_or_else(|| PyString::new_bound(py, "single")); + .unwrap_or_else(|| PyString::new_bound(py, "uniform")); let var_kwargs_mode = VarKwargsMode::from_str(py_var_kwargs_mode.to_str()?)?; let var_kwargs_validator = match schema.get_item(intern!(py, "var_kwargs_schema"))? { @@ -293,11 +293,12 @@ impl Validator for ArgumentsValidator { } } } + + let remaining_kwargs = PyDict::new_bound(py); + // if there are kwargs check any that haven't been processed yet if let Some(kwargs) = args.kwargs() { if kwargs.len() > used_kwargs.len() { - let remaining_kwargs = PyDict::new_bound(py); - for result in kwargs.iter() { let (raw_key, value) = result?; let either_str = match raw_key @@ -349,24 +350,24 @@ impl Validator for ArgumentsValidator { } } } + } + } - if self.var_kwargs_mode == VarKwargsMode::UnpackedTypedDict { - // `var_kwargs_validator` is guaranteed to be `Some`: - match self - .var_kwargs_validator - .as_ref() - .unwrap() - .validate(py, remaining_kwargs.as_any(), state) - { - Ok(value) => { - output_kwargs.update(value.downcast_bound::(py).unwrap().as_mapping())?; - } - Err(ValError::LineErrors(line_errors)) => { - errors.extend(line_errors); - } - Err(err) => return Err(err), - } + if self.var_kwargs_mode == VarKwargsMode::UnpackedTypedDict { + // `var_kwargs_validator` is guaranteed to be `Some`: + match self + .var_kwargs_validator + .as_ref() + .unwrap() + .validate(py, remaining_kwargs.as_any(), state) + { + Ok(value) => { + output_kwargs.update(value.downcast_bound::(py).unwrap().as_mapping())?; + } + Err(ValError::LineErrors(line_errors)) => { + errors.extend(line_errors); } + Err(err) => return Err(err), } } From 57467bdae36e13885c5d3c00507103b2fa428fc6 Mon Sep 17 00:00:00 2001 From: Sydney Runkle <54324534+sydney-runkle@users.noreply.github.com> Date: Fri, 20 Sep 2024 11:29:57 -0500 Subject: [PATCH 6/6] Update python/pydantic_core/core_schema.py --- python/pydantic_core/core_schema.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/python/pydantic_core/core_schema.py b/python/pydantic_core/core_schema.py index 9ad3eaccd..455cd3095 100644 --- a/python/pydantic_core/core_schema.py +++ b/python/pydantic_core/core_schema.py @@ -3421,7 +3421,7 @@ def arguments_schema( var_args_schema: The variable args schema to use for the arguments schema var_kwargs_mode: The validation mode to use for variadic keyword arguments. If `'uniform'`, every value of the keyword arguments will be validated against the `var_kwargs_schema` schema. If `'unpacked-typed-dict'`, - the `schema` argument must be a [`typed_dict_schema`][pydantic_core.core_schema.typed_dict_schema] + the `var_kwargs_schema` argument must be a [`typed_dict_schema`][pydantic_core.core_schema.typed_dict_schema] var_kwargs_schema: The variable kwargs schema to use for the arguments schema ref: optional unique identifier of the schema, used to reference the schema in other places metadata: Any other information you want to include with the schema, not used by pydantic-core