Skip to content

Commit

Permalink
Add list fail-fast config option (#1322)
Browse files Browse the repository at this point in the history
  • Loading branch information
uriyyo authored Jun 24, 2024
1 parent a65f327 commit aba682e
Show file tree
Hide file tree
Showing 10 changed files with 254 additions and 3 deletions.
16 changes: 16 additions & 0 deletions python/pydantic_core/core_schema.py
Original file line number Diff line number Diff line change
Expand Up @@ -1399,6 +1399,7 @@ class ListSchema(TypedDict, total=False):
items_schema: CoreSchema
min_length: int
max_length: int
fail_fast: bool
strict: bool
ref: str
metadata: Any
Expand All @@ -1410,6 +1411,7 @@ def list_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: Any = None,
Expand All @@ -1430,6 +1432,7 @@ def list_schema(
items_schema: The value must be a list of items that match this schema
min_length: The value must be a list with at least this many items
max_length: The value must be a list with at most this many items
fail_fast: Stop validation on the first error
strict: The value must be a list with exactly this many items
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
Expand All @@ -1440,6 +1443,7 @@ def list_schema(
items_schema=items_schema,
min_length=min_length,
max_length=max_length,
fail_fast=fail_fast,
strict=strict,
ref=ref,
metadata=metadata,
Expand Down Expand Up @@ -1547,6 +1551,7 @@ class TupleSchema(TypedDict, total=False):
variadic_item_index: int
min_length: int
max_length: int
fail_fast: bool
strict: bool
ref: str
metadata: Any
Expand All @@ -1559,6 +1564,7 @@ def tuple_schema(
variadic_item_index: int | None = None,
min_length: int | None = None,
max_length: int | None = None,
fail_fast: bool | None = None,
strict: bool | None = None,
ref: str | None = None,
metadata: Any = None,
Expand All @@ -1583,6 +1589,7 @@ def tuple_schema(
variadic_item_index: The index of the schema in `items_schema` to be treated as variadic (following PEP 646)
min_length: The value must be a tuple with at least this many items
max_length: The value must be a tuple with at most this many items
fail_fast: Stop validation on the first error
strict: The value must be a tuple with exactly this many items
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
Expand All @@ -1594,6 +1601,7 @@ def tuple_schema(
variadic_item_index=variadic_item_index,
min_length=min_length,
max_length=max_length,
fail_fast=fail_fast,
strict=strict,
ref=ref,
metadata=metadata,
Expand All @@ -1606,6 +1614,7 @@ class SetSchema(TypedDict, total=False):
items_schema: CoreSchema
min_length: int
max_length: int
fail_fast: bool
strict: bool
ref: str
metadata: Any
Expand All @@ -1617,6 +1626,7 @@ def set_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: Any = None,
Expand All @@ -1639,6 +1649,7 @@ def set_schema(
items_schema: The value must be a set with items that match this schema
min_length: The value must be a set with at least this many items
max_length: The value must be a set with at most this many items
fail_fast: Stop validation on the first error
strict: The value must be a set with exactly this many items
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
Expand All @@ -1649,6 +1660,7 @@ def set_schema(
items_schema=items_schema,
min_length=min_length,
max_length=max_length,
fail_fast=fail_fast,
strict=strict,
ref=ref,
metadata=metadata,
Expand All @@ -1661,6 +1673,7 @@ class FrozenSetSchema(TypedDict, total=False):
items_schema: CoreSchema
min_length: int
max_length: int
fail_fast: bool
strict: bool
ref: str
metadata: Any
Expand All @@ -1672,6 +1685,7 @@ def frozenset_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: Any = None,
Expand All @@ -1694,6 +1708,7 @@ def frozenset_schema(
items_schema: The value must be a frozenset with items that match this schema
min_length: The value must be a frozenset with at least this many items
max_length: The value must be a frozenset with at most this many items
fail_fast: Stop validation on the first error
strict: The value must be a frozenset with exactly this many items
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
Expand All @@ -1704,6 +1719,7 @@ def frozenset_schema(
items_schema=items_schema,
min_length=min_length,
max_length=max_length,
fail_fast=fail_fast,
strict=strict,
ref=ref,
metadata=metadata,
Expand Down
8 changes: 8 additions & 0 deletions src/input/return_enums.rs
Original file line number Diff line number Diff line change
Expand Up @@ -124,6 +124,7 @@ pub(crate) fn validate_iter_to_vec<'py>(
mut max_length_check: MaxLengthCheck<'_, impl Input<'py> + ?Sized>,
validator: &CombinedValidator,
state: &mut ValidationState<'_, 'py>,
fail_fast: bool,
) -> ValResult<Vec<PyObject>> {
let mut output: Vec<PyObject> = Vec::with_capacity(capacity);
let mut errors: Vec<ValLineError> = Vec::new();
Expand All @@ -137,6 +138,9 @@ pub(crate) fn validate_iter_to_vec<'py>(
Err(ValError::LineErrors(line_errors)) => {
max_length_check.incr()?;
errors.extend(line_errors.into_iter().map(|err| err.with_outer_location(index)));
if fail_fast {
break;
}
}
Err(ValError::Omit) => (),
Err(err) => return Err(err),
Expand Down Expand Up @@ -190,6 +194,7 @@ pub(crate) fn validate_iter_to_set<'py>(
max_length: Option<usize>,
validator: &CombinedValidator,
state: &mut ValidationState<'_, 'py>,
fail_fast: bool,
) -> ValResult<()> {
let mut errors: Vec<ValLineError> = Vec::new();
for (index, item_result) in iter.enumerate() {
Expand Down Expand Up @@ -220,6 +225,9 @@ pub(crate) fn validate_iter_to_set<'py>(
Err(ValError::Omit) => (),
Err(err) => return Err(err),
}
if fail_fast && !errors.is_empty() {
break;
}
}

if errors.is_empty() {
Expand Down
4 changes: 4 additions & 0 deletions src/validators/frozenset.rs
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ pub struct FrozenSetValidator {
min_length: Option<usize>,
max_length: Option<usize>,
name: String,
fail_fast: bool,
}

impl BuildValidator for FrozenSetValidator {
Expand All @@ -42,6 +43,7 @@ impl Validator for FrozenSetValidator {
max_length: self.max_length,
item_validator: &self.item_validator,
state,
fail_fast: self.fail_fast,
})??;
min_length_check!(input, "Frozenset", self.min_length, f_set);
Ok(f_set.into_py(py))
Expand All @@ -59,6 +61,7 @@ struct ValidateToFrozenSet<'a, 's, 'py, I: Input<'py> + ?Sized> {
max_length: Option<usize>,
item_validator: &'a CombinedValidator,
state: &'a mut ValidationState<'s, 'py>,
fail_fast: bool,
}

impl<'py, T, I> ConsumeIterator<PyResult<T>> for ValidateToFrozenSet<'_, '_, 'py, I>
Expand All @@ -77,6 +80,7 @@ where
self.max_length,
self.item_validator,
self.state,
self.fail_fast,
)
}
}
5 changes: 5 additions & 0 deletions src/validators/list.rs
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ pub struct ListValidator {
min_length: Option<usize>,
max_length: Option<usize>,
name: OnceLock<String>,
fail_fast: bool,
}

pub fn get_items_schema(
Expand Down Expand Up @@ -109,6 +110,7 @@ impl BuildValidator for ListValidator {
min_length: schema.get_as(pyo3::intern!(py, "min_length"))?,
max_length: schema.get_as(pyo3::intern!(py, "max_length"))?,
name: OnceLock::new(),
fail_fast: schema.get_as(pyo3::intern!(py, "fail_fast"))?.unwrap_or(false),
}
.into())
}
Expand All @@ -135,6 +137,7 @@ impl Validator for ListValidator {
field_type: "List",
item_validator: v,
state,
fail_fast: self.fail_fast,
})??,
None => {
if let Some(py_list) = seq.as_py_list() {
Expand Down Expand Up @@ -184,6 +187,7 @@ struct ValidateToVec<'a, 's, 'py, I: Input<'py> + ?Sized> {
field_type: &'static str,
item_validator: &'a CombinedValidator,
state: &'a mut ValidationState<'s, 'py>,
fail_fast: bool,
}

// pretty arbitrary default capacity when creating vecs from iteration
Expand All @@ -204,6 +208,7 @@ where
max_length_check,
self.item_validator,
self.state,
self.fail_fast,
)
}
}
Expand Down
5 changes: 5 additions & 0 deletions src/validators/set.rs
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ pub struct SetValidator {
min_length: Option<usize>,
max_length: Option<usize>,
name: String,
fail_fast: bool,
}

macro_rules! set_build {
Expand Down Expand Up @@ -42,6 +43,7 @@ macro_rules! set_build {
min_length: schema.get_as(pyo3::intern!(py, "min_length"))?,
max_length,
name,
fail_fast: schema.get_as(pyo3::intern!(py, "fail_fast"))?.unwrap_or(false),
}
.into())
}
Expand Down Expand Up @@ -72,6 +74,7 @@ impl Validator for SetValidator {
max_length: self.max_length,
item_validator: &self.item_validator,
state,
fail_fast: self.fail_fast,
})??;
min_length_check!(input, "Set", self.min_length, set);
Ok(set.into_py(py))
Expand All @@ -89,6 +92,7 @@ struct ValidateToSet<'a, 's, 'py, I: Input<'py> + ?Sized> {
max_length: Option<usize>,
item_validator: &'a CombinedValidator,
state: &'a mut ValidationState<'s, 'py>,
fail_fast: bool,
}

impl<'py, T, I> ConsumeIterator<PyResult<T>> for ValidateToSet<'_, '_, 'py, I>
Expand All @@ -107,6 +111,7 @@ where
self.max_length,
self.item_validator,
self.state,
self.fail_fast,
)
}
}
25 changes: 25 additions & 0 deletions src/validators/tuple.rs
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ pub struct TupleValidator {
min_length: Option<usize>,
max_length: Option<usize>,
name: String,
fail_fast: bool,
}

impl BuildValidator for TupleValidator {
Expand Down Expand Up @@ -50,6 +51,7 @@ impl BuildValidator for TupleValidator {
min_length: schema.get_as(intern!(py, "min_length"))?,
max_length: schema.get_as(intern!(py, "max_length"))?,
name,
fail_fast: schema.get_as(intern!(py, "fail_fast"))?.unwrap_or(false),
}
.into())
}
Expand All @@ -69,6 +71,7 @@ impl TupleValidator {
item_validators: &[CombinedValidator],
collection_iter: &mut NextCountingIterator<impl Iterator<Item = I>>,
actual_length: Option<usize>,
fail_fast: bool,
) -> ValResult<()> {
// Validate the head:
for validator in item_validators {
Expand All @@ -90,6 +93,9 @@ impl TupleValidator {
}
}
}
if fail_fast && !errors.is_empty() {
return Ok(());
}
}

Ok(())
Expand Down Expand Up @@ -128,8 +134,13 @@ impl TupleValidator {
head_validators,
collection_iter,
actual_length,
self.fail_fast,
)?;

if self.fail_fast && !errors.is_empty() {
return Ok(output);
}

let n_tail_validators = tail_validators.len();
if n_tail_validators == 0 {
for (index, input_item) in collection_iter {
Expand All @@ -141,6 +152,10 @@ impl TupleValidator {
Err(ValError::Omit) => (),
Err(err) => return Err(err),
}

if self.fail_fast && !errors.is_empty() {
return Ok(output);
}
}
} else {
// Populate a buffer with the first n_tail_validators items
Expand Down Expand Up @@ -172,6 +187,10 @@ impl TupleValidator {
Err(ValError::Omit) => (),
Err(err) => return Err(err),
}

if self.fail_fast && !errors.is_empty() {
return Ok(output);
}
}

// Validate the buffered items using the tail validators
Expand All @@ -184,6 +203,7 @@ impl TupleValidator {
tail_validators,
&mut NextCountingIterator::new(tail_buffer.into_iter(), index),
actual_length,
self.fail_fast,
)?;
}
} else {
Expand All @@ -197,8 +217,13 @@ impl TupleValidator {
&self.validators,
collection_iter,
actual_length,
self.fail_fast,
)?;

if self.fail_fast && !errors.is_empty() {
return Ok(output);
}

// Generate an error if there are any extra items:
if collection_iter.next().is_some() {
return Err(ValError::new(
Expand Down
Loading

0 comments on commit aba682e

Please sign in to comment.