From 5dd9abda17b0770bfe8e9420fd24701cc724fb27 Mon Sep 17 00:00:00 2001 From: Yurii Karabas <1998uriyyo@gmail.com> Date: Sun, 17 Nov 2024 14:23:07 +0100 Subject: [PATCH 1/2] Add fail-fast for dicts, model and dataclass --- python/pydantic_core/core_schema.py | 18 ++++++++ src/validators/dataclass.rs | 11 +++++ src/validators/dict.rs | 20 +++++++++ src/validators/model_fields.rs | 7 ++++ src/validators/typed_dict.rs | 13 ++++++ tests/validators/test_dataclasses.py | 60 +++++++++++++++++++++++++++ tests/validators/test_dict.py | 58 ++++++++++++++++++++++++++ tests/validators/test_model_fields.py | 53 +++++++++++++++++++++++ tests/validators/test_typed_dict.py | 59 ++++++++++++++++++++++++++ 9 files changed, 299 insertions(+) diff --git a/python/pydantic_core/core_schema.py b/python/pydantic_core/core_schema.py index c023a5635..630850357 100644 --- a/python/pydantic_core/core_schema.py +++ b/python/pydantic_core/core_schema.py @@ -96,6 +96,8 @@ class CoreConfig(TypedDict, total=False): validate_default: bool # used on typed-dicts and arguments populate_by_name: bool # replaces `allow_population_by_field_name` in pydantic v1 + # stop validation on a first error, used with typed-dict, model-fields, and dataclass fields + fail_fast: bool # fields related to string fields only str_max_length: int str_min_length: int @@ -1885,6 +1887,7 @@ class DictSchema(TypedDict, total=False): values_schema: CoreSchema # default: AnySchema min_length: int max_length: int + fail_fast: bool strict: bool ref: str metadata: Dict[str, Any] @@ -1897,6 +1900,7 @@ def dict_schema( *, min_length: int | None = None, max_length: int | None = None, + fail_fast: bool | None = None, strict: bool | None = None, ref: str | None = None, metadata: Dict[str, Any] | None = None, @@ -1920,6 +1924,7 @@ def dict_schema( values_schema: The value must be a dict with values that match this schema min_length: The value must be a dict with at least this many items max_length: The value must be a dict with at most this many items + fail_fast: Stop validation on the first error strict: Whether the keys and values should be validated with strict mode 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 @@ -1931,6 +1936,7 @@ def dict_schema( values_schema=values_schema, min_length=min_length, max_length=max_length, + fail_fast=fail_fast, strict=strict, ref=ref, metadata=metadata, @@ -2868,6 +2874,7 @@ class TypedDictSchema(TypedDict, total=False): extra_behavior: ExtraBehavior total: bool # default: True populate_by_name: bool # replaces `allow_population_by_field_name` in pydantic v1 + fail_fast: bool # default: False ref: str metadata: Dict[str, Any] serialization: SerSchema @@ -2884,6 +2891,7 @@ def typed_dict_schema( extra_behavior: ExtraBehavior | None = None, total: bool | None = None, populate_by_name: bool | None = None, + fail_fast: bool | None = None, ref: str | None = None, metadata: Dict[str, Any] | None = None, serialization: SerSchema | None = None, @@ -2918,6 +2926,7 @@ class MyTypedDict(TypedDict): extra_behavior: The extra behavior to use for the typed dict total: Whether the typed dict is total, otherwise uses `typed_dict_total` from config populate_by_name: Whether the typed dict should populate by name + fail_fast: Stop validation on the first error serialization: Custom serialization schema """ return _dict_not_none( @@ -2930,6 +2939,7 @@ class MyTypedDict(TypedDict): extra_behavior=extra_behavior, total=total, populate_by_name=populate_by_name, + fail_fast=fail_fast, ref=ref, metadata=metadata, serialization=serialization, @@ -2994,6 +3004,7 @@ class ModelFieldsSchema(TypedDict, total=False): # all these values can be set via config, equivalent fields have `typed_dict_` prefix extra_behavior: ExtraBehavior populate_by_name: bool # replaces `allow_population_by_field_name` in pydantic v1 + fail_fast: bool # default: False from_attributes: bool ref: str metadata: Dict[str, Any] @@ -3010,6 +3021,7 @@ def model_fields_schema( extra_behavior: ExtraBehavior | None = None, populate_by_name: bool | None = None, from_attributes: bool | None = None, + fail_fast: bool | None = None, ref: str | None = None, metadata: Dict[str, Any] | None = None, serialization: SerSchema | None = None, @@ -3039,6 +3051,7 @@ def model_fields_schema( extra_behavior: The extra behavior to use for the typed dict populate_by_name: Whether the typed dict should populate by name from_attributes: Whether the typed dict should be populated from attributes + fail_fast: Stop validation on the first error serialization: Custom serialization schema """ return _dict_not_none( @@ -3051,6 +3064,7 @@ def model_fields_schema( extra_behavior=extra_behavior, populate_by_name=populate_by_name, from_attributes=from_attributes, + fail_fast=fail_fast, ref=ref, metadata=metadata, serialization=serialization, @@ -3234,6 +3248,7 @@ class DataclassArgsSchema(TypedDict, total=False): fields: Required[List[DataclassField]] computed_fields: List[ComputedField] populate_by_name: bool # default: False + fail_fast: bool # default: False collect_init_only: bool # default: False ref: str metadata: Dict[str, Any] @@ -3247,6 +3262,7 @@ def dataclass_args_schema( *, computed_fields: List[ComputedField] | None = None, populate_by_name: bool | None = None, + fail_fast: bool | None = None, collect_init_only: bool | None = None, ref: str | None = None, metadata: Dict[str, Any] | None = None, @@ -3275,6 +3291,7 @@ def dataclass_args_schema( fields: The fields to use for the dataclass computed_fields: Computed fields to use when serializing the dataclass populate_by_name: Whether to populate by name + fail_fast: Stop validation on the first error collect_init_only: Whether to collect init only fields into a dict to pass to `__post_init__` 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 @@ -3287,6 +3304,7 @@ def dataclass_args_schema( fields=fields, computed_fields=computed_fields, populate_by_name=populate_by_name, + fail_fast=fail_fast, collect_init_only=collect_init_only, ref=ref, metadata=metadata, diff --git a/src/validators/dataclass.rs b/src/validators/dataclass.rs index b41de429f..f29c196a8 100644 --- a/src/validators/dataclass.rs +++ b/src/validators/dataclass.rs @@ -40,6 +40,7 @@ pub struct DataclassArgsValidator { validator_name: String, extra_behavior: ExtraBehavior, extras_validator: Option>, + fail_fast: bool, loc_by_alias: bool, } @@ -54,6 +55,7 @@ impl BuildValidator for DataclassArgsValidator { let py = schema.py(); let populate_by_name = schema_or_config_same(schema, config, intern!(py, "populate_by_name"))?.unwrap_or(false); + let fail_fast = schema_or_config_same(schema, config, intern!(py, "fail_fast"))?.unwrap_or(false); let extra_behavior = ExtraBehavior::from_schema_or_config(py, schema, config, ExtraBehavior::Ignore)?; @@ -128,6 +130,7 @@ impl BuildValidator for DataclassArgsValidator { validator_name, extra_behavior, extras_validator, + fail_fast, loc_by_alias: config.get_as(intern!(py, "loc_by_alias"))?.unwrap_or(true), } .into()) @@ -174,6 +177,10 @@ impl Validator for DataclassArgsValidator { // go through fields getting the value from args or kwargs and validating it for (index, field) in self.fields.iter().enumerate() { + if self.fail_fast && !errors.is_empty() { + break; + } + if !field.init { match field.validator.default_value(py, Some(field.name.as_str()), state) { Ok(Some(value)) => { @@ -291,6 +298,10 @@ impl Validator for DataclassArgsValidator { if let Some(kwargs) = args.kwargs() { if kwargs.len() != used_keys.len() { for result in kwargs.iter() { + if self.fail_fast && !errors.is_empty() { + break; + } + let (raw_key, value) = result?; match raw_key .borrow_input() diff --git a/src/validators/dict.rs b/src/validators/dict.rs index 985fe95b6..e773fe19c 100644 --- a/src/validators/dict.rs +++ b/src/validators/dict.rs @@ -21,6 +21,7 @@ pub struct DictValidator { value_validator: Box, min_length: Option, max_length: Option, + fail_fast: bool, name: String, } @@ -53,6 +54,7 @@ impl BuildValidator for DictValidator { value_validator, min_length: schema.get_as(intern!(py, "min_length"))?, max_length: schema.get_as(intern!(py, "max_length"))?, + fail_fast: schema.get_as(intern!(py, "fail_fast"))?.unwrap_or(false), name, } .into()) @@ -78,6 +80,7 @@ impl Validator for DictValidator { input, min_length: self.min_length, max_length: self.max_length, + fail_fast: self.fail_fast, key_validator: &self.key_validator, value_validator: &self.value_validator, state, @@ -94,6 +97,7 @@ struct ValidateToDict<'a, 's, 'py, I: Input<'py> + ?Sized> { input: &'a I, min_length: Option, max_length: Option, + fail_fast: bool, key_validator: &'a CombinedValidator, value_validator: &'a CombinedValidator, state: &'a mut ValidationState<'s, 'py>, @@ -111,6 +115,12 @@ where let mut errors: Vec = Vec::new(); let allow_partial = self.state.allow_partial; + macro_rules! should_fail_fast { + () => { + self.fail_fast && !errors.is_empty() + }; + } + for (_, is_last_partial, item_result) in self.state.enumerate_last_partial(iterator) { self.state.allow_partial = false.into(); let (key, value) = item_result?; @@ -130,6 +140,11 @@ where true => allow_partial, false => false.into(), }; + + if should_fail_fast!() { + break; + } + let output_value = match self.value_validator.validate(self.py, value.borrow_input(), self.state) { Ok(value) => value, Err(ValError::LineErrors(line_errors)) => { @@ -141,6 +156,11 @@ where Err(ValError::Omit) => continue, Err(err) => return Err(err), }; + + if should_fail_fast!() { + break; + } + if let Some(key) = output_key { output.set_item(key, output_value)?; } diff --git a/src/validators/model_fields.rs b/src/validators/model_fields.rs index 7aba7c8e3..bf296cfc1 100644 --- a/src/validators/model_fields.rs +++ b/src/validators/model_fields.rs @@ -36,6 +36,7 @@ pub struct ModelFieldsValidator { strict: bool, from_attributes: bool, loc_by_alias: bool, + fail_fast: bool, } impl BuildValidator for ModelFieldsValidator { @@ -51,6 +52,7 @@ impl BuildValidator for ModelFieldsValidator { let from_attributes = schema_or_config_same(schema, config, intern!(py, "from_attributes"))?.unwrap_or(false); let populate_by_name = schema_or_config_same(schema, config, intern!(py, "populate_by_name"))?.unwrap_or(false); + let fail_fast = schema_or_config_same(schema, config, intern!(py, "fail_fast"))?.unwrap_or(false); let extra_behavior = ExtraBehavior::from_schema_or_config(py, schema, config, ExtraBehavior::Ignore)?; @@ -102,6 +104,7 @@ impl BuildValidator for ModelFieldsValidator { extras_validator, strict, from_attributes, + fail_fast, loc_by_alias: config.get_as(intern!(py, "loc_by_alias"))?.unwrap_or(true), } .into()) @@ -168,6 +171,10 @@ impl Validator for ModelFieldsValidator { let state = &mut state.rebind_extra(|extra| extra.data = Some(model_dict.clone())); for field in &self.fields { + if self.fail_fast && !errors.is_empty() { + break; + } + let op_key_value = match dict.get_item(&field.lookup_key) { Ok(v) => v, Err(ValError::LineErrors(line_errors)) => { diff --git a/src/validators/typed_dict.rs b/src/validators/typed_dict.rs index 0c127b93b..e23915c5f 100644 --- a/src/validators/typed_dict.rs +++ b/src/validators/typed_dict.rs @@ -34,6 +34,7 @@ pub struct TypedDictValidator { extra_behavior: ExtraBehavior, extras_validator: Option>, strict: bool, + fail_fast: bool, loc_by_alias: bool, } @@ -56,6 +57,7 @@ impl BuildValidator for TypedDictValidator { let total = schema_or_config(schema, config, intern!(py, "total"), intern!(py, "typed_dict_total"))?.unwrap_or(true); let populate_by_name = schema_or_config_same(schema, config, intern!(py, "populate_by_name"))?.unwrap_or(false); + let fail_fast = schema_or_config_same(schema, config, intern!(py, "fail_fast"))?.unwrap_or(false); let extra_behavior = ExtraBehavior::from_schema_or_config(py, schema, config, ExtraBehavior::Ignore)?; @@ -129,6 +131,7 @@ impl BuildValidator for TypedDictValidator { extra_behavior, extras_validator, strict, + fail_fast, loc_by_alias: config.get_as(intern!(py, "loc_by_alias"))?.unwrap_or(true), } .into()) @@ -174,6 +177,10 @@ impl Validator for TypedDictValidator { let mut fields_set_count: usize = 0; for field in &self.fields { + if self.fail_fast && !errors.is_empty() { + break; + } + let op_key_value = match dict.get_item(&field.lookup_key) { Ok(v) => v, Err(ValError::LineErrors(line_errors)) => { @@ -265,6 +272,7 @@ impl Validator for TypedDictValidator { extra_behavior: ExtraBehavior, partial_last_key: Option, allow_partial: PartialMode, + fail_fast: bool, } impl<'py, Key, Value> ConsumeIterator> for ValidateExtras<'_, '_, 'py> @@ -275,6 +283,10 @@ impl Validator for TypedDictValidator { type Output = ValResult<()>; fn consume_iterator(self, iterator: impl Iterator>) -> ValResult<()> { for item_result in iterator { + if self.fail_fast && !self.errors.is_empty() { + break; + } + let (raw_key, value) = item_result?; let either_str = match raw_key .borrow_input() @@ -354,6 +366,7 @@ impl Validator for TypedDictValidator { extra_behavior: self.extra_behavior, partial_last_key, allow_partial, + fail_fast: self.fail_fast, })??; } diff --git a/tests/validators/test_dataclasses.py b/tests/validators/test_dataclasses.py index a9b367008..9edff579c 100644 --- a/tests/validators/test_dataclasses.py +++ b/tests/validators/test_dataclasses.py @@ -1713,3 +1713,63 @@ class Foo: assert exc_info.value.errors(include_url=False) == expected.errors else: assert dataclasses.asdict(v.validate_python(input_value)) == expected + + +@pytest.mark.parametrize( + ('fail_fast', 'expected'), + [ + pytest.param( + True, + [ + { + 'type': 'string_type', + 'loc': ('a',), + 'msg': 'Input should be a valid string', + 'input': 10, + }, + ], + id='fail_fast', + ), + pytest.param( + False, + [ + { + 'type': 'string_type', + 'loc': ('a',), + 'msg': 'Input should be a valid string', + 'input': 10, + }, + { + 'type': 'string_type', + 'loc': ('b',), + 'msg': 'Input should be a valid string', + 'input': 20, + }, + ], + id='not_fail_fast', + ), + ], +) +def test_dataclass_fail_fast(fail_fast, expected): + @dataclasses.dataclass + class Foo: + a: str + b: str + + schema = core_schema.dataclass_schema( + Foo, + core_schema.dataclass_args_schema( + 'Foo', + [ + core_schema.dataclass_field(name='a', schema=core_schema.str_schema()), + core_schema.dataclass_field(name='b', schema=core_schema.str_schema()), + ], + fail_fast=fail_fast, + ), + ['a', 'b'], + ) + + with pytest.raises(ValidationError) as exc_info: + SchemaValidator(schema).validate_python({'a': 10, 'b': 20}) + + assert exc_info.value.errors(include_url=False) == expected diff --git a/tests/validators/test_dict.py b/tests/validators/test_dict.py index 4057ce76e..7443a0c42 100644 --- a/tests/validators/test_dict.py +++ b/tests/validators/test_dict.py @@ -258,3 +258,61 @@ def test_json_dict_complex_key(): assert v.validate_json('{"1+2j": 2, "infj": 4}') == {complex(1, 2): 2, complex(0, float('inf')): 4} with pytest.raises(ValidationError, match='Input should be a valid complex string'): v.validate_json('{"1+2j": 2, "": 4}') == {complex(1, 2): 2, complex(0, float('inf')): 4} + + +@pytest.mark.parametrize( + ('fail_fast', 'expected'), + [ + pytest.param( + True, + [ + { + 'type': 'int_parsing', + 'loc': ('a', '[key]'), + 'msg': 'Input should be a valid integer, unable to parse string as an integer', + 'input': 'a', + }, + ], + id='fail_fast', + ), + pytest.param( + False, + [ + { + 'type': 'int_parsing', + 'loc': ('a', '[key]'), + 'msg': 'Input should be a valid integer, unable to parse string as an integer', + 'input': 'a', + }, + { + 'type': 'int_parsing', + 'loc': ('a',), + 'msg': 'Input should be a valid integer, unable to parse string as an integer', + 'input': 'b', + }, + { + 'type': 'int_parsing', + 'loc': ('c', '[key]'), + 'msg': 'Input should be a valid integer, unable to parse string as an integer', + 'input': 'c', + }, + { + 'type': 'int_parsing', + 'loc': ('c',), + 'msg': 'Input should be a valid integer, unable to parse string as an integer', + 'input': 'd', + }, + ], + id='not_fail_fast', + ), + ], +) +def test_dict_fail_fast(fail_fast, expected): + v = SchemaValidator( + {'type': 'dict', 'keys_schema': {'type': 'int'}, 'values_schema': {'type': 'int'}, 'fail_fast': fail_fast} + ) + + with pytest.raises(ValidationError) as exc_info: + v.validate_python({'a': 'b', 'c': 'd'}) + + assert exc_info.value.errors(include_url=False) == expected diff --git a/tests/validators/test_model_fields.py b/tests/validators/test_model_fields.py index 8a22d96c3..2c971ac5b 100644 --- a/tests/validators/test_model_fields.py +++ b/tests/validators/test_model_fields.py @@ -1781,3 +1781,56 @@ def test_extra_behavior_ignore(config: Union[core_schema.CoreConfig, None], sche } ] assert 'not_f' not in m + + +@pytest.mark.parametrize( + ('fail_fast', 'expected'), + [ + pytest.param( + True, + [ + { + 'input': 'x', + 'loc': ('a',), + 'msg': 'Input should be a valid integer, unable to parse string as an integer', + 'type': 'int_parsing', + }, + ], + id='fail_fast', + ), + pytest.param( + False, + [ + { + 'input': 'x', + 'loc': ('a',), + 'msg': 'Input should be a valid integer, unable to parse string as an integer', + 'type': 'int_parsing', + }, + { + 'input': 'y', + 'loc': ('b',), + 'msg': 'Input should be a valid integer, unable to parse string as an integer', + 'type': 'int_parsing', + }, + ], + id='not_fail_fast', + ), + ], +) +def test_model_fields_fail_fast(fail_fast, expected): + v = SchemaValidator( + { + 'type': 'model-fields', + 'fields': { + 'a': {'type': 'model-field', 'schema': {'type': 'int'}}, + 'b': {'type': 'model-field', 'schema': {'type': 'int'}}, + }, + 'fail_fast': fail_fast, + }, + ) + + with pytest.raises(ValidationError) as exc_info: + v.validate_python({'a': 'x', 'b': 'y'}) + + assert exc_info.value.errors(include_url=False) == expected diff --git a/tests/validators/test_typed_dict.py b/tests/validators/test_typed_dict.py index dc18cd86e..630f4c45b 100644 --- a/tests/validators/test_typed_dict.py +++ b/tests/validators/test_typed_dict.py @@ -1196,3 +1196,62 @@ def validate(v, info): gc.collect() assert ref() is None + + +@pytest.mark.parametrize( + ('fail_fast', 'expected'), + [ + pytest.param( + True, + [ + { + 'input': 'c', + 'loc': ('a',), + 'msg': 'Input should be a valid integer, unable to parse string as an integer', + 'type': 'int_parsing', + }, + ], + id='fail_fast', + ), + pytest.param( + False, + [ + { + 'input': 'c', + 'loc': ('a',), + 'msg': 'Input should be a valid integer, unable to parse string as an integer', + 'type': 'int_parsing', + }, + { + 'input': 'd', + 'loc': ('b',), + 'msg': 'Input should be a valid integer, unable to parse string as an integer', + 'type': 'int_parsing', + }, + ], + id='not_fail_fast', + ), + ], +) +def test_typed_dict_fail_fast(fail_fast, expected): + v = SchemaValidator( + { + 'type': 'typed-dict', + 'fields': { + 'a': { + 'type': 'typed-dict-field', + 'schema': {'type': 'int'}, + }, + 'b': { + 'type': 'typed-dict-field', + 'schema': {'type': 'int'}, + }, + }, + 'fail_fast': fail_fast, + } + ) + + with pytest.raises(ValidationError) as exc_info: + v.validate_python({'a': 'c', 'b': 'd'}) + + assert exc_info.value.errors(include_url=False) == expected From 7642acc048c74c0897a68a1559e6ab47034d9bb6 Mon Sep 17 00:00:00 2001 From: Yurii Karabas <1998uriyyo@gmail.com> Date: Thu, 26 Dec 2024 09:33:33 +0100 Subject: [PATCH 2/2] Revert changes to model-fields and dataclass --- python/pydantic_core/core_schema.py | 10 +---- src/validators/dataclass.rs | 11 ----- src/validators/model_fields.rs | 7 ---- tests/validators/test_dataclasses.py | 60 --------------------------- tests/validators/test_model_fields.py | 53 ----------------------- 5 files changed, 1 insertion(+), 140 deletions(-) diff --git a/python/pydantic_core/core_schema.py b/python/pydantic_core/core_schema.py index c968ab8f6..b5bc8e960 100644 --- a/python/pydantic_core/core_schema.py +++ b/python/pydantic_core/core_schema.py @@ -96,7 +96,7 @@ class CoreConfig(TypedDict, total=False): validate_default: bool # used on typed-dicts and arguments populate_by_name: bool # replaces `allow_population_by_field_name` in pydantic v1 - # stop validation on a first error, used with typed-dict, model-fields, and dataclass fields + # stop validation on a first error, used with typed-dict fail_fast: bool # fields related to string fields only str_max_length: int @@ -3004,7 +3004,6 @@ class ModelFieldsSchema(TypedDict, total=False): # all these values can be set via config, equivalent fields have `typed_dict_` prefix extra_behavior: ExtraBehavior populate_by_name: bool # replaces `allow_population_by_field_name` in pydantic v1 - fail_fast: bool # default: False from_attributes: bool ref: str metadata: Dict[str, Any] @@ -3021,7 +3020,6 @@ def model_fields_schema( extra_behavior: ExtraBehavior | None = None, populate_by_name: bool | None = None, from_attributes: bool | None = None, - fail_fast: bool | None = None, ref: str | None = None, metadata: Dict[str, Any] | None = None, serialization: SerSchema | None = None, @@ -3051,7 +3049,6 @@ def model_fields_schema( extra_behavior: The extra behavior to use for the typed dict populate_by_name: Whether the typed dict should populate by name from_attributes: Whether the typed dict should be populated from attributes - fail_fast: Stop validation on the first error serialization: Custom serialization schema """ return _dict_not_none( @@ -3064,7 +3061,6 @@ def model_fields_schema( extra_behavior=extra_behavior, populate_by_name=populate_by_name, from_attributes=from_attributes, - fail_fast=fail_fast, ref=ref, metadata=metadata, serialization=serialization, @@ -3248,7 +3244,6 @@ class DataclassArgsSchema(TypedDict, total=False): fields: Required[List[DataclassField]] computed_fields: List[ComputedField] populate_by_name: bool # default: False - fail_fast: bool # default: False collect_init_only: bool # default: False ref: str metadata: Dict[str, Any] @@ -3262,7 +3257,6 @@ def dataclass_args_schema( *, computed_fields: List[ComputedField] | None = None, populate_by_name: bool | None = None, - fail_fast: bool | None = None, collect_init_only: bool | None = None, ref: str | None = None, metadata: Dict[str, Any] | None = None, @@ -3291,7 +3285,6 @@ def dataclass_args_schema( fields: The fields to use for the dataclass computed_fields: Computed fields to use when serializing the dataclass populate_by_name: Whether to populate by name - fail_fast: Stop validation on the first error collect_init_only: Whether to collect init only fields into a dict to pass to `__post_init__` 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 @@ -3304,7 +3297,6 @@ def dataclass_args_schema( fields=fields, computed_fields=computed_fields, populate_by_name=populate_by_name, - fail_fast=fail_fast, collect_init_only=collect_init_only, ref=ref, metadata=metadata, diff --git a/src/validators/dataclass.rs b/src/validators/dataclass.rs index 1b63ad95c..53be75e88 100644 --- a/src/validators/dataclass.rs +++ b/src/validators/dataclass.rs @@ -40,7 +40,6 @@ pub struct DataclassArgsValidator { validator_name: String, extra_behavior: ExtraBehavior, extras_validator: Option>, - fail_fast: bool, loc_by_alias: bool, } @@ -55,7 +54,6 @@ impl BuildValidator for DataclassArgsValidator { let py = schema.py(); let populate_by_name = schema_or_config_same(schema, config, intern!(py, "populate_by_name"))?.unwrap_or(false); - let fail_fast = schema_or_config_same(schema, config, intern!(py, "fail_fast"))?.unwrap_or(false); let extra_behavior = ExtraBehavior::from_schema_or_config(py, schema, config, ExtraBehavior::Ignore)?; @@ -130,7 +128,6 @@ impl BuildValidator for DataclassArgsValidator { validator_name, extra_behavior, extras_validator, - fail_fast, loc_by_alias: config.get_as(intern!(py, "loc_by_alias"))?.unwrap_or(true), } .into()) @@ -177,10 +174,6 @@ impl Validator for DataclassArgsValidator { // go through fields getting the value from args or kwargs and validating it for (index, field) in self.fields.iter().enumerate() { - if self.fail_fast && !errors.is_empty() { - break; - } - if !field.init { match field.validator.default_value(py, Some(field.name.as_str()), state) { Ok(Some(value)) => { @@ -298,10 +291,6 @@ impl Validator for DataclassArgsValidator { if let Some(kwargs) = args.kwargs() { if kwargs.len() != used_keys.len() { for result in kwargs.iter() { - if self.fail_fast && !errors.is_empty() { - break; - } - let (raw_key, value) = result?; match raw_key .borrow_input() diff --git a/src/validators/model_fields.rs b/src/validators/model_fields.rs index bf296cfc1..7aba7c8e3 100644 --- a/src/validators/model_fields.rs +++ b/src/validators/model_fields.rs @@ -36,7 +36,6 @@ pub struct ModelFieldsValidator { strict: bool, from_attributes: bool, loc_by_alias: bool, - fail_fast: bool, } impl BuildValidator for ModelFieldsValidator { @@ -52,7 +51,6 @@ impl BuildValidator for ModelFieldsValidator { let from_attributes = schema_or_config_same(schema, config, intern!(py, "from_attributes"))?.unwrap_or(false); let populate_by_name = schema_or_config_same(schema, config, intern!(py, "populate_by_name"))?.unwrap_or(false); - let fail_fast = schema_or_config_same(schema, config, intern!(py, "fail_fast"))?.unwrap_or(false); let extra_behavior = ExtraBehavior::from_schema_or_config(py, schema, config, ExtraBehavior::Ignore)?; @@ -104,7 +102,6 @@ impl BuildValidator for ModelFieldsValidator { extras_validator, strict, from_attributes, - fail_fast, loc_by_alias: config.get_as(intern!(py, "loc_by_alias"))?.unwrap_or(true), } .into()) @@ -171,10 +168,6 @@ impl Validator for ModelFieldsValidator { let state = &mut state.rebind_extra(|extra| extra.data = Some(model_dict.clone())); for field in &self.fields { - if self.fail_fast && !errors.is_empty() { - break; - } - let op_key_value = match dict.get_item(&field.lookup_key) { Ok(v) => v, Err(ValError::LineErrors(line_errors)) => { diff --git a/tests/validators/test_dataclasses.py b/tests/validators/test_dataclasses.py index 9edff579c..a9b367008 100644 --- a/tests/validators/test_dataclasses.py +++ b/tests/validators/test_dataclasses.py @@ -1713,63 +1713,3 @@ class Foo: assert exc_info.value.errors(include_url=False) == expected.errors else: assert dataclasses.asdict(v.validate_python(input_value)) == expected - - -@pytest.mark.parametrize( - ('fail_fast', 'expected'), - [ - pytest.param( - True, - [ - { - 'type': 'string_type', - 'loc': ('a',), - 'msg': 'Input should be a valid string', - 'input': 10, - }, - ], - id='fail_fast', - ), - pytest.param( - False, - [ - { - 'type': 'string_type', - 'loc': ('a',), - 'msg': 'Input should be a valid string', - 'input': 10, - }, - { - 'type': 'string_type', - 'loc': ('b',), - 'msg': 'Input should be a valid string', - 'input': 20, - }, - ], - id='not_fail_fast', - ), - ], -) -def test_dataclass_fail_fast(fail_fast, expected): - @dataclasses.dataclass - class Foo: - a: str - b: str - - schema = core_schema.dataclass_schema( - Foo, - core_schema.dataclass_args_schema( - 'Foo', - [ - core_schema.dataclass_field(name='a', schema=core_schema.str_schema()), - core_schema.dataclass_field(name='b', schema=core_schema.str_schema()), - ], - fail_fast=fail_fast, - ), - ['a', 'b'], - ) - - with pytest.raises(ValidationError) as exc_info: - SchemaValidator(schema).validate_python({'a': 10, 'b': 20}) - - assert exc_info.value.errors(include_url=False) == expected diff --git a/tests/validators/test_model_fields.py b/tests/validators/test_model_fields.py index 2c971ac5b..8a22d96c3 100644 --- a/tests/validators/test_model_fields.py +++ b/tests/validators/test_model_fields.py @@ -1781,56 +1781,3 @@ def test_extra_behavior_ignore(config: Union[core_schema.CoreConfig, None], sche } ] assert 'not_f' not in m - - -@pytest.mark.parametrize( - ('fail_fast', 'expected'), - [ - pytest.param( - True, - [ - { - 'input': 'x', - 'loc': ('a',), - 'msg': 'Input should be a valid integer, unable to parse string as an integer', - 'type': 'int_parsing', - }, - ], - id='fail_fast', - ), - pytest.param( - False, - [ - { - 'input': 'x', - 'loc': ('a',), - 'msg': 'Input should be a valid integer, unable to parse string as an integer', - 'type': 'int_parsing', - }, - { - 'input': 'y', - 'loc': ('b',), - 'msg': 'Input should be a valid integer, unable to parse string as an integer', - 'type': 'int_parsing', - }, - ], - id='not_fail_fast', - ), - ], -) -def test_model_fields_fail_fast(fail_fast, expected): - v = SchemaValidator( - { - 'type': 'model-fields', - 'fields': { - 'a': {'type': 'model-field', 'schema': {'type': 'int'}}, - 'b': {'type': 'model-field', 'schema': {'type': 'int'}}, - }, - 'fail_fast': fail_fast, - }, - ) - - with pytest.raises(ValidationError) as exc_info: - v.validate_python({'a': 'x', 'b': 'y'}) - - assert exc_info.value.errors(include_url=False) == expected