Skip to content

Commit 27db075

Browse files
committed
Add tests to cover fail-fast feature
1 parent 0c85ada commit 27db075

File tree

5 files changed

+160
-2
lines changed

5 files changed

+160
-2
lines changed

python/pydantic_core/core_schema.py

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1551,6 +1551,7 @@ class TupleSchema(TypedDict, total=False):
15511551
variadic_item_index: int
15521552
min_length: int
15531553
max_length: int
1554+
fail_fast: bool
15541555
strict: bool
15551556
ref: str
15561557
metadata: Any
@@ -1563,6 +1564,7 @@ def tuple_schema(
15631564
variadic_item_index: int | None = None,
15641565
min_length: int | None = None,
15651566
max_length: int | None = None,
1567+
fail_fast: bool | None = None,
15661568
strict: bool | None = None,
15671569
ref: str | None = None,
15681570
metadata: Any = None,
@@ -1587,6 +1589,7 @@ def tuple_schema(
15871589
variadic_item_index: The index of the schema in `items_schema` to be treated as variadic (following PEP 646)
15881590
min_length: The value must be a tuple with at least this many items
15891591
max_length: The value must be a tuple with at most this many items
1592+
fail_fast: Stop validation on the first error
15901593
strict: The value must be a tuple with exactly this many items
15911594
ref: Optional unique identifier of the schema, used to reference the schema in other places
15921595
metadata: Any other information you want to include with the schema, not used by pydantic-core
@@ -1598,6 +1601,7 @@ def tuple_schema(
15981601
variadic_item_index=variadic_item_index,
15991602
min_length=min_length,
16001603
max_length=max_length,
1604+
fail_fast=fail_fast,
16011605
strict=strict,
16021606
ref=ref,
16031607
metadata=metadata,
@@ -1610,6 +1614,7 @@ class SetSchema(TypedDict, total=False):
16101614
items_schema: CoreSchema
16111615
min_length: int
16121616
max_length: int
1617+
fail_fast: bool
16131618
strict: bool
16141619
ref: str
16151620
metadata: Any
@@ -1621,6 +1626,7 @@ def set_schema(
16211626
*,
16221627
min_length: int | None = None,
16231628
max_length: int | None = None,
1629+
fail_fast: bool | None = None,
16241630
strict: bool | None = None,
16251631
ref: str | None = None,
16261632
metadata: Any = None,
@@ -1643,6 +1649,7 @@ def set_schema(
16431649
items_schema: The value must be a set with items that match this schema
16441650
min_length: The value must be a set with at least this many items
16451651
max_length: The value must be a set with at most this many items
1652+
fail_fast: Stop validation on the first error
16461653
strict: The value must be a set with exactly this many items
16471654
ref: optional unique identifier of the schema, used to reference the schema in other places
16481655
metadata: Any other information you want to include with the schema, not used by pydantic-core
@@ -1653,6 +1660,7 @@ def set_schema(
16531660
items_schema=items_schema,
16541661
min_length=min_length,
16551662
max_length=max_length,
1663+
fail_fast=fail_fast,
16561664
strict=strict,
16571665
ref=ref,
16581666
metadata=metadata,
@@ -1665,6 +1673,7 @@ class FrozenSetSchema(TypedDict, total=False):
16651673
items_schema: CoreSchema
16661674
min_length: int
16671675
max_length: int
1676+
fail_fast: bool
16681677
strict: bool
16691678
ref: str
16701679
metadata: Any
@@ -1676,6 +1685,7 @@ def frozenset_schema(
16761685
*,
16771686
min_length: int | None = None,
16781687
max_length: int | None = None,
1688+
fail_fast: bool | None = None,
16791689
strict: bool | None = None,
16801690
ref: str | None = None,
16811691
metadata: Any = None,
@@ -1698,6 +1708,7 @@ def frozenset_schema(
16981708
items_schema: The value must be a frozenset with items that match this schema
16991709
min_length: The value must be a frozenset with at least this many items
17001710
max_length: The value must be a frozenset with at most this many items
1711+
fail_fast: Stop validation on the first error
17011712
strict: The value must be a frozenset with exactly this many items
17021713
ref: optional unique identifier of the schema, used to reference the schema in other places
17031714
metadata: Any other information you want to include with the schema, not used by pydantic-core
@@ -1708,6 +1719,7 @@ def frozenset_schema(
17081719
items_schema=items_schema,
17091720
min_length=min_length,
17101721
max_length=max_length,
1722+
fail_fast=fail_fast,
17111723
strict=strict,
17121724
ref=ref,
17131725
metadata=metadata,

src/validators/tuple.rs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -220,6 +220,10 @@ impl TupleValidator {
220220
self.fail_fast,
221221
)?;
222222

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

tests/validators/test_frozenset.py

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -297,3 +297,47 @@ def test_frozenset_from_dict_items(input_value, items_schema, expected):
297297
output = v.validate_python(input_value)
298298
assert isinstance(output, frozenset)
299299
assert output == expected
300+
301+
302+
@pytest.mark.parametrize(
303+
'fail_fast,expected',
304+
[
305+
pytest.param(
306+
True,
307+
[
308+
{
309+
'type': 'int_parsing',
310+
'loc': (1,),
311+
'msg': 'Input should be a valid integer, unable to parse string as an integer',
312+
'input': 'not-num',
313+
},
314+
],
315+
id='fail_fast',
316+
),
317+
pytest.param(
318+
False,
319+
[
320+
{
321+
'type': 'int_parsing',
322+
'loc': (1,),
323+
'msg': 'Input should be a valid integer, unable to parse string as an integer',
324+
'input': 'not-num',
325+
},
326+
{
327+
'type': 'int_parsing',
328+
'loc': (2,),
329+
'msg': 'Input should be a valid integer, unable to parse string as an integer',
330+
'input': 'again',
331+
},
332+
],
333+
id='not_fail_fast',
334+
),
335+
],
336+
)
337+
def test_frozenset_fail_fast(fail_fast, expected):
338+
v = SchemaValidator({'type': 'frozenset', 'items_schema': {'type': 'int'}, 'fail_fast': fail_fast})
339+
340+
with pytest.raises(ValidationError) as exc_info:
341+
v.validate_python([1, 'not-num', 'again'])
342+
343+
assert exc_info.value.errors(include_url=False) == expected

tests/validators/test_set.py

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -277,3 +277,47 @@ def test_set_any(input_value, expected):
277277
output = v.validate_python(input_value)
278278
assert output == expected
279279
assert isinstance(output, set)
280+
281+
282+
@pytest.mark.parametrize(
283+
'fail_fast,expected',
284+
[
285+
pytest.param(
286+
True,
287+
[
288+
{
289+
'type': 'int_parsing',
290+
'loc': (1,),
291+
'msg': 'Input should be a valid integer, unable to parse string as an integer',
292+
'input': 'not-num',
293+
},
294+
],
295+
id='fail_fast',
296+
),
297+
pytest.param(
298+
False,
299+
[
300+
{
301+
'type': 'int_parsing',
302+
'loc': (1,),
303+
'msg': 'Input should be a valid integer, unable to parse string as an integer',
304+
'input': 'not-num',
305+
},
306+
{
307+
'type': 'int_parsing',
308+
'loc': (2,),
309+
'msg': 'Input should be a valid integer, unable to parse string as an integer',
310+
'input': 'again',
311+
},
312+
],
313+
id='not_fail_fast',
314+
),
315+
],
316+
)
317+
def test_set_fail_fast(fail_fast, expected):
318+
v = SchemaValidator({'type': 'set', 'items_schema': {'type': 'int'}, 'fail_fast': fail_fast})
319+
320+
with pytest.raises(ValidationError) as exc_info:
321+
v.validate_python([1, 'not-num', 'again'])
322+
323+
assert exc_info.value.errors(include_url=False) == expected

tests/validators/test_tuple.py

Lines changed: 56 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -58,8 +58,9 @@ def test_tuple_strict_passes_with_tuple(variadic_item_index, items, input_value,
5858
assert v.validate_python(input_value) == expected
5959

6060

61-
def test_empty_positional_tuple():
62-
v = SchemaValidator({'type': 'tuple', 'items_schema': []})
61+
@pytest.mark.parametrize('fail_fast', [True, False])
62+
def test_empty_positional_tuple(fail_fast):
63+
v = SchemaValidator({'type': 'tuple', 'items_schema': [], 'fail_fast': fail_fast})
6364
assert v.validate_python(()) == ()
6465
assert v.validate_python([]) == ()
6566
with pytest.raises(ValidationError) as exc_info:
@@ -493,3 +494,56 @@ def test_length_constraints_omit(input_value, expected):
493494
v.validate_python(input_value)
494495
else:
495496
assert v.validate_python(input_value) == expected
497+
498+
499+
@pytest.mark.parametrize(
500+
'fail_fast,expected',
501+
[
502+
pytest.param(
503+
True,
504+
[
505+
{
506+
'type': 'int_parsing',
507+
'loc': (1,),
508+
'msg': 'Input should be a valid integer, unable to parse string as an integer',
509+
'input': 'not-num',
510+
}
511+
],
512+
id='fail_fast',
513+
),
514+
pytest.param(
515+
False,
516+
[
517+
{
518+
'type': 'int_parsing',
519+
'loc': (1,),
520+
'msg': 'Input should be a valid integer, unable to parse string as an integer',
521+
'input': 'not-num',
522+
},
523+
{
524+
'type': 'float_parsing',
525+
'loc': (2,),
526+
'msg': 'Input should be a valid number, unable to parse string as a number',
527+
'input': 'again',
528+
},
529+
],
530+
id='not_fail_fast',
531+
),
532+
],
533+
)
534+
def test_tuple_fail_fast(fail_fast, expected):
535+
s = core_schema.tuple_schema(
536+
[
537+
core_schema.str_schema(),
538+
core_schema.int_schema(),
539+
core_schema.float_schema(),
540+
],
541+
variadic_item_index=None,
542+
fail_fast=fail_fast,
543+
)
544+
v = SchemaValidator(s)
545+
546+
with pytest.raises(ValidationError) as exc_info:
547+
v.validate_python(['str', 'not-num', 'again'])
548+
549+
assert exc_info.value.errors(include_url=False) == expected

0 commit comments

Comments
 (0)