diff --git a/.mypy-stubtest-allowlist b/.mypy-stubtest-allowlist index c75b9bf8a..0f9ad4418 100644 --- a/.mypy-stubtest-allowlist +++ b/.mypy-stubtest-allowlist @@ -1,2 +1,4 @@ # TODO: don't want to expose this staticmethod, requires https://github.com/PyO3/pyo3/issues/2384 pydantic_core._pydantic_core.PydanticUndefinedType.new +# As per #1240, from_json has custom logic to coverage the `cache_strings` kwarg +pydantic_core._pydantic_core.from_json diff --git a/Cargo.lock b/Cargo.lock index 910e9b456..0fefe39b1 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -24,12 +24,6 @@ dependencies = [ "memchr", ] -[[package]] -name = "allocator-api2" -version = "0.2.16" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0942ffc6dcaadf03badf6e6a2d0228460359d5e34b57ccdc720b7382dfbd5ec5" - [[package]] name = "autocfg" version = "1.1.0" @@ -103,10 +97,6 @@ name = "hashbrown" version = "0.14.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "290f1a1d9242c78d09ce40a5e87e7554ee637af1351968159f4952f028f75604" -dependencies = [ - "ahash", - "allocator-api2", -] [[package]] name = "heck" @@ -148,12 +138,11 @@ checksum = "62b02a5381cc465bd3041d84623d0fa3b66738b52b8e2fc3bab8ad63ab032f4a" [[package]] name = "jiter" -version = "0.1.0" +version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a9cbc4bba9fee7e90f7ab23d53caa097007c6b870b2ca0a33334e774eb34c1ff" +checksum = "2c0b7c896d2b1da897be13affb0bbf7bff95437e9c50823ede962addadae58d8" dependencies = [ "ahash", - "hashbrown", "lexical-parse-float", "num-bigint", "num-traits", diff --git a/Cargo.toml b/Cargo.toml index 46b1ed4ca..8648b62b8 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -44,7 +44,7 @@ base64 = "0.21.7" num-bigint = "0.4.4" python3-dll-a = "0.2.7" uuid = "1.7.0" -jiter = { version = "0.1.0", features = ["python"] } +jiter = { version = "0.1.1", features = ["python"] } [lib] name = "_pydantic_core" diff --git a/python/pydantic_core/_pydantic_core.pyi b/python/pydantic_core/_pydantic_core.pyi index 334334087..d7e1dd5bb 100644 --- a/python/pydantic_core/_pydantic_core.pyi +++ b/python/pydantic_core/_pydantic_core.pyi @@ -390,17 +390,26 @@ def to_json( JSON bytes. """ -def from_json(data: str | bytes | bytearray, *, allow_inf_nan: bool = True, cache_strings: bool = True) -> Any: +def from_json( + data: str | bytes | bytearray, + *, + allow_inf_nan: bool = True, + cache_strings: bool | Literal['all', 'keys', 'none'] = True, + allow_partial: bool = False, +) -> Any: """ Deserialize JSON data to a Python object. - This is effectively a faster version of `json.loads()`. + This is effectively a faster version of `json.loads()`, with some extra functionality. Arguments: data: The JSON data to deserialize. allow_inf_nan: Whether to allow `Infinity`, `-Infinity` and `NaN` values as `json.loads()` does by default. cache_strings: Whether to cache strings to avoid constructing new Python objects, - this should have a significant impact on performance while increasing memory usage slightly. + this should have a significant impact on performance while increasing memory usage slightly, + `all/True` means cache all strings, `keys` means cache only dict keys, `none/False` means no caching. + allow_partial: Whether to allow partial deserialization, if `True` JSON data is returned if the end of the + input is reached before the full object is deserialized, e.g. `["aa", "bb", "c` would return `['aa', 'bb']`. Raises: ValueError: If deserialization fails. diff --git a/python/pydantic_core/core_schema.py b/python/pydantic_core/core_schema.py index 1b1ecc7fa..5722dc365 100644 --- a/python/pydantic_core/core_schema.py +++ b/python/pydantic_core/core_schema.py @@ -75,6 +75,8 @@ class CoreConfig(TypedDict, total=False): Requires exceptiongroup backport pre Python 3.11. coerce_numbers_to_str: Whether to enable coercion of any `Number` type to `str` (not applicable in `strict` mode). regex_engine: The regex engine to use for regex pattern validation. Default is 'rust-regex'. See `StringSchema`. + cache_strings: Whether to cache strings. Default is `True`, `True` or `'all'` is required to cache strings + during general validation since validators don't know if they're in a key or a value. """ title: str @@ -110,6 +112,7 @@ class CoreConfig(TypedDict, total=False): validation_error_cause: bool # default: False coerce_numbers_to_str: bool # default: False regex_engine: Literal['rust-regex', 'python-re'] # default: 'rust-regex' + cache_strings: Union[bool, Literal['all', 'keys', 'none']] # default: 'True' IncExCall: TypeAlias = 'set[int | str] | dict[int | str, IncExCall] | None' diff --git a/src/input/return_enums.rs b/src/input/return_enums.rs index 753ace478..5d1019f05 100644 --- a/src/input/return_enums.rs +++ b/src/input/return_enums.rs @@ -3,7 +3,7 @@ use std::cmp::Ordering; use std::ops::Rem; use std::str::FromStr; -use jiter::{JsonArray, JsonValue}; +use jiter::{JsonArray, JsonValue, StringCacheMode}; use num_bigint::BigInt; use pyo3::exceptions::PyTypeError; @@ -435,9 +435,15 @@ impl<'a> EitherString<'a> { } } - pub fn as_py_string(&'a self, py: Python<'a>) -> Bound<'a, PyString> { + pub fn as_py_string(&'a self, py: Python<'a>, cache_str: StringCacheMode) -> Bound<'a, PyString> { match self { - Self::Cow(cow) => PyString::new_bound(py, cow), + Self::Cow(cow) => { + if matches!(cache_str, StringCacheMode::All) { + jiter::cached_py_string(py, cow.as_ref()) + } else { + PyString::new_bound(py, cow.as_ref()) + } + } Self::Py(py_string) => py_string.clone(), } } @@ -461,12 +467,6 @@ impl<'a> From> for EitherString<'a> { } } -impl<'a> IntoPy for EitherString<'a> { - fn into_py(self, py: Python<'_>) -> PyObject { - self.as_py_string(py).into_py(py) - } -} - pub fn py_string_str<'a>(py_str: &'a Bound<'_, PyString>) -> ValResult<&'a str> { py_str.to_str().map_err(|_| { ValError::new_custom_input( diff --git a/src/lib.rs b/src/lib.rs index 93fd91252..82f941f20 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -4,6 +4,7 @@ extern crate core; use std::sync::OnceLock; +use jiter::StringCacheMode; use pyo3::exceptions::PyTypeError; use pyo3::{prelude::*, sync::GILOnceCell}; @@ -38,19 +39,31 @@ pub use validators::{validate_core_schema, PySome, SchemaValidator}; use crate::input::Input; -#[pyfunction(signature = (data, *, allow_inf_nan=true, cache_strings=true))] +#[derive(FromPyObject)] +pub enum CacheStringsArg { + Bool(bool), + Literal(StringCacheMode), +} + +#[pyfunction(signature = (data, *, allow_inf_nan=true, cache_strings=CacheStringsArg::Bool(true), allow_partial=false))] pub fn from_json<'py>( py: Python<'py>, data: &Bound<'_, PyAny>, allow_inf_nan: bool, - cache_strings: bool, + cache_strings: CacheStringsArg, + allow_partial: bool, ) -> PyResult> { let v_match = data .validate_bytes(false) .map_err(|_| PyTypeError::new_err("Expected bytes, bytearray or str"))?; let json_either_bytes = v_match.into_inner(); let json_bytes = json_either_bytes.as_slice(); - jiter::python_parse(py, json_bytes, allow_inf_nan, cache_strings).map_err(|e| jiter::map_json_error(json_bytes, &e)) + let cache_mode = match cache_strings { + CacheStringsArg::Bool(b) => b.into(), + CacheStringsArg::Literal(mode) => mode, + }; + jiter::python_parse(py, json_bytes, allow_inf_nan, cache_mode, allow_partial) + .map_err(|e| jiter::map_json_error(json_bytes, &e)) } pub fn get_pydantic_core_version() -> &'static str { diff --git a/src/validators/arguments.rs b/src/validators/arguments.rs index 024348527..457474d65 100644 --- a/src/validators/arguments.rs +++ b/src/validators/arguments.rs @@ -55,7 +55,8 @@ impl BuildValidator for ArgumentsValidator { for (arg_index, arg) in arguments_schema.iter().enumerate() { let arg = arg.downcast::()?; - let name: String = arg.get_as_req(intern!(py, "name"))?; + let py_name: Bound = arg.get_as_req(intern!(py, "name"))?; + let name = py_name.to_string(); let mode = arg.get_as::>(intern!(py, "mode"))?; let mode = mode .as_ref() @@ -77,7 +78,7 @@ impl BuildValidator for ArgumentsValidator { } None => Some(LookupKey::from_string(py, &name)), }; - kwarg_key = Some(PyString::new_bound(py, &name).into()); + kwarg_key = Some(py_name.into_py(py)); } let schema = arg.get_as_req(intern!(py, "schema"))?; @@ -274,7 +275,9 @@ impl Validator for ArgumentsValidator { if !used_kwargs.contains(either_str.as_cow()?.as_ref()) { match self.var_kwargs_validator { Some(ref validator) => match validator.validate(py, value.borrow_input(), state) { - Ok(value) => output_kwargs.set_item(either_str.as_py_string(py), value)?, + Ok(value) => { + output_kwargs.set_item(either_str.as_py_string(py, state.cache_str()), value)?; + } Err(ValError::LineErrors(line_errors)) => { for err in line_errors { errors.push(err.with_outer_location(raw_key.clone())); diff --git a/src/validators/dataclass.rs b/src/validators/dataclass.rs index 907e1511e..4e98538a8 100644 --- a/src/validators/dataclass.rs +++ b/src/validators/dataclass.rs @@ -302,7 +302,10 @@ impl Validator for DataclassArgsValidator { if let Some(ref validator) = self.extras_validator { match validator.validate(py, value.borrow_input(), state) { Ok(value) => { - output_dict.set_item(either_str.as_py_string(py), value)?; + output_dict.set_item( + either_str.as_py_string(py, state.cache_str()), + value, + )?; } Err(ValError::LineErrors(line_errors)) => { for err in line_errors { @@ -312,7 +315,8 @@ impl Validator for DataclassArgsValidator { Err(err) => return Err(err), } } else { - output_dict.set_item(either_str.as_py_string(py), value)?; + output_dict + .set_item(either_str.as_py_string(py, state.cache_str()), value)?; } } } @@ -455,7 +459,7 @@ impl BuildValidator for DataclassValidator { let validator = build_validator(&sub_schema, config, definitions)?; let post_init = if schema.get_as::(intern!(py, "post_init"))?.unwrap_or(false) { - Some(PyString::new_bound(py, "__post_init__").into()) + Some(intern!(py, "__post_init__").into_py(py)) } else { None }; diff --git a/src/validators/generator.rs b/src/validators/generator.rs index f7c932397..904f4a0ef 100644 --- a/src/validators/generator.rs +++ b/src/validators/generator.rs @@ -219,6 +219,7 @@ pub struct InternalValidator { validation_mode: InputType, hide_input_in_errors: bool, validation_error_cause: bool, + cache_str: jiter::StringCacheMode, } impl fmt::Debug for InternalValidator { @@ -250,6 +251,7 @@ impl InternalValidator { validation_mode: extra.input_type, hide_input_in_errors, validation_error_cause, + cache_str: extra.cache_str, } } @@ -268,6 +270,7 @@ impl InternalValidator { from_attributes: self.from_attributes, 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, }; let mut state = ValidationState::new(extra, &mut self.recursion_guard); state.exactness = self.exactness; @@ -302,6 +305,7 @@ impl InternalValidator { from_attributes: self.from_attributes, 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, }; let mut state = ValidationState::new(extra, &mut self.recursion_guard); state.exactness = self.exactness; diff --git a/src/validators/json.rs b/src/validators/json.rs index 62a23f0e6..5a5927058 100644 --- a/src/validators/json.rs +++ b/src/validators/json.rs @@ -64,8 +64,8 @@ impl Validator for JsonValidator { validator.validate(py, &json_value, &mut json_state) } None => { - let obj = - jiter::python_parse(py, json_bytes, true, true).map_err(|e| map_json_err(input, e, json_bytes))?; + let obj = jiter::python_parse(py, json_bytes, true, state.cache_str(), false) + .map_err(|e| map_json_err(input, e, json_bytes))?; Ok(obj.unbind()) } } diff --git a/src/validators/mod.rs b/src/validators/mod.rs index ac5d6d63c..ede6489ab 100644 --- a/src/validators/mod.rs +++ b/src/validators/mod.rs @@ -1,6 +1,7 @@ use std::fmt::Debug; use enum_dispatch::enum_dispatch; +use jiter::StringCacheMode; use pyo3::exceptions::PyTypeError; use pyo3::prelude::*; @@ -110,6 +111,7 @@ pub struct SchemaValidator { title: PyObject, hide_input_in_errors: bool, validation_error_cause: bool, + cache_str: StringCacheMode, } #[pymethods] @@ -135,6 +137,9 @@ impl SchemaValidator { }; let hide_input_in_errors: bool = config.get_as(intern!(py, "hide_input_in_errors"))?.unwrap_or(false); let validation_error_cause: bool = config.get_as(intern!(py, "validation_error_cause"))?.unwrap_or(false); + let cache_str: StringCacheMode = config + .get_as(intern!(py, "cache_strings"))? + .unwrap_or(StringCacheMode::All); Ok(Self { validator, definitions, @@ -143,6 +148,7 @@ impl SchemaValidator { title, hide_input_in_errors, validation_error_cause, + cache_str, }) } @@ -262,6 +268,7 @@ impl SchemaValidator { from_attributes, context, self_instance: None, + cache_str: self.cache_str, }; let guard = &mut RecursionState::default(); @@ -285,6 +292,7 @@ impl SchemaValidator { from_attributes: None, context, self_instance: None, + cache_str: self.cache_str, }; let recursion_guard = &mut RecursionState::default(); let mut state = ValidationState::new(extra, recursion_guard); @@ -300,10 +308,15 @@ impl SchemaValidator { pub fn __repr__(&self, py: Python) -> String { format!( - "SchemaValidator(title={:?}, validator={:#?}, definitions={:#?})", + "SchemaValidator(title={:?}, validator={:#?}, definitions={:#?}, cache_strings={})", self.title.extract::<&str>(py).unwrap(), self.validator, self.definitions, + match self.cache_str { + StringCacheMode::All => "True", + StringCacheMode::Keys => "'keys'", + StringCacheMode::None => "False", + } ) } @@ -331,7 +344,14 @@ impl SchemaValidator { ) -> ValResult { let mut recursion_guard = RecursionState::default(); let mut state = ValidationState::new( - Extra::new(strict, from_attributes, context, self_instance, input_type), + Extra::new( + strict, + from_attributes, + context, + self_instance, + input_type, + self.cache_str, + ), &mut recursion_guard, ); self.validator.validate(py, input, &mut state) @@ -384,7 +404,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), + Extra::new(strict, None, None, None, InputType::Python, true.into()), &mut recursion_guard, ); match self.validator.validator.validate(py, schema, &mut state) { @@ -414,6 +434,7 @@ impl<'py> SelfValidator<'py> { title: "Self Schema".into_py(py), hide_input_in_errors: false, validation_error_cause: false, + cache_str: true.into(), }) } } @@ -577,6 +598,8 @@ pub struct Extra<'a, 'py> { pub context: Option<&'a Bound<'py, PyAny>>, /// This is an instance of the model or dataclass being validated, when validation is performed from `__init__` self_instance: Option<&'a Bound<'py, PyAny>>, + /// Whether to use a cache of short strings to accelerate python string construction + cache_str: StringCacheMode, } impl<'a, 'py> Extra<'a, 'py> { @@ -586,6 +609,7 @@ impl<'a, 'py> Extra<'a, 'py> { context: Option<&'a Bound<'py, PyAny>>, self_instance: Option<&'a Bound<'py, PyAny>>, input_type: InputType, + cache_str: StringCacheMode, ) -> Self { Extra { input_type, @@ -594,6 +618,7 @@ impl<'a, 'py> Extra<'a, 'py> { from_attributes, context, self_instance, + cache_str, } } } @@ -607,6 +632,7 @@ impl Extra<'_, '_> { from_attributes: self.from_attributes, context: self.context, self_instance: self.self_instance, + cache_str: self.cache_str, } } } diff --git a/src/validators/model_fields.rs b/src/validators/model_fields.rs index f470557dd..eda76056a 100644 --- a/src/validators/model_fields.rs +++ b/src/validators/model_fields.rs @@ -283,7 +283,7 @@ impl Validator for ModelFieldsValidator { } ExtraBehavior::Ignore => {} ExtraBehavior::Allow => { - let py_key = either_str.as_py_string(self.py); + let py_key = either_str.as_py_string(self.py, self.state.cache_str()); if let Some(validator) = self.extras_validator { match validator.validate(self.py, value, self.state) { Ok(value) => { diff --git a/src/validators/string.rs b/src/validators/string.rs index 645217edd..8cd9394d0 100644 --- a/src/validators/string.rs +++ b/src/validators/string.rs @@ -49,7 +49,7 @@ impl Validator for StrValidator { ) -> ValResult { input .validate_str(state.strict_or(self.strict), self.coerce_numbers_to_str) - .map(|val_match| val_match.unpack(state).into_py(py)) + .map(|val_match| val_match.unpack(state).as_py_string(py, state.cache_str()).into_py(py)) } fn get_name(&self) -> &str { @@ -129,14 +129,14 @@ impl Validator for StrConstrainedValidator { } let py_string = if self.to_lower { - PyString::new_bound(py, &str.to_lowercase()) + state.maybe_cached_str(py, &str.to_lowercase()) } else if self.to_upper { - PyString::new_bound(py, &str.to_uppercase()) + state.maybe_cached_str(py, &str.to_uppercase()) } else if self.strip_whitespace { - PyString::new_bound(py, str) + state.maybe_cached_str(py, str) } else { // we haven't modified the string, return the original as it might be a PyString - either_str.as_py_string(py) + either_str.as_py_string(py, state.cache_str()) }; Ok(py_string.into_py(py)) } diff --git a/src/validators/typed_dict.rs b/src/validators/typed_dict.rs index 956ebac31..c90196c7c 100644 --- a/src/validators/typed_dict.rs +++ b/src/validators/typed_dict.rs @@ -281,7 +281,7 @@ impl Validator for TypedDictValidator { } ExtraBehavior::Ignore => {} ExtraBehavior::Allow => { - let py_key = either_str.as_py_string(self.py); + let py_key = either_str.as_py_string(self.py, self.state.cache_str()); if let Some(validator) = self.extras_validator { match validator.validate(self.py, value, self.state) { Ok(value) => { diff --git a/src/validators/union.rs b/src/validators/union.rs index 874da49af..8a33870ec 100644 --- a/src/validators/union.rs +++ b/src/validators/union.rs @@ -417,7 +417,7 @@ impl Validator for TaggedUnionValidator { } } Discriminator::SelfSchema => { - self.find_call_validator(py, self.self_schema_tag(py, input)?.as_any(), input, state) + self.find_call_validator(py, self.self_schema_tag(py, input, state)?.as_any(), input, state) } } } @@ -432,6 +432,7 @@ impl TaggedUnionValidator { &self, py: Python<'py>, input: &(impl Input<'py> + ?Sized), + state: &mut ValidationState<'_, 'py>, ) -> ValResult> { let dict = input.strict_dict()?; let dict = dict.as_py_dict().expect("self schema is always a Python dictionary"); @@ -452,7 +453,7 @@ impl TaggedUnionValidator { }; tag } else { - Ok(PyString::new_bound(py, tag)) + Ok(state.maybe_cached_str(py, tag)) } } diff --git a/src/validators/validation_state.rs b/src/validators/validation_state.rs index 1de2d75bd..8e68cd8d9 100644 --- a/src/validators/validation_state.rs +++ b/src/validators/validation_state.rs @@ -1,3 +1,8 @@ +use pyo3::prelude::*; +use pyo3::types::PyString; + +use jiter::StringCacheMode; + use crate::recursion_guard::{ContainsRecursionState, RecursionState}; use super::Extra; @@ -61,6 +66,18 @@ impl<'a, 'py> ValidationState<'a, 'py> { Some(Exactness::Exact) => self.exactness = Some(exactness), } } + + pub fn cache_str(&self) -> StringCacheMode { + self.extra.cache_str + } + + pub fn maybe_cached_str(&self, py: Python<'py>, s: &str) -> Bound<'py, PyString> { + if matches!(self.extra.cache_str, StringCacheMode::All) { + jiter::cached_py_string(py, s) + } else { + PyString::new_bound(py, s) + } + } } impl ContainsRecursionState for ValidationState<'_, '_> { diff --git a/tests/benchmarks/test_micro_benchmarks.py b/tests/benchmarks/test_micro_benchmarks.py index da7868c69..a86f698c1 100644 --- a/tests/benchmarks/test_micro_benchmarks.py +++ b/tests/benchmarks/test_micro_benchmarks.py @@ -366,6 +366,32 @@ def t(): v.validate_json(json_data[1]) +list_of_strs_data = [str(i) for i in range(1000)] + + +@pytest.mark.benchmark(group='list[str]') +def test_list_of_strs_py_cached(benchmark): + v = SchemaValidator(core_schema.list_schema(core_schema.str_schema())) + + benchmark(v.validate_python, list_of_strs_data) + + +@pytest.mark.benchmark(group='list[str]') +def test_list_of_strs_json_cached(benchmark): + v = SchemaValidator(core_schema.list_schema(core_schema.str_schema())) + + json_data = json.dumps(list_of_strs_data) + benchmark(v.validate_json, json_data) + + +@pytest.mark.benchmark(group='list[str]') +def test_list_of_strs_json_uncached(benchmark): + v = SchemaValidator(core_schema.list_schema(core_schema.str_schema()), {'cache_strings': False}) + + json_data = json.dumps(list_of_strs_data) + benchmark(v.validate_json, json_data) + + @pytest.mark.benchmark(group='List[Any]') def test_list_of_any_core_py(benchmark): v = SchemaValidator({'type': 'list'}) diff --git a/tests/test_config.py b/tests/test_config.py index 656918f82..8f552c20b 100644 --- a/tests/test_config.py +++ b/tests/test_config.py @@ -137,3 +137,17 @@ def test_hide_input_in_errors(config, input_str): with pytest.raises(ValidationError, match=re.escape(f'Input should be a valid string [{input_str}]')): assert v.validate_python({'f': 123}) + + +def test_cache_strings(): + v = SchemaValidator({'type': 'str'}) + assert 'cache_strings=True' in plain_repr(v) + + v = SchemaValidator({'type': 'str'}, {'cache_strings': True}) + assert 'cache_strings=True' in plain_repr(v) + + v = SchemaValidator({'type': 'str'}, {'cache_strings': False}) + assert 'cache_strings=False' in plain_repr(v) + + v = SchemaValidator({'type': 'str'}, {'cache_strings': 'keys'}) + assert "cache_strings='keys'" in plain_repr(v) diff --git a/tests/test_json.py b/tests/test_json.py index 4ef8a1d40..0f599cee1 100644 --- a/tests/test_json.py +++ b/tests/test_json.py @@ -13,6 +13,7 @@ SchemaValidator, ValidationError, core_schema, + from_json, to_json, to_jsonable_python, ) @@ -365,3 +366,13 @@ def test_inf_nan_allow(): assert v.validate_json('Infinity') == float('inf') assert v.validate_json('-Infinity') == float('-inf') assert v.validate_json('NaN') == IsFloatNan() + + +def test_partial_parse(): + with pytest.raises(ValueError, match='EOF while parsing a string at line 1 column 15'): + from_json('["aa", "bb", "c') + assert from_json('["aa", "bb", "c', allow_partial=True) == ['aa', 'bb'] + + with pytest.raises(ValueError, match='EOF while parsing a string at line 1 column 15'): + from_json(b'["aa", "bb", "c') + assert from_json(b'["aa", "bb", "c', allow_partial=True) == ['aa', 'bb'] diff --git a/tests/validators/test_bool.py b/tests/validators/test_bool.py index 3ac900701..88f30add4 100644 --- a/tests/validators/test_bool.py +++ b/tests/validators/test_bool.py @@ -77,9 +77,15 @@ def test_bool_error(pydantic_version): def test_bool_repr(): v = SchemaValidator({'type': 'bool'}) - assert plain_repr(v) == 'SchemaValidator(title="bool",validator=Bool(BoolValidator{strict:false}),definitions=[])' + assert ( + plain_repr(v) + == 'SchemaValidator(title="bool",validator=Bool(BoolValidator{strict:false}),definitions=[],cache_strings=True)' + ) v = SchemaValidator({'type': 'bool', 'strict': True}) - assert plain_repr(v) == 'SchemaValidator(title="bool",validator=Bool(BoolValidator{strict:true}),definitions=[])' + assert ( + plain_repr(v) + == 'SchemaValidator(title="bool",validator=Bool(BoolValidator{strict:true}),definitions=[],cache_strings=True)' + ) def test_bool_key(py_and_json: PyAndJson): diff --git a/tests/validators/test_definitions.py b/tests/validators/test_definitions.py index 606c8a6d9..18101fa93 100644 --- a/tests/validators/test_definitions.py +++ b/tests/validators/test_definitions.py @@ -32,7 +32,7 @@ def test_ignored_def(): def test_extract_used_refs_ignores_metadata(): v = SchemaValidator(core_schema.any_schema(metadata={'type': 'definition-ref'})) assert v.validate_python([1, 2, 3]) == [1, 2, 3] - assert plain_repr(v).endswith('definitions=[])') + assert plain_repr(v).endswith('definitions=[],cache_strings=True)') def test_check_ref_used_ignores_metadata(): diff --git a/tests/validators/test_float.py b/tests/validators/test_float.py index 5af0d4adb..88966261a 100644 --- a/tests/validators/test_float.py +++ b/tests/validators/test_float.py @@ -183,12 +183,12 @@ def test_float_repr(): v = SchemaValidator({'type': 'float'}) assert ( plain_repr(v) - == 'SchemaValidator(title="float",validator=Float(FloatValidator{strict:false,allow_inf_nan:true}),definitions=[])' + == 'SchemaValidator(title="float",validator=Float(FloatValidator{strict:false,allow_inf_nan:true}),definitions=[],cache_strings=True)' ) v = SchemaValidator({'type': 'float', 'strict': True}) assert ( plain_repr(v) - == 'SchemaValidator(title="float",validator=Float(FloatValidator{strict:true,allow_inf_nan:true}),definitions=[])' + == 'SchemaValidator(title="float",validator=Float(FloatValidator{strict:true,allow_inf_nan:true}),definitions=[],cache_strings=True)' ) v = SchemaValidator({'type': 'float', 'multiple_of': 7}) assert plain_repr(v).startswith('SchemaValidator(title="constrained-float",validator=ConstrainedFloat(') diff --git a/tests/validators/test_frozenset.py b/tests/validators/test_frozenset.py index 6ee297b22..407e0a579 100644 --- a/tests/validators/test_frozenset.py +++ b/tests/validators/test_frozenset.py @@ -249,7 +249,9 @@ def test_repr(): 'validator=FrozenSet(FrozenSetValidator{' 'strict:true,item_validator:Any(AnyValidator),min_length:Some(42),max_length:None,' 'name:"frozenset[any]"' - '}),definitions=[])' + '}),' + 'definitions=[],' + 'cache_strings=True)' ) diff --git a/tests/validators/test_int.py b/tests/validators/test_int.py index c5ccd7957..baddb6289 100644 --- a/tests/validators/test_int.py +++ b/tests/validators/test_int.py @@ -342,9 +342,15 @@ def test_union_int_simple(py_and_json: PyAndJson): def test_int_repr(): v = SchemaValidator({'type': 'int'}) - assert plain_repr(v) == 'SchemaValidator(title="int",validator=Int(IntValidator{strict:false}),definitions=[])' + assert ( + plain_repr(v) + == 'SchemaValidator(title="int",validator=Int(IntValidator{strict:false}),definitions=[],cache_strings=True)' + ) v = SchemaValidator({'type': 'int', 'strict': True}) - assert plain_repr(v) == 'SchemaValidator(title="int",validator=Int(IntValidator{strict:true}),definitions=[])' + assert ( + plain_repr(v) + == 'SchemaValidator(title="int",validator=Int(IntValidator{strict:true}),definitions=[],cache_strings=True)' + ) v = SchemaValidator({'type': 'int', 'multiple_of': 7}) assert plain_repr(v).startswith('SchemaValidator(title="constrained-int",validator=ConstrainedInt(') diff --git a/tests/validators/test_string.py b/tests/validators/test_string.py index 22bcd5445..81b43d19f 100644 --- a/tests/validators/test_string.py +++ b/tests/validators/test_string.py @@ -212,7 +212,7 @@ def test_default_validator(): v = SchemaValidator(core_schema.str_schema(strict=True, to_lower=False), {'str_strip_whitespace': False}) assert ( plain_repr(v) - == 'SchemaValidator(title="str",validator=Str(StrValidator{strict:true,coerce_numbers_to_str:false}),definitions=[])' + == 'SchemaValidator(title="str",validator=Str(StrValidator{strict:true,coerce_numbers_to_str:false}),definitions=[],cache_strings=True)' ) diff --git a/tests/validators/test_union.py b/tests/validators/test_union.py index 42c542000..332b23257 100644 --- a/tests/validators/test_union.py +++ b/tests/validators/test_union.py @@ -264,7 +264,7 @@ def test_one_choice(): v = SchemaValidator({'type': 'union', 'choices': [{'type': 'str'}]}) assert ( plain_repr(v) - == 'SchemaValidator(title="str",validator=Str(StrValidator{strict:false,coerce_numbers_to_str:false}),definitions=[])' + == 'SchemaValidator(title="str",validator=Str(StrValidator{strict:false,coerce_numbers_to_str:false}),definitions=[],cache_strings=True)' ) assert v.validate_python('hello') == 'hello'