Skip to content

Commit 8991709

Browse files
uriyyodavidhewitt
andauthored
Add fail-fast for dicts (#1543)
Co-authored-by: David Hewitt <[email protected]>
1 parent 1973ee3 commit 8991709

File tree

3 files changed

+82
-0
lines changed

3 files changed

+82
-0
lines changed

python/pydantic_core/core_schema.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1939,6 +1939,7 @@ class DictSchema(TypedDict, total=False):
19391939
values_schema: CoreSchema # default: AnySchema
19401940
min_length: int
19411941
max_length: int
1942+
fail_fast: bool
19421943
strict: bool
19431944
ref: str
19441945
metadata: dict[str, Any]
@@ -1951,6 +1952,7 @@ def dict_schema(
19511952
*,
19521953
min_length: int | None = None,
19531954
max_length: int | None = None,
1955+
fail_fast: bool | None = None,
19541956
strict: bool | None = None,
19551957
ref: str | None = None,
19561958
metadata: dict[str, Any] | None = None,
@@ -1974,6 +1976,7 @@ def dict_schema(
19741976
values_schema: The value must be a dict with values that match this schema
19751977
min_length: The value must be a dict with at least this many items
19761978
max_length: The value must be a dict with at most this many items
1979+
fail_fast: Stop validation on the first error
19771980
strict: Whether the keys and values should be validated with strict mode
19781981
ref: optional unique identifier of the schema, used to reference the schema in other places
19791982
metadata: Any other information you want to include with the schema, not used by pydantic-core
@@ -1985,6 +1988,7 @@ def dict_schema(
19851988
values_schema=values_schema,
19861989
min_length=min_length,
19871990
max_length=max_length,
1991+
fail_fast=fail_fast,
19881992
strict=strict,
19891993
ref=ref,
19901994
metadata=metadata,

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
}

tests/validators/test_dict.py

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -255,3 +255,61 @@ def test_json_dict_complex_key():
255255
assert v.validate_json('{"1+2j": 2, "infj": 4}') == {complex(1, 2): 2, complex(0, float('inf')): 4}
256256
with pytest.raises(ValidationError, match='Input should be a valid complex string'):
257257
v.validate_json('{"1+2j": 2, "": 4}') == {complex(1, 2): 2, complex(0, float('inf')): 4}
258+
259+
260+
@pytest.mark.parametrize(
261+
('fail_fast', 'expected'),
262+
[
263+
pytest.param(
264+
True,
265+
[
266+
{
267+
'type': 'int_parsing',
268+
'loc': ('a', '[key]'),
269+
'msg': 'Input should be a valid integer, unable to parse string as an integer',
270+
'input': 'a',
271+
},
272+
],
273+
id='fail_fast',
274+
),
275+
pytest.param(
276+
False,
277+
[
278+
{
279+
'type': 'int_parsing',
280+
'loc': ('a', '[key]'),
281+
'msg': 'Input should be a valid integer, unable to parse string as an integer',
282+
'input': 'a',
283+
},
284+
{
285+
'type': 'int_parsing',
286+
'loc': ('a',),
287+
'msg': 'Input should be a valid integer, unable to parse string as an integer',
288+
'input': 'b',
289+
},
290+
{
291+
'type': 'int_parsing',
292+
'loc': ('c', '[key]'),
293+
'msg': 'Input should be a valid integer, unable to parse string as an integer',
294+
'input': 'c',
295+
},
296+
{
297+
'type': 'int_parsing',
298+
'loc': ('c',),
299+
'msg': 'Input should be a valid integer, unable to parse string as an integer',
300+
'input': 'd',
301+
},
302+
],
303+
id='not_fail_fast',
304+
),
305+
],
306+
)
307+
def test_dict_fail_fast(fail_fast, expected):
308+
v = SchemaValidator(
309+
{'type': 'dict', 'keys_schema': {'type': 'int'}, 'values_schema': {'type': 'int'}, 'fail_fast': fail_fast}
310+
)
311+
312+
with pytest.raises(ValidationError) as exc_info:
313+
v.validate_python({'a': 'b', 'c': 'd'})
314+
315+
assert exc_info.value.errors(include_url=False) == expected

0 commit comments

Comments
 (0)