diff --git a/Cargo.lock b/Cargo.lock index 40ca98e4d..249fdfbf9 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -321,7 +321,7 @@ dependencies = [ [[package]] name = "pydantic-core" -version = "2.14.3" +version = "2.14.4" dependencies = [ "ahash", "base64", diff --git a/Cargo.toml b/Cargo.toml index 39da916f5..87a2f2282 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "pydantic-core" -version = "2.14.3" +version = "2.14.4" edition = "2021" license = "MIT" homepage = "https://github.com/pydantic/pydantic-core" @@ -75,3 +75,7 @@ pyo3 = { version = "0.20.0", features = ["auto-initialize"] } version_check = "0.9.4" # used where logic has to be version/distribution specific, e.g. pypy pyo3-build-config = { version = "0.20.0" } + +[lints.clippy] +dbg_macro = "warn" +print_stdout = "warn" diff --git a/Makefile b/Makefile index d9e0d0e0a..e477cf767 100644 --- a/Makefile +++ b/Makefile @@ -1,5 +1,5 @@ .DEFAULT_GOAL := all -sources = python/pydantic_core tests generate_self_schema.py wasm-preview/run_tests.py +sources = python/pydantic_core tests generate_self_schema.py wasm-preview/run_tests.py mypy-stubtest = python -m mypy.stubtest pydantic_core._pydantic_core --allowlist .mypy-stubtest-allowlist @@ -90,14 +90,14 @@ build-wasm: .PHONY: format format: - ruff --fix $(sources) - ruff format $(sources) + ruff --fix $(sources) + ruff format $(sources) cargo fmt .PHONY: lint-python lint-python: - ruff $(sources) - ruff format --check $(sources) + ruff $(sources) + ruff format --check $(sources) $(mypy-stubtest) griffe dump -f -d google -LWARNING -o/dev/null python/pydantic_core @@ -109,8 +109,6 @@ lint-rust: cargo clippy --tests -- \ -D warnings \ -W clippy::pedantic \ - -W clippy::dbg_macro \ - -W clippy::print_stdout \ -A clippy::cast-possible-truncation \ -A clippy::cast-possible-wrap \ -A clippy::cast-precision-loss \ diff --git a/src/validators/float.rs b/src/validators/float.rs index b72ffafc0..126e539c2 100644 --- a/src/validators/float.rs +++ b/src/validators/float.rs @@ -109,7 +109,7 @@ impl Validator for ConstrainedFloatValidator { } if let Some(multiple_of) = self.multiple_of { let rem = float % multiple_of; - let threshold = float / 1e9; + let threshold = float.abs() / 1e9; if rem.abs() > threshold && (rem - multiple_of).abs() > threshold { return Err(ValError::new( ErrorType::MultipleOf { diff --git a/src/validators/literal.rs b/src/validators/literal.rs index c9a846695..686920cca 100644 --- a/src/validators/literal.rs +++ b/src/validators/literal.rs @@ -9,7 +9,7 @@ use pyo3::{intern, PyTraverseError, PyVisit}; use crate::build_tools::{py_schema_err, py_schema_error_type}; use crate::errors::{ErrorType, ValError, ValResult}; -use crate::input::Input; +use crate::input::{Input, ValidationMatch}; use crate::py_gc::PyGcTraverse; use crate::tools::SchemaDict; @@ -116,8 +116,18 @@ impl LiteralLookup { } } if let Some(expected_strings) = &self.expected_str { - // dbg!(expected_strings); - if let Ok(either_str) = input.exact_str() { + let validation_result = if input.is_python() { + input.exact_str() + } else { + // Strings coming from JSON are treated as "strict" but not "exact" for reasons + // of parsing types like UUID; see the implementation of `validate_str` for Json + // inputs for justification. We might change that eventually, but for now we need + // to work around this when loading from JSON + // V3 TODO: revisit making this "exact" for JSON inputs + input.validate_str(true, false).map(ValidationMatch::into_inner) + }; + + if let Ok(either_str) = validation_result { let cow = either_str.as_cow()?; if let Some(id) = expected_strings.get(cow.as_ref()) { return Ok(Some((input, &self.values[*id]))); diff --git a/src/validators/model.rs b/src/validators/model.rs index 0299ce5d8..a571ccd9e 100644 --- a/src/validators/model.rs +++ b/src/validators/model.rs @@ -257,7 +257,10 @@ impl ModelValidator { // this work with from_attributes, and would essentially allow you to // handle init vars by adding them to the __init__ signature. if let Some(kwargs) = input.as_kwargs(py) { - return Ok(self.class.call(py, (), Some(kwargs))?); + return self + .class + .call(py, (), Some(kwargs)) + .map_err(|e| convert_err(py, e, input)); } } diff --git a/tests/test.rs b/tests/test.rs index 348520435..e0b76a4d8 100644 --- a/tests/test.rs +++ b/tests/test.rs @@ -1,6 +1,6 @@ #[cfg(test)] mod tests { - use _pydantic_core::SchemaSerializer; + use _pydantic_core::{SchemaSerializer, SchemaValidator}; use pyo3::prelude::*; use pyo3::types::PyDict; @@ -22,7 +22,7 @@ mod tests { // 'type': 'function-wrap', // 'function': lambda: None, // }, - let code = r#"{ + let code = r"{ 'type': 'definitions', 'schema': {'type': 'definition-ref', 'schema_ref': 'C-ref'}, 'definitions': [ @@ -44,7 +44,7 @@ mod tests { }, }, ] - }"#; + }"; let schema: &PyDict = py.eval(code, None, None).unwrap().extract().unwrap(); SchemaSerializer::py_new(py, schema, None).unwrap(); }); @@ -86,4 +86,35 @@ a = A() assert_eq!(serialized, b"{\"b\":\"b\"}"); }); } + + #[test] + fn test_literal_schema() { + Python::with_gil(|py| { + let code = r#" +schema = { + "type": "dict", + "keys_schema": { + "type": "literal", + "expected": ["a", "b"], + }, + "values_schema": { + "type": "str", + }, + "strict": False, +} +json_input = '{"a": "something"}' + "#; + let locals = PyDict::new(py); + py.run(code, None, Some(locals)).unwrap(); + let schema: &PyDict = locals.get_item("schema").unwrap().unwrap().extract().unwrap(); + let json_input: &PyAny = locals.get_item("json_input").unwrap().unwrap().extract().unwrap(); + let binding = SchemaValidator::py_new(py, schema, None) + .unwrap() + .validate_json(py, json_input, None, None, None) + .unwrap(); + let validation_result: &PyAny = binding.extract(py).unwrap(); + let repr = format!("{}", validation_result.repr().unwrap()); + assert_eq!(repr, "{'a': 'something'}"); + }); + } } diff --git a/tests/validators/test_decimal.py b/tests/validators/test_decimal.py index b9fabeaed..69cd52738 100644 --- a/tests/validators/test_decimal.py +++ b/tests/validators/test_decimal.py @@ -188,6 +188,7 @@ def test_decimal_kwargs(py_and_json: PyAndJson, kwargs: Dict[str, Any], input_va (0.1, 1, None), (0.1, 1.0, None), (0.1, int(5e10), None), + (2.0, -2.0, None), ], ids=repr, ) diff --git a/tests/validators/test_float.py b/tests/validators/test_float.py index 4e3bda0c4..56c03d40e 100644 --- a/tests/validators/test_float.py +++ b/tests/validators/test_float.py @@ -121,6 +121,7 @@ def test_float_kwargs(py_and_json: PyAndJson, kwargs: Dict[str, Any], input_valu (0.1, 1, None), (0.1, 1.0, None), (0.1, int(5e10), None), + (2.0, -2.0, None), ], ids=repr, ) diff --git a/tests/validators/test_model_init.py b/tests/validators/test_model_init.py index 5521f8da4..b0c28dc86 100644 --- a/tests/validators/test_model_init.py +++ b/tests/validators/test_model_init.py @@ -479,3 +479,48 @@ def _wrap_validator(cls, v, validator, info): gc.collect() assert ref() is None + + +def test_model_custom_init_with_union() -> None: + class A: + def __init__(self, **kwargs): + assert 'a' in kwargs + self.a = kwargs.get('a') + + class B: + def __init__(self, **kwargs): + assert 'b' in kwargs + self.b = kwargs.get('b') + + schema = { + 'type': 'union', + 'choices': [ + { + 'type': 'model', + 'cls': A, + 'schema': { + 'type': 'model-fields', + 'fields': {'a': {'type': 'model-field', 'schema': {'type': 'bool'}}}, + 'model_name': 'A', + }, + 'custom_init': True, + 'ref': '__main__.A:4947206928', + }, + { + 'type': 'model', + 'cls': B, + 'schema': { + 'type': 'model-fields', + 'fields': {'b': {'type': 'model-field', 'schema': {'type': 'bool'}}}, + 'model_name': 'B', + }, + 'custom_init': True, + 'ref': '__main__.B:4679932848', + }, + ], + } + + validator = SchemaValidator(schema) + + assert validator.validate_python({'a': False}).a is False + assert validator.validate_python({'b': True}).b is True