diff --git a/python/pydantic_core/_pydantic_core.pyi b/python/pydantic_core/_pydantic_core.pyi index fabd92b3a..db29d5399 100644 --- a/python/pydantic_core/_pydantic_core.pyi +++ b/python/pydantic_core/_pydantic_core.pyi @@ -96,6 +96,7 @@ class SchemaValidator: from_attributes: bool | None = None, context: Any | None = None, self_instance: Any | None = None, + allow_partial: bool = False, ) -> Any: """ Validate a Python object against the schema and return the validated object. @@ -110,6 +111,8 @@ class SchemaValidator: [`info.context`][pydantic_core.core_schema.ValidationInfo.context]. self_instance: An instance of a model set attributes on from validation, this is used when running validation from the `__init__` method of a model. + allow_partial: Whether to allow partial validation, if `True` errors in the last element of sequences + and mappings are ignored. Raises: ValidationError: If validation fails. @@ -143,6 +146,7 @@ class SchemaValidator: strict: bool | None = None, context: Any | None = None, self_instance: Any | None = None, + allow_partial: bool = False, ) -> Any: """ Validate JSON data directly against the schema and return the validated Python object. @@ -160,6 +164,8 @@ class SchemaValidator: context: The context to use for validation, this is passed to functional validators as [`info.context`][pydantic_core.core_schema.ValidationInfo.context]. self_instance: An instance of a model set attributes on from validation. + allow_partial: Whether to allow partial validation, if `True` errors in the last element of sequences + and mappings are ignored. Raises: ValidationError: If validation fails or if the JSON data is invalid. @@ -168,7 +174,14 @@ class SchemaValidator: Returns: The validated Python object. """ - def validate_strings(self, input: _StringInput, *, strict: bool | None = None, context: Any | None = None) -> Any: + def validate_strings( + self, + input: _StringInput, + *, + strict: bool | None = None, + context: Any | None = None, + allow_partial: bool = False, + ) -> Any: """ Validate a string against the schema and return the validated Python object. @@ -181,6 +194,8 @@ class SchemaValidator: If `None`, the value of [`CoreConfig.strict`][pydantic_core.core_schema.CoreConfig] is used. context: The context to use for validation, this is passed to functional validators as [`info.context`][pydantic_core.core_schema.ValidationInfo.context]. + allow_partial: Whether to allow partial validation, if `True` errors in the last element of sequences + and mappings are ignored. Raises: ValidationError: If validation fails or if the JSON data is invalid. diff --git a/python/pydantic_core/core_schema.py b/python/pydantic_core/core_schema.py index 23e384af1..17e66f625 100644 --- a/python/pydantic_core/core_schema.py +++ b/python/pydantic_core/core_schema.py @@ -122,45 +122,58 @@ class CoreConfig(TypedDict, total=False): class SerializationInfo(Protocol): @property - def include(self) -> IncExCall: ... + def include(self) -> IncExCall: + ... @property - def exclude(self) -> IncExCall: ... + def exclude(self) -> IncExCall: + ... @property def context(self) -> Any | None: """Current serialization context.""" @property - def mode(self) -> str: ... + def mode(self) -> str: + ... @property - def by_alias(self) -> bool: ... + def by_alias(self) -> bool: + ... @property - def exclude_unset(self) -> bool: ... + def exclude_unset(self) -> bool: + ... @property - def exclude_defaults(self) -> bool: ... + def exclude_defaults(self) -> bool: + ... @property - def exclude_none(self) -> bool: ... + def exclude_none(self) -> bool: + ... @property - def serialize_as_any(self) -> bool: ... + def serialize_as_any(self) -> bool: + ... - def round_trip(self) -> bool: ... + def round_trip(self) -> bool: + ... - def mode_is_json(self) -> bool: ... + def mode_is_json(self) -> bool: + ... - def __str__(self) -> str: ... + def __str__(self) -> str: + ... - def __repr__(self) -> str: ... + def __repr__(self) -> str: + ... class FieldSerializationInfo(SerializationInfo, Protocol): @property - def field_name(self) -> str: ... + def field_name(self) -> str: + ... class ValidationInfo(Protocol): @@ -305,7 +318,8 @@ def plain_serializer_function_ser_schema( class SerializerFunctionWrapHandler(Protocol): # pragma: no cover - def __call__(self, input_value: Any, index_key: int | str | None = None, /) -> Any: ... + def __call__(self, input_value: Any, index_key: int | str | None = None, /) -> Any: + ... # (input_value: Any, serializer: SerializerFunctionWrapHandler, /) -> Any diff --git a/src/errors/line_error.rs b/src/errors/line_error.rs index c3d2b66dc..417f5e790 100644 --- a/src/errors/line_error.rs +++ b/src/errors/line_error.rs @@ -145,6 +145,14 @@ impl ValLineError { self.error_type = error_type; self } + + pub fn last_loc_item(&self) -> Option<&LocItem> { + match &self.location { + Location::Empty => None, + // first because order is reversed + Location::List(loc_items) => loc_items.first(), + } + } } #[cfg_attr(debug_assertions, derive(Debug))] diff --git a/src/errors/location.rs b/src/errors/location.rs index 07e1623d7..e9e20ab5d 100644 --- a/src/errors/location.rs +++ b/src/errors/location.rs @@ -12,7 +12,7 @@ use crate::lookup_key::{LookupPath, PathItem}; /// Used to store individual items of the error location, e.g. a string for key/field names /// or a number for array indices. -#[derive(Clone)] +#[derive(Clone, Eq, PartialEq)] #[cfg_attr(debug_assertions, derive(Debug))] pub enum LocItem { /// string type key, used to identify items from a dict or anything that implements `__getitem__` diff --git a/src/errors/mod.rs b/src/errors/mod.rs index ffdda90e3..2f0176695 100644 --- a/src/errors/mod.rs +++ b/src/errors/mod.rs @@ -1,3 +1,4 @@ +use crate::validators::ValidationState; use pyo3::prelude::*; mod line_error; @@ -30,3 +31,40 @@ pub fn py_err_string(py: Python, err: PyErr) -> String { Err(_) => "Unknown Error".to_string(), } } + +/// If we're in `allow_partial` mode, whether all errors occurred in the last element of the input. +pub fn sequence_valid_as_partial(state: &ValidationState, input_length: usize, errors: &[ValLineError]) -> bool { + if !state.extra().allow_partial { + return false; + } + // for the error to be in the last element, the index of all errors must be `input_length - 1` + let last_index = (input_length - 1) as i64; + errors.iter().all(|error| { + if let Some(LocItem::I(loc_index)) = error.last_loc_item() { + *loc_index == last_index + } else { + false + } + }) +} + +/// If we're in `allow_partial` mode, whether all errors occurred in the last value of the input. +pub fn mapping_valid_as_partial( + state: &ValidationState, + opt_last_key: Option>, + errors: &[ValLineError], +) -> bool { + if !state.extra().allow_partial { + return false; + } + let Some(last_key) = opt_last_key.map(Into::into) else { + return false; + }; + errors.iter().all(|error| { + if let Some(loc_item) = error.last_loc_item() { + loc_item == &last_key + } else { + false + } + }) +} diff --git a/src/input/input_abstract.rs b/src/input/input_abstract.rs index 01deea4bc..d8ae85f1e 100644 --- a/src/input/input_abstract.rs +++ b/src/input/input_abstract.rs @@ -236,6 +236,8 @@ pub trait ValidatedDict<'py> { &'a self, consumer: impl ConsumeIterator, Self::Item<'a>)>, Output = R>, ) -> ValResult; + // used in partial mode to check all errors occurred in the last key value pair + fn last_key(&self) -> Option>; } /// For validations from a list @@ -276,6 +278,9 @@ impl<'py> ValidatedDict<'py> for Never { ) -> ValResult { unreachable!() } + fn last_key(&self) -> Option> { + unreachable!() + } } impl<'py> ValidatedList<'py> for Never { diff --git a/src/input/input_json.rs b/src/input/input_json.rs index 7c77928d0..d69874613 100644 --- a/src/input/input_json.rs +++ b/src/input/input_json.rs @@ -516,6 +516,10 @@ impl<'py, 'data> ValidatedDict<'py> for &'_ JsonObject<'data> { ) -> ValResult { Ok(consumer.consume_iterator(LazyIndexMap::iter(self).map(|(k, v)| Ok((k.as_ref(), v))))) } + + fn last_key(&self) -> Option> { + self.keys().last().map(AsRef::as_ref) + } } impl<'a, 'py, 'data> ValidatedList<'py> for &'a JsonArray<'data> { diff --git a/src/input/input_python.rs b/src/input/input_python.rs index b9d95b8bf..9156bc14b 100644 --- a/src/input/input_python.rs +++ b/src/input/input_python.rs @@ -823,6 +823,14 @@ impl<'py> ValidatedDict<'py> for GenericPyMapping<'_, 'py> { Self::GetAttr(obj, _) => Ok(consumer.consume_iterator(iterate_attributes(obj)?)), } } + + fn last_key(&self) -> Option> { + match self { + Self::Dict(dict) => dict.keys().iter().last(), + Self::Mapping(mapping) => mapping.keys().ok()?.iter().ok()?.last()?.ok(), + Self::GetAttr(_, _) => None, + } + } } /// Container for all the collections (sized iterable containers) types, which diff --git a/src/input/input_string.rs b/src/input/input_string.rs index 1e6b14825..2ac218658 100644 --- a/src/input/input_string.rs +++ b/src/input/input_string.rs @@ -303,4 +303,12 @@ impl<'py> ValidatedDict<'py> for StringMappingDict<'py> { .map(|(key, val)| Ok((StringMapping::new_key(key)?, StringMapping::new_value(val)?))), )) } + + fn last_key(&self) -> Option> { + self.0 + .keys() + .iter() + .last() + .and_then(|key| StringMapping::new_key(key).ok()) + } } diff --git a/src/input/return_enums.rs b/src/input/return_enums.rs index 888dff714..6980917a3 100644 --- a/src/input/return_enums.rs +++ b/src/input/return_enums.rs @@ -17,7 +17,8 @@ use pyo3::types::{PyBytes, PyComplex, PyFloat, PyFrozenSet, PyIterator, PyMappin use serde::{ser::Error, Serialize, Serializer}; use crate::errors::{ - py_err_string, ErrorType, ErrorTypeDefaults, InputValue, ToErrorValue, ValError, ValLineError, ValResult, + py_err_string, sequence_valid_as_partial, ErrorType, ErrorTypeDefaults, InputValue, ToErrorValue, ValError, + ValLineError, ValResult, }; use crate::py_gc::PyGcTraverse; use crate::tools::{extract_i64, extract_int, new_py_string, py_err}; @@ -128,7 +129,9 @@ pub(crate) fn validate_iter_to_vec<'py>( ) -> ValResult> { let mut output: Vec = Vec::with_capacity(capacity); let mut errors: Vec = Vec::new(); - for (index, item_result) in iter.enumerate() { + let mut index = 0; + for item_result in iter { + index += 1; let item = item_result.map_err(|e| any_next_error!(py, e, max_length_check.input, index))?; match validator.validate(py, item.borrow_input(), state) { Ok(item) => { @@ -139,7 +142,7 @@ pub(crate) fn validate_iter_to_vec<'py>( max_length_check.incr()?; errors.extend(line_errors.into_iter().map(|err| err.with_outer_location(index))); if fail_fast { - break; + return Err(ValError::LineErrors(errors)); } } Err(ValError::Omit) => (), @@ -147,7 +150,7 @@ pub(crate) fn validate_iter_to_vec<'py>( } } - if errors.is_empty() { + if errors.is_empty() || sequence_valid_as_partial(state, index, &errors) { Ok(output) } else { Err(ValError::LineErrors(errors)) @@ -197,7 +200,9 @@ pub(crate) fn validate_iter_to_set<'py>( fail_fast: bool, ) -> ValResult<()> { let mut errors: Vec = Vec::new(); - for (index, item_result) in iter.enumerate() { + let mut index = 0; + for item_result in iter { + index += 1; let item = item_result.map_err(|e| any_next_error!(py, e, input, index))?; match validator.validate(py, item.borrow_input(), state) { Ok(item) => { @@ -226,11 +231,11 @@ pub(crate) fn validate_iter_to_set<'py>( Err(err) => return Err(err), } if fail_fast && !errors.is_empty() { - break; + return Err(ValError::LineErrors(errors)); } } - if errors.is_empty() { + if errors.is_empty() || sequence_valid_as_partial(state, index, &errors) { Ok(()) } else { Err(ValError::LineErrors(errors)) diff --git a/src/url.rs b/src/url.rs index faaab9c63..4a7e118bb 100644 --- a/src/url.rs +++ b/src/url.rs @@ -45,7 +45,7 @@ impl PyUrl { pub fn py_new(py: Python, url: &Bound<'_, PyAny>) -> PyResult { let schema_obj = SCHEMA_DEFINITION_URL .get_or_init(py, || build_schema_validator(py, "url")) - .validate_python(py, url, None, None, None, None)?; + .validate_python(py, url, None, None, None, None, false)?; schema_obj.extract(py) } @@ -225,7 +225,7 @@ impl PyMultiHostUrl { pub fn py_new(py: Python, url: &Bound<'_, PyAny>) -> PyResult { let schema_obj = SCHEMA_DEFINITION_MULTI_HOST_URL .get_or_init(py, || build_schema_validator(py, "multi-host-url")) - .validate_python(py, url, None, None, None, None)?; + .validate_python(py, url, None, None, None, None, false)?; schema_obj.extract(py) } diff --git a/src/validators/dict.rs b/src/validators/dict.rs index ea0000236..edd537ba7 100644 --- a/src/validators/dict.rs +++ b/src/validators/dict.rs @@ -3,7 +3,7 @@ use pyo3::prelude::*; use pyo3::types::PyDict; use crate::build_tools::is_strict; -use crate::errors::{LocItem, ValError, ValLineError, ValResult}; +use crate::errors::{sequence_valid_as_partial, LocItem, ValError, ValLineError, ValResult}; use crate::input::BorrowInput; use crate::input::ConsumeIterator; use crate::input::{Input, ValidatedDict}; @@ -109,8 +109,10 @@ where fn consume_iterator(self, iterator: impl Iterator>) -> ValResult { let output = PyDict::new_bound(self.py); let mut errors: Vec = Vec::new(); + let mut input_length = 0; for item_result in iterator { + input_length += 1; let (key, value) = item_result?; let output_key = match self.key_validator.validate(self.py, key.borrow_input(), self.state) { Ok(value) => Some(value), @@ -140,7 +142,7 @@ where } } - if errors.is_empty() { + if errors.is_empty() || sequence_valid_as_partial(self.state, input_length, &errors) { let input = self.input; length_check!(input, "Dictionary", self.min_length, self.max_length, output); Ok(output.into()) diff --git a/src/validators/generator.rs b/src/validators/generator.rs index de9949a8c..aeb0462fd 100644 --- a/src/validators/generator.rs +++ b/src/validators/generator.rs @@ -227,6 +227,7 @@ pub struct InternalValidator { hide_input_in_errors: bool, validation_error_cause: bool, cache_str: jiter::StringCacheMode, + allow_partial: bool, } impl fmt::Debug for InternalValidator { @@ -259,6 +260,7 @@ impl InternalValidator { hide_input_in_errors, validation_error_cause, cache_str: extra.cache_str, + allow_partial: extra.allow_partial, } } @@ -278,6 +280,7 @@ impl InternalValidator { context: self.context.as_ref().map(|data| data.bind(py)), self_instance: self.self_instance.as_ref().map(|data| data.bind(py)), cache_str: self.cache_str, + allow_partial: self.allow_partial, }; let mut state = ValidationState::new(extra, &mut self.recursion_guard); state.exactness = self.exactness; @@ -313,6 +316,7 @@ impl InternalValidator { context: self.context.as_ref().map(|data| data.bind(py)), self_instance: self.self_instance.as_ref().map(|data| data.bind(py)), cache_str: self.cache_str, + allow_partial: self.allow_partial, }; let mut state = ValidationState::new(extra, &mut self.recursion_guard); state.exactness = self.exactness; diff --git a/src/validators/mod.rs b/src/validators/mod.rs index fcd64ebf2..3640204da 100644 --- a/src/validators/mod.rs +++ b/src/validators/mod.rs @@ -164,7 +164,8 @@ impl SchemaValidator { Ok((cls, init_args)) } - #[pyo3(signature = (input, *, strict=None, from_attributes=None, context=None, self_instance=None))] + #[allow(clippy::too_many_arguments)] + #[pyo3(signature = (input, *, strict=None, from_attributes=None, context=None, self_instance=None, allow_partial=false))] pub fn validate_python( &self, py: Python, @@ -173,6 +174,7 @@ impl SchemaValidator { from_attributes: Option, context: Option<&Bound<'_, PyAny>>, self_instance: Option<&Bound<'_, PyAny>>, + allow_partial: bool, ) -> PyResult { self._validate( py, @@ -182,6 +184,7 @@ impl SchemaValidator { from_attributes, context, self_instance, + allow_partial, ) .map_err(|e| self.prepare_validation_err(py, e, InputType::Python)) } @@ -204,6 +207,7 @@ impl SchemaValidator { from_attributes, context, self_instance, + false, ) { Ok(_) => Ok(true), Err(ValError::InternalErr(err)) => Err(err), @@ -213,7 +217,7 @@ impl SchemaValidator { } } - #[pyo3(signature = (input, *, strict=None, context=None, self_instance=None))] + #[pyo3(signature = (input, *, strict=None, context=None, self_instance=None, allow_partial=false))] pub fn validate_json( &self, py: Python, @@ -221,6 +225,7 @@ impl SchemaValidator { strict: Option, context: Option<&Bound<'_, PyAny>>, self_instance: Option<&Bound<'_, PyAny>>, + allow_partial: bool, ) -> PyResult { let r = match json::validate_json_bytes(input) { Ok(v_match) => self._validate_json( @@ -230,24 +235,26 @@ impl SchemaValidator { strict, context, self_instance, + allow_partial, ), Err(err) => Err(err), }; r.map_err(|e| self.prepare_validation_err(py, e, InputType::Json)) } - #[pyo3(signature = (input, *, strict=None, context=None))] + #[pyo3(signature = (input, *, strict=None, context=None, allow_partial=false))] pub fn validate_strings( &self, py: Python, input: Bound<'_, PyAny>, strict: Option, context: Option<&Bound<'_, PyAny>>, + allow_partial: bool, ) -> PyResult { let t = InputType::String; let string_mapping = StringMapping::new_value(input).map_err(|e| self.prepare_validation_err(py, e, t))?; - match self._validate(py, &string_mapping, t, strict, None, context, None) { + match self._validate(py, &string_mapping, t, strict, None, context, None, allow_partial) { Ok(r) => Ok(r), Err(e) => Err(self.prepare_validation_err(py, e, t)), } @@ -273,6 +280,7 @@ impl SchemaValidator { context, self_instance: None, cache_str: self.cache_str, + allow_partial: false, }; let guard = &mut RecursionState::default(); @@ -297,6 +305,7 @@ impl SchemaValidator { context, self_instance: None, cache_str: self.cache_str, + allow_partial: false, }; let recursion_guard = &mut RecursionState::default(); let mut state = ValidationState::new(extra, recursion_guard); @@ -345,6 +354,7 @@ impl SchemaValidator { from_attributes: Option, context: Option<&Bound<'py, PyAny>>, self_instance: Option<&Bound<'py, PyAny>>, + allow_partial: bool, ) -> ValResult { let mut recursion_guard = RecursionState::default(); let mut state = ValidationState::new( @@ -355,12 +365,14 @@ impl SchemaValidator { self_instance, input_type, self.cache_str, + allow_partial, ), &mut recursion_guard, ); self.validator.validate(py, input, &mut state) } + #[allow(clippy::too_many_arguments)] fn _validate_json( &self, py: Python, @@ -369,10 +381,20 @@ impl SchemaValidator { strict: Option, context: Option<&Bound<'_, PyAny>>, self_instance: Option<&Bound<'_, PyAny>>, + allow_partial: bool, ) -> ValResult { let json_value = jiter::JsonValue::parse(json_data, true).map_err(|e| json::map_json_err(input, e, json_data))?; - self._validate(py, &json_value, InputType::Json, strict, None, context, self_instance) + self._validate( + py, + &json_value, + InputType::Json, + strict, + None, + context, + self_instance, + allow_partial, + ) } fn prepare_validation_err(&self, py: Python, error: ValError, input_type: InputType) -> PyErr { @@ -408,7 +430,7 @@ impl<'py> SelfValidator<'py> { let py = schema.py(); let mut recursion_guard = RecursionState::default(); let mut state = ValidationState::new( - Extra::new(strict, None, None, None, InputType::Python, true.into()), + Extra::new(strict, None, None, None, InputType::Python, true.into(), false), &mut recursion_guard, ); match self.validator.validator.validate(py, schema, &mut state) { @@ -606,6 +628,8 @@ pub struct Extra<'a, 'py> { self_instance: Option<&'a Bound<'py, PyAny>>, /// Whether to use a cache of short strings to accelerate python string construction cache_str: StringCacheMode, + /// Whether to allow validation of partial objects + pub allow_partial: bool, } impl<'a, 'py> Extra<'a, 'py> { @@ -616,6 +640,7 @@ impl<'a, 'py> Extra<'a, 'py> { self_instance: Option<&'a Bound<'py, PyAny>>, input_type: InputType, cache_str: StringCacheMode, + allow_partial: bool, ) -> Self { Extra { input_type, @@ -625,6 +650,7 @@ impl<'a, 'py> Extra<'a, 'py> { context, self_instance, cache_str, + allow_partial, } } } @@ -639,6 +665,7 @@ impl Extra<'_, '_> { context: self.context, self_instance: self.self_instance, cache_str: self.cache_str, + allow_partial: self.allow_partial, } } } diff --git a/src/validators/typed_dict.rs b/src/validators/typed_dict.rs index f72d969a8..cfa42d52d 100644 --- a/src/validators/typed_dict.rs +++ b/src/validators/typed_dict.rs @@ -6,7 +6,7 @@ use ahash::AHashSet; use crate::build_tools::py_schema_err; use crate::build_tools::{is_strict, schema_or_config, schema_or_config_same, ExtraBehavior}; -use crate::errors::LocItem; +use crate::errors::{mapping_valid_as_partial, LocItem}; use crate::errors::{ErrorTypeDefaults, ValError, ValLineError, ValResult}; use crate::input::BorrowInput; use crate::input::ConsumeIterator; @@ -322,10 +322,10 @@ impl Validator for TypedDictValidator { })??; } - if !errors.is_empty() { - Err(ValError::LineErrors(errors)) - } else { + if errors.is_empty() || mapping_valid_as_partial(state, dict.last_key(), &errors) { Ok(output_dict.to_object(py)) + } else { + Err(ValError::LineErrors(errors)) } } diff --git a/tests/test.rs b/tests/test.rs index 1a4a87ef3..fb141d377 100644 --- a/tests/test.rs +++ b/tests/test.rs @@ -129,7 +129,7 @@ json_input = '{"a": "something"}' let json_input = locals.get_item("json_input").unwrap().unwrap(); let binding = SchemaValidator::py_new(py, &schema, None) .unwrap() - .validate_json(py, &json_input, None, None, None) + .validate_json(py, &json_input, None, None, None, false) .unwrap(); let validation_result: Bound<'_, PyAny> = binding.extract(py).unwrap(); let repr = format!("{}", validation_result.repr().unwrap()); diff --git a/tests/test_typing.py b/tests/test_typing.py index 2f460fb48..0f5e45d0c 100644 --- a/tests/test_typing.py +++ b/tests/test_typing.py @@ -19,16 +19,20 @@ class Foo: bar: str -def foo(bar: str) -> None: ... +def foo(bar: str) -> None: + ... -def validator_deprecated(value: Any, info: core_schema.FieldValidationInfo) -> None: ... +def validator_deprecated(value: Any, info: core_schema.FieldValidationInfo) -> None: + ... -def validator(value: Any, info: core_schema.ValidationInfo) -> None: ... +def validator(value: Any, info: core_schema.ValidationInfo) -> None: + ... -def wrap_validator(value: Any, call_next: Callable[[Any], Any], info: core_schema.ValidationInfo) -> None: ... +def wrap_validator(value: Any, call_next: Callable[[Any], Any], info: core_schema.ValidationInfo) -> None: + ... def test_schema_typing() -> None: diff --git a/tests/validators/test_list.py b/tests/validators/test_list.py index 6914529f9..92f3aefc3 100644 --- a/tests/validators/test_list.py +++ b/tests/validators/test_list.py @@ -43,7 +43,7 @@ def test_list_strict(): def test_list_no_copy(): v = SchemaValidator({'type': 'list'}) - assert v.validate_python([1, 2, 3]) is not [1, 2, 3] # noqa: F632 + assert v.validate_python([1, 2, 3]) is not [1, 2, 3] def gen_ints(): @@ -547,3 +547,13 @@ def test_list_dict_items_input(testcase: ListInputTestCase) -> None: output = v.validate_python(testcase.input) assert output == testcase.output assert output is not testcase.input + + +def test_validate_partial(): + v = SchemaValidator( + core_schema.list_schema( + core_schema.tuple_positional_schema([core_schema.int_schema(), core_schema.int_schema()]), + ) + ) + assert v.validate_python([[1, 2], [3, 4]]) == [(1, 2), (3, 4)] + assert v.validate_python([[1, 2], [3, 4]], allow_partial=True) == [(1, 2), (3, 4)] diff --git a/tests/validators/test_uuid.py b/tests/validators/test_uuid.py index d1475f38e..c406c031e 100644 --- a/tests/validators/test_uuid.py +++ b/tests/validators/test_uuid.py @@ -9,7 +9,8 @@ from ..conftest import Err, PyAndJson -class MyStr(str): ... +class MyStr(str): + ... @pytest.mark.parametrize(