Skip to content

Commit 5dd9abd

Browse files
committed
Add fail-fast for dicts, model and dataclass
1 parent 6472887 commit 5dd9abd

File tree

9 files changed

+299
-0
lines changed

9 files changed

+299
-0
lines changed

python/pydantic_core/core_schema.py

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -96,6 +96,8 @@ class CoreConfig(TypedDict, total=False):
9696
validate_default: bool
9797
# used on typed-dicts and arguments
9898
populate_by_name: bool # replaces `allow_population_by_field_name` in pydantic v1
99+
# stop validation on a first error, used with typed-dict, model-fields, and dataclass fields
100+
fail_fast: bool
99101
# fields related to string fields only
100102
str_max_length: int
101103
str_min_length: int
@@ -1885,6 +1887,7 @@ class DictSchema(TypedDict, total=False):
18851887
values_schema: CoreSchema # default: AnySchema
18861888
min_length: int
18871889
max_length: int
1890+
fail_fast: bool
18881891
strict: bool
18891892
ref: str
18901893
metadata: Dict[str, Any]
@@ -1897,6 +1900,7 @@ def dict_schema(
18971900
*,
18981901
min_length: int | None = None,
18991902
max_length: int | None = None,
1903+
fail_fast: bool | None = None,
19001904
strict: bool | None = None,
19011905
ref: str | None = None,
19021906
metadata: Dict[str, Any] | None = None,
@@ -1920,6 +1924,7 @@ def dict_schema(
19201924
values_schema: The value must be a dict with values that match this schema
19211925
min_length: The value must be a dict with at least this many items
19221926
max_length: The value must be a dict with at most this many items
1927+
fail_fast: Stop validation on the first error
19231928
strict: Whether the keys and values should be validated with strict mode
19241929
ref: optional unique identifier of the schema, used to reference the schema in other places
19251930
metadata: Any other information you want to include with the schema, not used by pydantic-core
@@ -1931,6 +1936,7 @@ def dict_schema(
19311936
values_schema=values_schema,
19321937
min_length=min_length,
19331938
max_length=max_length,
1939+
fail_fast=fail_fast,
19341940
strict=strict,
19351941
ref=ref,
19361942
metadata=metadata,
@@ -2868,6 +2874,7 @@ class TypedDictSchema(TypedDict, total=False):
28682874
extra_behavior: ExtraBehavior
28692875
total: bool # default: True
28702876
populate_by_name: bool # replaces `allow_population_by_field_name` in pydantic v1
2877+
fail_fast: bool # default: False
28712878
ref: str
28722879
metadata: Dict[str, Any]
28732880
serialization: SerSchema
@@ -2884,6 +2891,7 @@ def typed_dict_schema(
28842891
extra_behavior: ExtraBehavior | None = None,
28852892
total: bool | None = None,
28862893
populate_by_name: bool | None = None,
2894+
fail_fast: bool | None = None,
28872895
ref: str | None = None,
28882896
metadata: Dict[str, Any] | None = None,
28892897
serialization: SerSchema | None = None,
@@ -2918,6 +2926,7 @@ class MyTypedDict(TypedDict):
29182926
extra_behavior: The extra behavior to use for the typed dict
29192927
total: Whether the typed dict is total, otherwise uses `typed_dict_total` from config
29202928
populate_by_name: Whether the typed dict should populate by name
2929+
fail_fast: Stop validation on the first error
29212930
serialization: Custom serialization schema
29222931
"""
29232932
return _dict_not_none(
@@ -2930,6 +2939,7 @@ class MyTypedDict(TypedDict):
29302939
extra_behavior=extra_behavior,
29312940
total=total,
29322941
populate_by_name=populate_by_name,
2942+
fail_fast=fail_fast,
29332943
ref=ref,
29342944
metadata=metadata,
29352945
serialization=serialization,
@@ -2994,6 +3004,7 @@ class ModelFieldsSchema(TypedDict, total=False):
29943004
# all these values can be set via config, equivalent fields have `typed_dict_` prefix
29953005
extra_behavior: ExtraBehavior
29963006
populate_by_name: bool # replaces `allow_population_by_field_name` in pydantic v1
3007+
fail_fast: bool # default: False
29973008
from_attributes: bool
29983009
ref: str
29993010
metadata: Dict[str, Any]
@@ -3010,6 +3021,7 @@ def model_fields_schema(
30103021
extra_behavior: ExtraBehavior | None = None,
30113022
populate_by_name: bool | None = None,
30123023
from_attributes: bool | None = None,
3024+
fail_fast: bool | None = None,
30133025
ref: str | None = None,
30143026
metadata: Dict[str, Any] | None = None,
30153027
serialization: SerSchema | None = None,
@@ -3039,6 +3051,7 @@ def model_fields_schema(
30393051
extra_behavior: The extra behavior to use for the typed dict
30403052
populate_by_name: Whether the typed dict should populate by name
30413053
from_attributes: Whether the typed dict should be populated from attributes
3054+
fail_fast: Stop validation on the first error
30423055
serialization: Custom serialization schema
30433056
"""
30443057
return _dict_not_none(
@@ -3051,6 +3064,7 @@ def model_fields_schema(
30513064
extra_behavior=extra_behavior,
30523065
populate_by_name=populate_by_name,
30533066
from_attributes=from_attributes,
3067+
fail_fast=fail_fast,
30543068
ref=ref,
30553069
metadata=metadata,
30563070
serialization=serialization,
@@ -3234,6 +3248,7 @@ class DataclassArgsSchema(TypedDict, total=False):
32343248
fields: Required[List[DataclassField]]
32353249
computed_fields: List[ComputedField]
32363250
populate_by_name: bool # default: False
3251+
fail_fast: bool # default: False
32373252
collect_init_only: bool # default: False
32383253
ref: str
32393254
metadata: Dict[str, Any]
@@ -3247,6 +3262,7 @@ def dataclass_args_schema(
32473262
*,
32483263
computed_fields: List[ComputedField] | None = None,
32493264
populate_by_name: bool | None = None,
3265+
fail_fast: bool | None = None,
32503266
collect_init_only: bool | None = None,
32513267
ref: str | None = None,
32523268
metadata: Dict[str, Any] | None = None,
@@ -3275,6 +3291,7 @@ def dataclass_args_schema(
32753291
fields: The fields to use for the dataclass
32763292
computed_fields: Computed fields to use when serializing the dataclass
32773293
populate_by_name: Whether to populate by name
3294+
fail_fast: Stop validation on the first error
32783295
collect_init_only: Whether to collect init only fields into a dict to pass to `__post_init__`
32793296
ref: optional unique identifier of the schema, used to reference the schema in other places
32803297
metadata: Any other information you want to include with the schema, not used by pydantic-core
@@ -3287,6 +3304,7 @@ def dataclass_args_schema(
32873304
fields=fields,
32883305
computed_fields=computed_fields,
32893306
populate_by_name=populate_by_name,
3307+
fail_fast=fail_fast,
32903308
collect_init_only=collect_init_only,
32913309
ref=ref,
32923310
metadata=metadata,

src/validators/dataclass.rs

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,7 @@ pub struct DataclassArgsValidator {
4040
validator_name: String,
4141
extra_behavior: ExtraBehavior,
4242
extras_validator: Option<Box<CombinedValidator>>,
43+
fail_fast: bool,
4344
loc_by_alias: bool,
4445
}
4546

@@ -54,6 +55,7 @@ impl BuildValidator for DataclassArgsValidator {
5455
let py = schema.py();
5556

5657
let populate_by_name = schema_or_config_same(schema, config, intern!(py, "populate_by_name"))?.unwrap_or(false);
58+
let fail_fast = schema_or_config_same(schema, config, intern!(py, "fail_fast"))?.unwrap_or(false);
5759

5860
let extra_behavior = ExtraBehavior::from_schema_or_config(py, schema, config, ExtraBehavior::Ignore)?;
5961

@@ -128,6 +130,7 @@ impl BuildValidator for DataclassArgsValidator {
128130
validator_name,
129131
extra_behavior,
130132
extras_validator,
133+
fail_fast,
131134
loc_by_alias: config.get_as(intern!(py, "loc_by_alias"))?.unwrap_or(true),
132135
}
133136
.into())
@@ -174,6 +177,10 @@ impl Validator for DataclassArgsValidator {
174177

175178
// go through fields getting the value from args or kwargs and validating it
176179
for (index, field) in self.fields.iter().enumerate() {
180+
if self.fail_fast && !errors.is_empty() {
181+
break;
182+
}
183+
177184
if !field.init {
178185
match field.validator.default_value(py, Some(field.name.as_str()), state) {
179186
Ok(Some(value)) => {
@@ -291,6 +298,10 @@ impl Validator for DataclassArgsValidator {
291298
if let Some(kwargs) = args.kwargs() {
292299
if kwargs.len() != used_keys.len() {
293300
for result in kwargs.iter() {
301+
if self.fail_fast && !errors.is_empty() {
302+
break;
303+
}
304+
294305
let (raw_key, value) = result?;
295306
match raw_key
296307
.borrow_input()

src/validators/dict.rs

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ pub struct DictValidator {
2121
value_validator: Box<CombinedValidator>,
2222
min_length: Option<usize>,
2323
max_length: Option<usize>,
24+
fail_fast: bool,
2425
name: String,
2526
}
2627

@@ -53,6 +54,7 @@ impl BuildValidator for DictValidator {
5354
value_validator,
5455
min_length: schema.get_as(intern!(py, "min_length"))?,
5556
max_length: schema.get_as(intern!(py, "max_length"))?,
57+
fail_fast: schema.get_as(intern!(py, "fail_fast"))?.unwrap_or(false),
5658
name,
5759
}
5860
.into())
@@ -78,6 +80,7 @@ impl Validator for DictValidator {
7880
input,
7981
min_length: self.min_length,
8082
max_length: self.max_length,
83+
fail_fast: self.fail_fast,
8184
key_validator: &self.key_validator,
8285
value_validator: &self.value_validator,
8386
state,
@@ -94,6 +97,7 @@ struct ValidateToDict<'a, 's, 'py, I: Input<'py> + ?Sized> {
9497
input: &'a I,
9598
min_length: Option<usize>,
9699
max_length: Option<usize>,
100+
fail_fast: bool,
97101
key_validator: &'a CombinedValidator,
98102
value_validator: &'a CombinedValidator,
99103
state: &'a mut ValidationState<'s, 'py>,
@@ -111,6 +115,12 @@ where
111115
let mut errors: Vec<ValLineError> = Vec::new();
112116
let allow_partial = self.state.allow_partial;
113117

118+
macro_rules! should_fail_fast {
119+
() => {
120+
self.fail_fast && !errors.is_empty()
121+
};
122+
}
123+
114124
for (_, is_last_partial, item_result) in self.state.enumerate_last_partial(iterator) {
115125
self.state.allow_partial = false.into();
116126
let (key, value) = item_result?;
@@ -130,6 +140,11 @@ where
130140
true => allow_partial,
131141
false => false.into(),
132142
};
143+
144+
if should_fail_fast!() {
145+
break;
146+
}
147+
133148
let output_value = match self.value_validator.validate(self.py, value.borrow_input(), self.state) {
134149
Ok(value) => value,
135150
Err(ValError::LineErrors(line_errors)) => {
@@ -141,6 +156,11 @@ where
141156
Err(ValError::Omit) => continue,
142157
Err(err) => return Err(err),
143158
};
159+
160+
if should_fail_fast!() {
161+
break;
162+
}
163+
144164
if let Some(key) = output_key {
145165
output.set_item(key, output_value)?;
146166
}

src/validators/model_fields.rs

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@ pub struct ModelFieldsValidator {
3636
strict: bool,
3737
from_attributes: bool,
3838
loc_by_alias: bool,
39+
fail_fast: bool,
3940
}
4041

4142
impl BuildValidator for ModelFieldsValidator {
@@ -51,6 +52,7 @@ impl BuildValidator for ModelFieldsValidator {
5152

5253
let from_attributes = schema_or_config_same(schema, config, intern!(py, "from_attributes"))?.unwrap_or(false);
5354
let populate_by_name = schema_or_config_same(schema, config, intern!(py, "populate_by_name"))?.unwrap_or(false);
55+
let fail_fast = schema_or_config_same(schema, config, intern!(py, "fail_fast"))?.unwrap_or(false);
5456

5557
let extra_behavior = ExtraBehavior::from_schema_or_config(py, schema, config, ExtraBehavior::Ignore)?;
5658

@@ -102,6 +104,7 @@ impl BuildValidator for ModelFieldsValidator {
102104
extras_validator,
103105
strict,
104106
from_attributes,
107+
fail_fast,
105108
loc_by_alias: config.get_as(intern!(py, "loc_by_alias"))?.unwrap_or(true),
106109
}
107110
.into())
@@ -168,6 +171,10 @@ impl Validator for ModelFieldsValidator {
168171
let state = &mut state.rebind_extra(|extra| extra.data = Some(model_dict.clone()));
169172

170173
for field in &self.fields {
174+
if self.fail_fast && !errors.is_empty() {
175+
break;
176+
}
177+
171178
let op_key_value = match dict.get_item(&field.lookup_key) {
172179
Ok(v) => v,
173180
Err(ValError::LineErrors(line_errors)) => {

src/validators/typed_dict.rs

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@ pub struct TypedDictValidator {
3434
extra_behavior: ExtraBehavior,
3535
extras_validator: Option<Box<CombinedValidator>>,
3636
strict: bool,
37+
fail_fast: bool,
3738
loc_by_alias: bool,
3839
}
3940

@@ -56,6 +57,7 @@ impl BuildValidator for TypedDictValidator {
5657
let total =
5758
schema_or_config(schema, config, intern!(py, "total"), intern!(py, "typed_dict_total"))?.unwrap_or(true);
5859
let populate_by_name = schema_or_config_same(schema, config, intern!(py, "populate_by_name"))?.unwrap_or(false);
60+
let fail_fast = schema_or_config_same(schema, config, intern!(py, "fail_fast"))?.unwrap_or(false);
5961

6062
let extra_behavior = ExtraBehavior::from_schema_or_config(py, schema, config, ExtraBehavior::Ignore)?;
6163

@@ -129,6 +131,7 @@ impl BuildValidator for TypedDictValidator {
129131
extra_behavior,
130132
extras_validator,
131133
strict,
134+
fail_fast,
132135
loc_by_alias: config.get_as(intern!(py, "loc_by_alias"))?.unwrap_or(true),
133136
}
134137
.into())
@@ -174,6 +177,10 @@ impl Validator for TypedDictValidator {
174177
let mut fields_set_count: usize = 0;
175178

176179
for field in &self.fields {
180+
if self.fail_fast && !errors.is_empty() {
181+
break;
182+
}
183+
177184
let op_key_value = match dict.get_item(&field.lookup_key) {
178185
Ok(v) => v,
179186
Err(ValError::LineErrors(line_errors)) => {
@@ -265,6 +272,7 @@ impl Validator for TypedDictValidator {
265272
extra_behavior: ExtraBehavior,
266273
partial_last_key: Option<LocItem>,
267274
allow_partial: PartialMode,
275+
fail_fast: bool,
268276
}
269277

270278
impl<'py, Key, Value> ConsumeIterator<ValResult<(Key, Value)>> for ValidateExtras<'_, '_, 'py>
@@ -275,6 +283,10 @@ impl Validator for TypedDictValidator {
275283
type Output = ValResult<()>;
276284
fn consume_iterator(self, iterator: impl Iterator<Item = ValResult<(Key, Value)>>) -> ValResult<()> {
277285
for item_result in iterator {
286+
if self.fail_fast && !self.errors.is_empty() {
287+
break;
288+
}
289+
278290
let (raw_key, value) = item_result?;
279291
let either_str = match raw_key
280292
.borrow_input()
@@ -354,6 +366,7 @@ impl Validator for TypedDictValidator {
354366
extra_behavior: self.extra_behavior,
355367
partial_last_key,
356368
allow_partial,
369+
fail_fast: self.fail_fast,
357370
})??;
358371
}
359372

0 commit comments

Comments
 (0)