Skip to content

Commit aba682e

Browse files
authored
Add list fail-fast config option (#1322)
1 parent a65f327 commit aba682e

File tree

10 files changed

+254
-3
lines changed

10 files changed

+254
-3
lines changed

python/pydantic_core/core_schema.py

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1399,6 +1399,7 @@ class ListSchema(TypedDict, total=False):
13991399
items_schema: CoreSchema
14001400
min_length: int
14011401
max_length: int
1402+
fail_fast: bool
14021403
strict: bool
14031404
ref: str
14041405
metadata: Any
@@ -1410,6 +1411,7 @@ def list_schema(
14101411
*,
14111412
min_length: int | None = None,
14121413
max_length: int | None = None,
1414+
fail_fast: bool | None = None,
14131415
strict: bool | None = None,
14141416
ref: str | None = None,
14151417
metadata: Any = None,
@@ -1430,6 +1432,7 @@ def list_schema(
14301432
items_schema: The value must be a list of items that match this schema
14311433
min_length: The value must be a list with at least this many items
14321434
max_length: The value must be a list with at most this many items
1435+
fail_fast: Stop validation on the first error
14331436
strict: The value must be a list with exactly this many items
14341437
ref: optional unique identifier of the schema, used to reference the schema in other places
14351438
metadata: Any other information you want to include with the schema, not used by pydantic-core
@@ -1440,6 +1443,7 @@ def list_schema(
14401443
items_schema=items_schema,
14411444
min_length=min_length,
14421445
max_length=max_length,
1446+
fail_fast=fail_fast,
14431447
strict=strict,
14441448
ref=ref,
14451449
metadata=metadata,
@@ -1547,6 +1551,7 @@ class TupleSchema(TypedDict, total=False):
15471551
variadic_item_index: int
15481552
min_length: int
15491553
max_length: int
1554+
fail_fast: bool
15501555
strict: bool
15511556
ref: str
15521557
metadata: Any
@@ -1559,6 +1564,7 @@ def tuple_schema(
15591564
variadic_item_index: int | None = None,
15601565
min_length: int | None = None,
15611566
max_length: int | None = None,
1567+
fail_fast: bool | None = None,
15621568
strict: bool | None = None,
15631569
ref: str | None = None,
15641570
metadata: Any = None,
@@ -1583,6 +1589,7 @@ def tuple_schema(
15831589
variadic_item_index: The index of the schema in `items_schema` to be treated as variadic (following PEP 646)
15841590
min_length: The value must be a tuple with at least this many items
15851591
max_length: The value must be a tuple with at most this many items
1592+
fail_fast: Stop validation on the first error
15861593
strict: The value must be a tuple with exactly this many items
15871594
ref: Optional unique identifier of the schema, used to reference the schema in other places
15881595
metadata: Any other information you want to include with the schema, not used by pydantic-core
@@ -1594,6 +1601,7 @@ def tuple_schema(
15941601
variadic_item_index=variadic_item_index,
15951602
min_length=min_length,
15961603
max_length=max_length,
1604+
fail_fast=fail_fast,
15971605
strict=strict,
15981606
ref=ref,
15991607
metadata=metadata,
@@ -1606,6 +1614,7 @@ class SetSchema(TypedDict, total=False):
16061614
items_schema: CoreSchema
16071615
min_length: int
16081616
max_length: int
1617+
fail_fast: bool
16091618
strict: bool
16101619
ref: str
16111620
metadata: Any
@@ -1617,6 +1626,7 @@ def set_schema(
16171626
*,
16181627
min_length: int | None = None,
16191628
max_length: int | None = None,
1629+
fail_fast: bool | None = None,
16201630
strict: bool | None = None,
16211631
ref: str | None = None,
16221632
metadata: Any = None,
@@ -1639,6 +1649,7 @@ def set_schema(
16391649
items_schema: The value must be a set with items that match this schema
16401650
min_length: The value must be a set with at least this many items
16411651
max_length: The value must be a set with at most this many items
1652+
fail_fast: Stop validation on the first error
16421653
strict: The value must be a set with exactly this many items
16431654
ref: optional unique identifier of the schema, used to reference the schema in other places
16441655
metadata: Any other information you want to include with the schema, not used by pydantic-core
@@ -1649,6 +1660,7 @@ def set_schema(
16491660
items_schema=items_schema,
16501661
min_length=min_length,
16511662
max_length=max_length,
1663+
fail_fast=fail_fast,
16521664
strict=strict,
16531665
ref=ref,
16541666
metadata=metadata,
@@ -1661,6 +1673,7 @@ class FrozenSetSchema(TypedDict, total=False):
16611673
items_schema: CoreSchema
16621674
min_length: int
16631675
max_length: int
1676+
fail_fast: bool
16641677
strict: bool
16651678
ref: str
16661679
metadata: Any
@@ -1672,6 +1685,7 @@ def frozenset_schema(
16721685
*,
16731686
min_length: int | None = None,
16741687
max_length: int | None = None,
1688+
fail_fast: bool | None = None,
16751689
strict: bool | None = None,
16761690
ref: str | None = None,
16771691
metadata: Any = None,
@@ -1694,6 +1708,7 @@ def frozenset_schema(
16941708
items_schema: The value must be a frozenset with items that match this schema
16951709
min_length: The value must be a frozenset with at least this many items
16961710
max_length: The value must be a frozenset with at most this many items
1711+
fail_fast: Stop validation on the first error
16971712
strict: The value must be a frozenset with exactly this many items
16981713
ref: optional unique identifier of the schema, used to reference the schema in other places
16991714
metadata: Any other information you want to include with the schema, not used by pydantic-core
@@ -1704,6 +1719,7 @@ def frozenset_schema(
17041719
items_schema=items_schema,
17051720
min_length=min_length,
17061721
max_length=max_length,
1722+
fail_fast=fail_fast,
17071723
strict=strict,
17081724
ref=ref,
17091725
metadata=metadata,

src/input/return_enums.rs

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -124,6 +124,7 @@ pub(crate) fn validate_iter_to_vec<'py>(
124124
mut max_length_check: MaxLengthCheck<'_, impl Input<'py> + ?Sized>,
125125
validator: &CombinedValidator,
126126
state: &mut ValidationState<'_, 'py>,
127+
fail_fast: bool,
127128
) -> ValResult<Vec<PyObject>> {
128129
let mut output: Vec<PyObject> = Vec::with_capacity(capacity);
129130
let mut errors: Vec<ValLineError> = Vec::new();
@@ -137,6 +138,9 @@ pub(crate) fn validate_iter_to_vec<'py>(
137138
Err(ValError::LineErrors(line_errors)) => {
138139
max_length_check.incr()?;
139140
errors.extend(line_errors.into_iter().map(|err| err.with_outer_location(index)));
141+
if fail_fast {
142+
break;
143+
}
140144
}
141145
Err(ValError::Omit) => (),
142146
Err(err) => return Err(err),
@@ -190,6 +194,7 @@ pub(crate) fn validate_iter_to_set<'py>(
190194
max_length: Option<usize>,
191195
validator: &CombinedValidator,
192196
state: &mut ValidationState<'_, 'py>,
197+
fail_fast: bool,
193198
) -> ValResult<()> {
194199
let mut errors: Vec<ValLineError> = Vec::new();
195200
for (index, item_result) in iter.enumerate() {
@@ -220,6 +225,9 @@ pub(crate) fn validate_iter_to_set<'py>(
220225
Err(ValError::Omit) => (),
221226
Err(err) => return Err(err),
222227
}
228+
if fail_fast && !errors.is_empty() {
229+
break;
230+
}
223231
}
224232

225233
if errors.is_empty() {

src/validators/frozenset.rs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ pub struct FrozenSetValidator {
1717
min_length: Option<usize>,
1818
max_length: Option<usize>,
1919
name: String,
20+
fail_fast: bool,
2021
}
2122

2223
impl BuildValidator for FrozenSetValidator {
@@ -42,6 +43,7 @@ impl Validator for FrozenSetValidator {
4243
max_length: self.max_length,
4344
item_validator: &self.item_validator,
4445
state,
46+
fail_fast: self.fail_fast,
4547
})??;
4648
min_length_check!(input, "Frozenset", self.min_length, f_set);
4749
Ok(f_set.into_py(py))
@@ -59,6 +61,7 @@ struct ValidateToFrozenSet<'a, 's, 'py, I: Input<'py> + ?Sized> {
5961
max_length: Option<usize>,
6062
item_validator: &'a CombinedValidator,
6163
state: &'a mut ValidationState<'s, 'py>,
64+
fail_fast: bool,
6265
}
6366

6467
impl<'py, T, I> ConsumeIterator<PyResult<T>> for ValidateToFrozenSet<'_, '_, 'py, I>
@@ -77,6 +80,7 @@ where
7780
self.max_length,
7881
self.item_validator,
7982
self.state,
83+
self.fail_fast,
8084
)
8185
}
8286
}

src/validators/list.rs

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ pub struct ListValidator {
1818
min_length: Option<usize>,
1919
max_length: Option<usize>,
2020
name: OnceLock<String>,
21+
fail_fast: bool,
2122
}
2223

2324
pub fn get_items_schema(
@@ -109,6 +110,7 @@ impl BuildValidator for ListValidator {
109110
min_length: schema.get_as(pyo3::intern!(py, "min_length"))?,
110111
max_length: schema.get_as(pyo3::intern!(py, "max_length"))?,
111112
name: OnceLock::new(),
113+
fail_fast: schema.get_as(pyo3::intern!(py, "fail_fast"))?.unwrap_or(false),
112114
}
113115
.into())
114116
}
@@ -135,6 +137,7 @@ impl Validator for ListValidator {
135137
field_type: "List",
136138
item_validator: v,
137139
state,
140+
fail_fast: self.fail_fast,
138141
})??,
139142
None => {
140143
if let Some(py_list) = seq.as_py_list() {
@@ -184,6 +187,7 @@ struct ValidateToVec<'a, 's, 'py, I: Input<'py> + ?Sized> {
184187
field_type: &'static str,
185188
item_validator: &'a CombinedValidator,
186189
state: &'a mut ValidationState<'s, 'py>,
190+
fail_fast: bool,
187191
}
188192

189193
// pretty arbitrary default capacity when creating vecs from iteration
@@ -204,6 +208,7 @@ where
204208
max_length_check,
205209
self.item_validator,
206210
self.state,
211+
self.fail_fast,
207212
)
208213
}
209214
}

src/validators/set.rs

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ pub struct SetValidator {
1515
min_length: Option<usize>,
1616
max_length: Option<usize>,
1717
name: String,
18+
fail_fast: bool,
1819
}
1920

2021
macro_rules! set_build {
@@ -42,6 +43,7 @@ macro_rules! set_build {
4243
min_length: schema.get_as(pyo3::intern!(py, "min_length"))?,
4344
max_length,
4445
name,
46+
fail_fast: schema.get_as(pyo3::intern!(py, "fail_fast"))?.unwrap_or(false),
4547
}
4648
.into())
4749
}
@@ -72,6 +74,7 @@ impl Validator for SetValidator {
7274
max_length: self.max_length,
7375
item_validator: &self.item_validator,
7476
state,
77+
fail_fast: self.fail_fast,
7578
})??;
7679
min_length_check!(input, "Set", self.min_length, set);
7780
Ok(set.into_py(py))
@@ -89,6 +92,7 @@ struct ValidateToSet<'a, 's, 'py, I: Input<'py> + ?Sized> {
8992
max_length: Option<usize>,
9093
item_validator: &'a CombinedValidator,
9194
state: &'a mut ValidationState<'s, 'py>,
95+
fail_fast: bool,
9296
}
9397

9498
impl<'py, T, I> ConsumeIterator<PyResult<T>> for ValidateToSet<'_, '_, 'py, I>
@@ -107,6 +111,7 @@ where
107111
self.max_length,
108112
self.item_validator,
109113
self.state,
114+
self.fail_fast,
110115
)
111116
}
112117
}

src/validators/tuple.rs

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ pub struct TupleValidator {
1919
min_length: Option<usize>,
2020
max_length: Option<usize>,
2121
name: String,
22+
fail_fast: bool,
2223
}
2324

2425
impl BuildValidator for TupleValidator {
@@ -50,6 +51,7 @@ impl BuildValidator for TupleValidator {
5051
min_length: schema.get_as(intern!(py, "min_length"))?,
5152
max_length: schema.get_as(intern!(py, "max_length"))?,
5253
name,
54+
fail_fast: schema.get_as(intern!(py, "fail_fast"))?.unwrap_or(false),
5355
}
5456
.into())
5557
}
@@ -69,6 +71,7 @@ impl TupleValidator {
6971
item_validators: &[CombinedValidator],
7072
collection_iter: &mut NextCountingIterator<impl Iterator<Item = I>>,
7173
actual_length: Option<usize>,
74+
fail_fast: bool,
7275
) -> ValResult<()> {
7376
// Validate the head:
7477
for validator in item_validators {
@@ -90,6 +93,9 @@ impl TupleValidator {
9093
}
9194
}
9295
}
96+
if fail_fast && !errors.is_empty() {
97+
return Ok(());
98+
}
9399
}
94100

95101
Ok(())
@@ -128,8 +134,13 @@ impl TupleValidator {
128134
head_validators,
129135
collection_iter,
130136
actual_length,
137+
self.fail_fast,
131138
)?;
132139

140+
if self.fail_fast && !errors.is_empty() {
141+
return Ok(output);
142+
}
143+
133144
let n_tail_validators = tail_validators.len();
134145
if n_tail_validators == 0 {
135146
for (index, input_item) in collection_iter {
@@ -141,6 +152,10 @@ impl TupleValidator {
141152
Err(ValError::Omit) => (),
142153
Err(err) => return Err(err),
143154
}
155+
156+
if self.fail_fast && !errors.is_empty() {
157+
return Ok(output);
158+
}
144159
}
145160
} else {
146161
// Populate a buffer with the first n_tail_validators items
@@ -172,6 +187,10 @@ impl TupleValidator {
172187
Err(ValError::Omit) => (),
173188
Err(err) => return Err(err),
174189
}
190+
191+
if self.fail_fast && !errors.is_empty() {
192+
return Ok(output);
193+
}
175194
}
176195

177196
// Validate the buffered items using the tail validators
@@ -184,6 +203,7 @@ impl TupleValidator {
184203
tail_validators,
185204
&mut NextCountingIterator::new(tail_buffer.into_iter(), index),
186205
actual_length,
206+
self.fail_fast,
187207
)?;
188208
}
189209
} else {
@@ -197,8 +217,13 @@ impl TupleValidator {
197217
&self.validators,
198218
collection_iter,
199219
actual_length,
220+
self.fail_fast,
200221
)?;
201222

223+
if self.fail_fast && !errors.is_empty() {
224+
return Ok(output);
225+
}
226+
202227
// Generate an error if there are any extra items:
203228
if collection_iter.next().is_some() {
204229
return Err(ValError::new(

0 commit comments

Comments
 (0)