diff --git a/Cargo.lock b/Cargo.lock index fc9a6ed95..e07369ccb 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -255,7 +255,7 @@ dependencies = [ [[package]] name = "pydantic-core" -version = "2.1.2" +version = "2.1.3" dependencies = [ "ahash", "base64", @@ -426,9 +426,9 @@ checksum = "a507befe795404456341dfab10cef66ead4c041f62b8b11bbb92bffe5d0953e0" [[package]] name = "speedate" -version = "0.9.0" +version = "0.9.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "312e03cbe7f96cdbd7b69de27d396d541632f660b400a953fe536403e4698e75" +checksum = "eb4daf811c41c4deda029974aac9583eab5cb4e1b6aef14c3cc23539e310faf6" dependencies = [ "strum", "strum_macros 0.25.0", diff --git a/Cargo.toml b/Cargo.toml index 7c3bae823..8969a4b51 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "pydantic-core" -version = "2.1.2" +version = "2.1.3" edition = "2021" license = "MIT" homepage = "https://github.com/pydantic/pydantic-core" @@ -35,7 +35,7 @@ enum_dispatch = "0.3.8" serde = "1.0.147" # disabled for benchmarks since it makes microbenchmark performance more flakey mimalloc = { version = "0.1.30", optional = true, default-features = false, features = ["local_dynamic_tls"] } -speedate = "0.9.0" +speedate = "0.9.1" ahash = "0.8.0" url = "2.3.1" # idna is already required by url, added here to be explicit diff --git a/python/pydantic_core/_pydantic_core.pyi b/python/pydantic_core/_pydantic_core.pyi index d860762c1..118e5d547 100644 --- a/python/pydantic_core/_pydantic_core.pyi +++ b/python/pydantic_core/_pydantic_core.pyi @@ -22,6 +22,7 @@ from _typeshed import SupportsAllComparisons __all__ = [ '__version__', 'build_profile', + 'build_info', '_recursion_limit', 'ArgsKwargs', 'SchemaValidator', @@ -45,6 +46,7 @@ __all__ = [ ] __version__: str build_profile: str +build_info: str _recursion_limit: int _T = TypeVar('_T', default=Any, covariant=True) diff --git a/python/pydantic_core/core_schema.py b/python/pydantic_core/core_schema.py index 060a57a28..267a7662f 100644 --- a/python/pydantic_core/core_schema.py +++ b/python/pydantic_core/core_schema.py @@ -3811,6 +3811,7 @@ def definition_reference_schema( 'string_too_short', 'string_too_long', 'string_pattern_mismatch', + 'enum', 'dict_type', 'mapping_type', 'list_type', diff --git a/src/errors/types.rs b/src/errors/types.rs index 049c442c9..7e6e66ac6 100644 --- a/src/errors/types.rs +++ b/src/errors/types.rs @@ -159,6 +159,11 @@ pub enum ErrorType { pattern: String, }, // --------------------- + // enum errors + Enum { + expected: String, + }, + // --------------------- // dict errors DictType, MappingType { @@ -199,10 +204,10 @@ pub enum ErrorType { // --------------------- // python errors from functions ValueError { - error: String, + error: Option, // Use Option because EnumIter requires Default to be implemented }, AssertionError { - error: String, + error: Option, // Use Option because EnumIter requires Default to be implemented }, // Note: strum message and serialize are not used here CustomError { @@ -415,12 +420,13 @@ impl ErrorType { Self::IterationError { .. } => extract_context!(IterationError, ctx, error: String), Self::StringTooShort { .. } => extract_context!(StringTooShort, ctx, min_length: usize), Self::StringTooLong { .. } => extract_context!(StringTooLong, ctx, max_length: usize), + Self::Enum { .. } => extract_context!(Enum, ctx, expected: String), Self::StringPatternMismatch { .. } => extract_context!(StringPatternMismatch, ctx, pattern: String), Self::MappingType { .. } => extract_context!(Cow::Owned, MappingType, ctx, error: String), Self::BytesTooShort { .. } => extract_context!(BytesTooShort, ctx, min_length: usize), Self::BytesTooLong { .. } => extract_context!(BytesTooLong, ctx, max_length: usize), - Self::ValueError { .. } => extract_context!(ValueError, ctx, error: String), - Self::AssertionError { .. } => extract_context!(AssertionError, ctx, error: String), + Self::ValueError { .. } => extract_context!(ValueError, ctx, error: Option), + Self::AssertionError { .. } => extract_context!(AssertionError, ctx, error: Option), Self::LiteralError { .. } => extract_context!(LiteralError, ctx, expected: String), Self::DateParsing { .. } => extract_context!(Cow::Owned, DateParsing, ctx, error: String), Self::DateFromDatetimeParsing { .. } => extract_context!(DateFromDatetimeParsing, ctx, error: String), @@ -492,6 +498,7 @@ impl ErrorType { Self::StringTooShort {..} => "String should have at least {min_length} characters", Self::StringTooLong {..} => "String should have at most {max_length} characters", Self::StringPatternMismatch {..} => "String should match pattern '{pattern}'", + Self::Enum {..} => "Input should be {expected}", Self::DictType => "Input should be a valid dictionary", Self::MappingType {..} => "Input should be a valid mapping, error: {error}", Self::ListType => "Input should be a valid list", @@ -628,11 +635,22 @@ impl ErrorType { Self::StringTooShort { min_length } => to_string_render!(tmpl, min_length), Self::StringTooLong { max_length } => to_string_render!(tmpl, max_length), Self::StringPatternMismatch { pattern } => render!(tmpl, pattern), + Self::Enum { expected } => to_string_render!(tmpl, expected), Self::MappingType { error } => render!(tmpl, error), Self::BytesTooShort { min_length } => to_string_render!(tmpl, min_length), Self::BytesTooLong { max_length } => to_string_render!(tmpl, max_length), - Self::ValueError { error } => render!(tmpl, error), - Self::AssertionError { error } => render!(tmpl, error), + Self::ValueError { error, .. } => { + let error = &error + .as_ref() + .map_or(Cow::Borrowed("None"), |v| Cow::Owned(v.as_ref(py).to_string())); + render!(tmpl, error) + } + Self::AssertionError { error, .. } => { + let error = &error + .as_ref() + .map_or(Cow::Borrowed("None"), |v| Cow::Owned(v.as_ref(py).to_string())); + render!(tmpl, error) + } Self::CustomError { custom_error: value_error, } => value_error.message(py), @@ -687,6 +705,7 @@ impl ErrorType { Self::StringTooShort { min_length } => py_dict!(py, min_length), Self::StringTooLong { max_length } => py_dict!(py, max_length), Self::StringPatternMismatch { pattern } => py_dict!(py, pattern), + Self::Enum { expected } => py_dict!(py, expected), Self::MappingType { error } => py_dict!(py, error), Self::BytesTooShort { min_length } => py_dict!(py, min_length), Self::BytesTooLong { max_length } => py_dict!(py, max_length), diff --git a/src/errors/validation_exception.rs b/src/errors/validation_exception.rs index 4ac3af6ca..8ccd737cd 100644 --- a/src/errors/validation_exception.rs +++ b/src/errors/validation_exception.rs @@ -15,7 +15,7 @@ use serde_json::ser::PrettyFormatter; use crate::build_tools::py_schema_error_type; use crate::errors::LocItem; -use crate::get_version; +use crate::get_pydantic_version; use crate::serializers::{SerMode, SerializationState}; use crate::tools::{safe_repr, SchemaDict}; @@ -113,7 +113,12 @@ static URL_PREFIX: GILOnceCell = GILOnceCell::new(); fn get_url_prefix(py: Python, include_url: bool) -> Option<&str> { if include_url { - Some(URL_PREFIX.get_or_init(py, || format!("https://errors.pydantic.dev/{}/v/", get_version()))) + Some(URL_PREFIX.get_or_init(py, || { + format!( + "https://errors.pydantic.dev/{}/v/", + get_pydantic_version(py).unwrap_or("latest") + ) + })) } else { None } diff --git a/src/lib.rs b/src/lib.rs index d52c33a5e..e4a7cee52 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -2,7 +2,9 @@ extern crate core; -use pyo3::prelude::*; +use std::sync::OnceLock; + +use pyo3::{prelude::*, sync::GILOnceCell}; #[cfg(feature = "mimalloc")] #[global_allocator] @@ -33,20 +35,47 @@ pub use serializers::{ }; pub use validators::{PySome, SchemaValidator}; -pub fn get_version() -> String { - let version = env!("CARGO_PKG_VERSION"); - // cargo uses "1.0-alpha1" etc. while python uses "1.0.0a1", this is not full compatibility, - // but it's good enough for now - // see https://docs.rs/semver/1.0.9/semver/struct.Version.html#method.parse for rust spec - // see https://peps.python.org/pep-0440/ for python spec - // it seems the dot after "alpha/beta" e.g. "-alpha.1" is not necessary, hence why this works - version.replace("-alpha", "a").replace("-beta", "b") +pub fn get_pydantic_core_version() -> &'static str { + static PYDANTIC_CORE_VERSION: OnceLock = OnceLock::new(); + + PYDANTIC_CORE_VERSION.get_or_init(|| { + let version = env!("CARGO_PKG_VERSION"); + // cargo uses "1.0-alpha1" etc. while python uses "1.0.0a1", this is not full compatibility, + // but it's good enough for now + // see https://docs.rs/semver/1.0.9/semver/struct.Version.html#method.parse for rust spec + // see https://peps.python.org/pep-0440/ for python spec + // it seems the dot after "alpha/beta" e.g. "-alpha.1" is not necessary, hence why this works + version.replace("-alpha", "a").replace("-beta", "b") + }) +} + +/// Returns the installed version of pydantic. +fn get_pydantic_version(py: Python<'_>) -> Option<&'static str> { + static PYDANTIC_VERSION: GILOnceCell> = GILOnceCell::new(); + + PYDANTIC_VERSION + .get_or_init(py, || { + py.import("pydantic") + .and_then(|pydantic| pydantic.getattr("__version__")?.extract()) + .ok() + }) + .as_deref() +} + +pub fn build_info() -> String { + format!( + "profile={} pgo={} mimalloc={}", + env!("PROFILE"), + option_env!("RUSTFLAGS").unwrap_or("").contains("-Cprofile-use="), + cfg!(feature = "mimalloc") + ) } #[pymodule] fn _pydantic_core(py: Python, m: &PyModule) -> PyResult<()> { - m.add("__version__", get_version())?; + m.add("__version__", get_pydantic_core_version())?; m.add("build_profile", env!("PROFILE"))?; + m.add("build_info", build_info())?; m.add("_recursion_limit", recursion_guard::RECURSION_GUARD_LIMIT)?; m.add("PydanticUndefined", PydanticUndefinedType::new(py))?; m.add_class::()?; diff --git a/src/recursion_guard.rs b/src/recursion_guard.rs index 89a599959..453f01a1d 100644 --- a/src/recursion_guard.rs +++ b/src/recursion_guard.rs @@ -20,7 +20,15 @@ pub struct RecursionGuard { } // A hard limit to avoid stack overflows when rampant recursion occurs -pub const RECURSION_GUARD_LIMIT: u16 = if cfg!(target_family = "wasm") { 50 } else { 255 }; +pub const RECURSION_GUARD_LIMIT: u16 = if cfg!(any(target_family = "wasm", all(windows, PyPy))) { + // wasm and windows PyPy have very limited stack sizes + 50 +} else if cfg!(any(PyPy, windows)) { + // PyPy and Windows in general have more restricted stack space + 100 +} else { + 255 +}; impl RecursionGuard { // insert a new id into the set, return whether the set already had the id in it diff --git a/src/validators/function.rs b/src/validators/function.rs index 40a1128af..fa204f847 100644 --- a/src/validators/function.rs +++ b/src/validators/function.rs @@ -477,13 +477,12 @@ macro_rules! py_err_string { ($error_value:expr, $type_member:ident, $input:ident) => { match $error_value.str() { Ok(py_string) => match py_string.to_str() { - Ok(s) => { - let error = match s.is_empty() { - true => "Unknown error".to_string(), - false => s.to_string(), - }; - ValError::new(ErrorType::$type_member { error }, $input) - } + Ok(_) => ValError::new( + ErrorType::$type_member { + error: Some($error_value.into()), + }, + $input, + ), Err(e) => ValError::InternalErr(e), }, Err(e) => ValError::InternalErr(e), diff --git a/tests/benchmarks/test_micro_benchmarks.py b/tests/benchmarks/test_micro_benchmarks.py index 663195bc5..55b9ce675 100644 --- a/tests/benchmarks/test_micro_benchmarks.py +++ b/tests/benchmarks/test_micro_benchmarks.py @@ -274,7 +274,7 @@ def definition_model_data(): data = {'width': -1} _data = data - for i in range(100): + for i in range(pydantic_core._pydantic_core._recursion_limit - 2): _data['branch'] = {'width': i} _data = _data['branch'] return data @@ -1189,14 +1189,15 @@ def f(v: int, info: core_schema.FieldValidationInfo) -> int: return v + 1 schema: core_schema.CoreSchema = core_schema.int_schema() + limit = pydantic_core._pydantic_core._recursion_limit - 3 - for _ in range(100): + for _ in range(limit): schema = core_schema.field_after_validator_function(f, 'x', schema) schema = core_schema.typed_dict_schema({'x': core_schema.typed_dict_field(schema)}) v = SchemaValidator(schema) payload = {'x': 0} - assert v.validate_python(payload) == {'x': 100} + assert v.validate_python(payload) == {'x': limit} benchmark(v.validate_python, payload) diff --git a/tests/conftest.py b/tests/conftest.py index d86b6069b..be06111d7 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -123,6 +123,16 @@ def _import_execute(source: str, *, custom_module_name: 'str | None' = None): return _import_execute +@pytest.fixture +def pydantic_version(): + try: + import pydantic + + return pydantic.__version__ + except ImportError: + return 'latest' + + def infinite_generator(): i = 0 while True: diff --git a/tests/test_build.py b/tests/test_build.py index e4d4428ef..7cc0b69af 100644 --- a/tests/test_build.py +++ b/tests/test_build.py @@ -2,7 +2,7 @@ import pytest -from pydantic_core import SchemaError, SchemaValidator, __version__ +from pydantic_core import SchemaError, SchemaValidator from pydantic_core import core_schema as cs @@ -32,13 +32,13 @@ def test_schema_as_string(): assert v.validate_python('tRuE') is True -def test_schema_wrong_type(): +def test_schema_wrong_type(pydantic_version): with pytest.raises(SchemaError) as exc_info: SchemaValidator(1) assert str(exc_info.value) == ( 'Invalid Schema:\n Input should be a valid dictionary or object to' ' extract fields from [type=model_attributes_type, input_value=1, input_type=int]\n' - f' For further information visit https://errors.pydantic.dev/{__version__}/v/model_attributes_type' + f' For further information visit https://errors.pydantic.dev/{pydantic_version}/v/model_attributes_type' ) assert exc_info.value.errors() == [ { diff --git a/tests/test_errors.py b/tests/test_errors.py index b92aad4fc..bca2220a2 100644 --- a/tests/test_errors.py +++ b/tests/test_errors.py @@ -1,5 +1,6 @@ import re from decimal import Decimal +from typing import Any import pytest from dirty_equals import HasRepr, IsInstance, IsJson, IsStr @@ -10,7 +11,6 @@ PydanticOmit, SchemaValidator, ValidationError, - __version__, core_schema, ) from pydantic_core._pydantic_core import list_all_errors @@ -191,6 +191,7 @@ def f(input_value, info): ('invalid_key', 'Keys should be strings', None), ('get_attribute_error', 'Error extracting attribute: foo', {'error': 'foo'}), ('none_required', 'Input should be None', None), + ('enum', 'Input should be foo', {'expected': 'foo'}), ('greater_than', 'Input should be greater than 42.1', {'gt': 42.1}), ('greater_than', 'Input should be greater than 42.1', {'gt': '42.1'}), ('greater_than', 'Input should be greater than 2020-01-01', {'gt': '2020-01-01'}), @@ -237,8 +238,8 @@ def f(input_value, info): ('bytes_type', 'Input should be a valid bytes', None), ('bytes_too_short', 'Data should have at least 42 bytes', {'min_length': 42}), ('bytes_too_long', 'Data should have at most 42 bytes', {'max_length': 42}), - ('value_error', 'Value error, foobar', {'error': 'foobar'}), - ('assertion_error', 'Assertion failed, foobar', {'error': 'foobar'}), + ('value_error', 'Value error, foobar', {'error': ValueError('foobar')}), + ('assertion_error', 'Assertion failed, foobar', {'error': AssertionError('foobar')}), ('literal_error', 'Input should be foo', {'expected': 'foo'}), ('literal_error', 'Input should be foo or bar', {'expected': 'foo or bar'}), ('date_type', 'Input should be a valid date', None), @@ -412,7 +413,7 @@ def __repr__(self): raise RuntimeError('bad repr') -def test_error_on_repr(): +def test_error_on_repr(pydantic_version): s = SchemaValidator({'type': 'int'}) with pytest.raises(ValidationError) as exc_info: s.validate_python(BadRepr()) @@ -421,7 +422,7 @@ def test_error_on_repr(): '1 validation error for int\n' ' Input should be a valid integer ' '[type=int_type, input_value=, input_type=BadRepr]\n' - f' For further information visit https://errors.pydantic.dev/{__version__}/v/int_type' + f' For further information visit https://errors.pydantic.dev/{pydantic_version}/v/int_type' ) assert exc_info.value.errors(include_url=False) == [ {'type': 'int_type', 'loc': (), 'msg': 'Input should be a valid integer', 'input': IsInstance(BadRepr)} @@ -438,7 +439,7 @@ def test_error_on_repr(): ) -def test_error_json(): +def test_error_json(pydantic_version): s = SchemaValidator({'type': 'str', 'min_length': 3}) with pytest.raises(ValidationError) as exc_info: s.validate_python('12') @@ -461,7 +462,7 @@ def test_error_json(): 'msg': 'String should have at least 3 characters', 'input': '12', 'ctx': {'min_length': 3}, - 'url': f'https://errors.pydantic.dev/{__version__}/v/string_too_short', + 'url': f'https://errors.pydantic.dev/{pydantic_version}/v/string_too_short', } ] ) @@ -472,6 +473,50 @@ def test_error_json(): assert exc_info.value.json(indent=2).startswith('[\n {\n "type": "string_too_short",') +def test_error_json_python_error(pydantic_version: str): + def raise_py_error(v: Any) -> Any: + try: + assert False + except AssertionError as e: + raise ValueError('Oh no!') from e + + s = SchemaValidator(core_schema.no_info_plain_validator_function(raise_py_error)) + with pytest.raises(ValidationError) as exc_info: + s.validate_python('anything') + + exc = exc_info.value.errors()[0]['ctx']['error'] # type: ignore + assert isinstance(exc, ValueError) + assert isinstance(exc.__context__, AssertionError) + + # insert_assert(exc_info.value.errors(include_url=False)) + assert exc_info.value.errors(include_url=False) == [ + { + 'type': 'value_error', + 'loc': (), + 'msg': 'Value error, Oh no!', + 'input': 'anything', + 'ctx': {'error': HasRepr(repr(ValueError('Oh no!')))}, + } + ] + assert exc_info.value.json() == IsJson( + [ + { + 'type': 'value_error', + 'loc': [], + 'msg': 'Value error, Oh no!', + 'input': 'anything', + 'ctx': {'error': 'Oh no!'}, + 'url': f'https://errors.pydantic.dev/{pydantic_version}/v/value_error', + } + ] + ) + assert exc_info.value.json(include_url=False, include_context=False) == IsJson( + [{'type': 'value_error', 'loc': [], 'msg': 'Value error, Oh no!', 'input': 'anything'}] + ) + assert exc_info.value.json().startswith('[{"type":"value_error",') + assert exc_info.value.json(indent=2).startswith('[\n {\n "type": "value_error",') + + def test_error_json_cycle(): s = SchemaValidator({'type': 'str', 'min_length': 3}) cycle = [] @@ -619,7 +664,7 @@ def test_raise_validation_error_custom(): ] -def test_loc_with_dots(): +def test_loc_with_dots(pydantic_version): v = SchemaValidator( core_schema.typed_dict_schema( { @@ -648,5 +693,5 @@ def test_loc_with_dots(): "`foo.bar`.0\n" " Input should be a valid integer, unable to parse string as an integer " "[type=int_parsing, input_value='x', input_type=str]\n" - f' For further information visit https://errors.pydantic.dev/{__version__}/v/int_parsing' + f' For further information visit https://errors.pydantic.dev/{pydantic_version}/v/int_parsing' ) diff --git a/tests/test_misc.py b/tests/test_misc.py index 00ac635ec..a728a5896 100644 --- a/tests/test_misc.py +++ b/tests/test_misc.py @@ -6,7 +6,14 @@ from typing_extensions import get_args from pydantic_core import CoreSchema, CoreSchemaType, PydanticUndefined, core_schema -from pydantic_core._pydantic_core import SchemaError, SchemaValidator, ValidationError, __version__, build_profile +from pydantic_core._pydantic_core import ( + SchemaError, + SchemaValidator, + ValidationError, + __version__, + build_info, + build_profile, +) @pytest.mark.parametrize('obj', [ValidationError, SchemaValidator, SchemaError]) @@ -23,6 +30,10 @@ def test_build_profile(): assert build_profile in ('debug', 'release') +def test_build_info(): + assert isinstance(build_info, str) + + def test_schema_error(): err = SchemaError('test') assert isinstance(err, Exception) @@ -30,7 +41,7 @@ def test_schema_error(): assert repr(err) == 'SchemaError("test")' -def test_validation_error(): +def test_validation_error(pydantic_version): v = SchemaValidator({'type': 'int'}) with pytest.raises(ValidationError) as exc_info: v.validate_python(1.5) @@ -56,7 +67,7 @@ def test_validation_error(): 'loc': (), 'msg': 'Input should be a valid integer, got a number with a fractional part', 'input': 1.5, - 'url': f'https://errors.pydantic.dev/{__version__}/v/int_from_float', + 'url': f'https://errors.pydantic.dev/{pydantic_version}/v/int_from_float', } ] @@ -97,7 +108,7 @@ def test_custom_title(): assert exc_info.value.title == 'MyInt' -def test_validation_error_multiple(): +def test_validation_error_multiple(pydantic_version): class MyModel: # this is not required, but it avoids `__pydantic_fields_set__` being included in `__dict__` __slots__ = '__dict__', '__pydantic_fields_set__', '__pydantic_extra__', '__pydantic_private__' @@ -141,11 +152,11 @@ class MyModel: 'x\n' ' Input should be a valid number, unable to parse string as a number ' "[type=float_parsing, input_value='xxxxxxxxxxxxxxxxxxxxxxxx...xxxxxxxxxxxxxxxxxxxxxxx', input_type=str]\n" - f' For further information visit https://errors.pydantic.dev/{__version__}/v/float_parsing\n' + f' For further information visit https://errors.pydantic.dev/{pydantic_version}/v/float_parsing\n' 'y\n' ' Input should be a valid integer, unable to parse string as an integer ' "[type=int_parsing, input_value='y', input_type=str]\n" - f' For further information visit https://errors.pydantic.dev/{__version__}/v/int_parsing' + f' For further information visit https://errors.pydantic.dev/{pydantic_version}/v/int_parsing' ) diff --git a/tests/validators/test_arguments.py b/tests/validators/test_arguments.py index a78952671..0f634eb54 100644 --- a/tests/validators/test_arguments.py +++ b/tests/validators/test_arguments.py @@ -6,7 +6,7 @@ import pytest -from pydantic_core import ArgsKwargs, SchemaError, SchemaValidator, ValidationError, __version__, core_schema +from pydantic_core import ArgsKwargs, SchemaError, SchemaValidator, ValidationError, core_schema from ..conftest import Err, PyAndJson, plain_repr @@ -984,7 +984,7 @@ def test_invalid_schema(): ) -def test_error_display(): +def test_error_display(pydantic_version): v = SchemaValidator( core_schema.arguments_schema( [ @@ -1013,7 +1013,7 @@ def test_error_display(): "b\n" " Missing required argument [type=missing_argument, " "input_value=ArgsKwargs((), {'a': 1}), input_type=ArgsKwargs]\n" - f" For further information visit https://errors.pydantic.dev/{__version__}/v/missing_argument" + f" For further information visit https://errors.pydantic.dev/{pydantic_version}/v/missing_argument" ) # insert_assert(exc_info.value.json(include_url=False)) assert exc_info.value.json(include_url=False) == ( diff --git a/tests/validators/test_bool.py b/tests/validators/test_bool.py index a193dbb09..714c07ffe 100644 --- a/tests/validators/test_bool.py +++ b/tests/validators/test_bool.py @@ -2,7 +2,7 @@ import pytest -from pydantic_core import SchemaValidator, ValidationError, __version__, core_schema +from pydantic_core import SchemaValidator, ValidationError, core_schema from ..conftest import Err, PyAndJson, plain_repr @@ -53,7 +53,7 @@ def test_bool_strict(py_and_json: PyAndJson): v.validate_test('true') -def test_bool_error(): +def test_bool_error(pydantic_version): v = SchemaValidator({'type': 'bool'}) with pytest.raises(ValidationError) as exc_info: @@ -63,7 +63,7 @@ def test_bool_error(): '1 validation error for bool\n' ' Input should be a valid boolean, ' "unable to interpret input [type=bool_parsing, input_value='wrong', input_type=str]\n" - f" For further information visit https://errors.pydantic.dev/{__version__}/v/bool_parsing" + f" For further information visit https://errors.pydantic.dev/{pydantic_version}/v/bool_parsing" ) assert exc_info.value.errors(include_url=False) == [ { diff --git a/tests/validators/test_definitions_recursive.py b/tests/validators/test_definitions_recursive.py index d190ba02a..06ee6c460 100644 --- a/tests/validators/test_definitions_recursive.py +++ b/tests/validators/test_definitions_recursive.py @@ -6,7 +6,7 @@ from dirty_equals import AnyThing, HasAttributes, IsList, IsPartialDict, IsStr, IsTuple import pydantic_core -from pydantic_core import SchemaError, SchemaValidator, ValidationError, __version__, core_schema +from pydantic_core import SchemaError, SchemaValidator, ValidationError, core_schema from ..conftest import Err, plain_repr from .test_typed_dict import Cls @@ -818,7 +818,7 @@ def test_error_inside_definition_wrapper(): ) -def test_recursive_definitions_schema() -> None: +def test_recursive_definitions_schema(pydantic_version) -> None: s = core_schema.definitions_schema( core_schema.definition_reference_schema(schema_ref='a'), [ @@ -854,7 +854,7 @@ def test_recursive_definitions_schema() -> None: 'loc': ('b', 0, 'a'), 'msg': 'Input should be a valid list', 'input': {}, - 'url': f'https://errors.pydantic.dev/{__version__}/v/list_type', + 'url': f'https://errors.pydantic.dev/{pydantic_version}/v/list_type', } ] @@ -878,7 +878,7 @@ def test_unsorted_definitions_schema() -> None: v.validate_python({'x': 'abc'}) -def test_validate_assignment() -> None: +def test_validate_assignment(pydantic_version) -> None: @dataclass class Model: x: List['Model'] @@ -916,6 +916,6 @@ class Model: 'msg': 'Input should be a dictionary or an instance of Model', 'input': 123, 'ctx': {'class_name': 'Model'}, - 'url': f'https://errors.pydantic.dev/{__version__}/v/dataclass_type', + 'url': f'https://errors.pydantic.dev/{pydantic_version}/v/dataclass_type', } ] diff --git a/tests/validators/test_function.py b/tests/validators/test_function.py index 819b4e44f..03ffcf55d 100644 --- a/tests/validators/test_function.py +++ b/tests/validators/test_function.py @@ -4,6 +4,7 @@ from typing import Any, Dict, Type, Union import pytest +from dirty_equals import HasRepr from pydantic_core import SchemaError, SchemaValidator, ValidationError, core_schema @@ -58,7 +59,7 @@ def f(input_value, info): 'loc': (), 'msg': 'Value error, foobar', 'input': 'input value', - 'ctx': {'error': 'foobar'}, + 'ctx': {'error': HasRepr(repr(ValueError('foobar')))}, } ] @@ -350,7 +351,7 @@ def f(input_value, info): 'loc': (), 'msg': 'Value error, foobar', 'input': 'input value', - 'ctx': {'error': 'foobar'}, + 'ctx': {'error': HasRepr(repr(ValueError('foobar')))}, } ] @@ -551,7 +552,7 @@ def f(input_value, info): 'loc': (), 'msg': 'Assertion failed, foobar', 'input': 'input value', - 'ctx': {'error': 'foobar'}, + 'ctx': {'error': HasRepr(repr(AssertionError('foobar')))}, } ] @@ -571,9 +572,9 @@ def f(input_value, info): { 'type': 'assertion_error', 'loc': (), - 'msg': 'Assertion failed, Unknown error', + 'msg': 'Assertion failed, ', 'input': 'input value', - 'ctx': {'error': 'Unknown error'}, + 'ctx': {'error': HasRepr(repr(AssertionError()))}, } ] diff --git a/tests/validators/test_int.py b/tests/validators/test_int.py index 4697c9941..e4ed26202 100644 --- a/tests/validators/test_int.py +++ b/tests/validators/test_int.py @@ -6,7 +6,7 @@ import pytest from dirty_equals import IsStr -from pydantic_core import SchemaValidator, ValidationError, __version__ +from pydantic_core import SchemaValidator, ValidationError from ..conftest import Err, PyAndJson, plain_repr @@ -340,7 +340,7 @@ def test_int_repr(): assert plain_repr(v).startswith('SchemaValidator(title="constrained-int",validator=ConstrainedInt(') -def test_too_long(): +def test_too_long(pydantic_version): v = SchemaValidator({'type': 'int'}) with pytest.raises(ValidationError) as exc_info: @@ -359,7 +359,7 @@ def test_too_long(): "1 validation error for int\n" " Unable to parse input string as an integer, exceeded maximum size " "[type=int_parsing_size, input_value='111111111111111111111111...11111111111111111111111', input_type=str]\n" - f" For further information visit https://errors.pydantic.dev/{__version__}/v/int_parsing_size" + f" For further information visit https://errors.pydantic.dev/{pydantic_version}/v/int_parsing_size" ) diff --git a/tests/validators/test_list.py b/tests/validators/test_list.py index 71fef927e..4d56a725d 100644 --- a/tests/validators/test_list.py +++ b/tests/validators/test_list.py @@ -248,8 +248,20 @@ def f(input_value, info): with pytest.raises(ValidationError) as exc_info: v.validate_python([1, 2]) assert exc_info.value.errors(include_url=False) == [ - {'type': 'value_error', 'loc': (0,), 'msg': 'Value error, error 1', 'input': 1, 'ctx': {'error': 'error 1'}}, - {'type': 'value_error', 'loc': (1,), 'msg': 'Value error, error 2', 'input': 2, 'ctx': {'error': 'error 2'}}, + { + 'type': 'value_error', + 'loc': (0,), + 'msg': 'Value error, error 1', + 'input': 1, + 'ctx': {'error': HasRepr(repr(ValueError('error 1')))}, + }, + { + 'type': 'value_error', + 'loc': (1,), + 'msg': 'Value error, error 2', + 'input': 2, + 'ctx': {'error': HasRepr(repr(ValueError('error 2')))}, + }, ] diff --git a/tests/validators/test_model.py b/tests/validators/test_model.py index 727c06ca5..d014e446c 100644 --- a/tests/validators/test_model.py +++ b/tests/validators/test_model.py @@ -3,7 +3,7 @@ from typing import Any, Callable, Dict, List, Set, Tuple import pytest -from dirty_equals import IsInstance +from dirty_equals import HasRepr, IsInstance from pydantic_core import SchemaError, SchemaValidator, ValidationError, core_schema @@ -139,7 +139,7 @@ def f( 'loc': (), 'msg': 'Assertion failed, assert 456 == 123', 'input': {'field_a': 456}, - 'ctx': {'error': 'assert 456 == 123'}, + 'ctx': {'error': HasRepr(repr(AssertionError('assert 456 == 123')))}, } ] @@ -173,7 +173,7 @@ def f(input_value: Dict[str, Any], info: core_schema.ValidationInfo): 'loc': (), 'msg': 'Assertion failed, assert 456 == 123', 'input': {'field_a': 456}, - 'ctx': {'error': 'assert 456 == 123'}, + 'ctx': {'error': HasRepr(repr(AssertionError('assert 456 == 123')))}, } ] @@ -208,7 +208,7 @@ def f(input_value_and_fields_set: Tuple[Dict[str, Any], Set[str]]): 'loc': (), 'msg': 'Assertion failed, assert 456 == 123', 'input': {'field_a': 456}, - 'ctx': {'error': 'assert 456 == 123'}, + 'ctx': {'error': HasRepr(repr(AssertionError('assert 456 == 123')))}, } ] @@ -876,7 +876,7 @@ def call_me_maybe(self, context, **kwargs): 'loc': (), 'msg': 'Value error, this is broken: test', 'input': {'field_a': 'test'}, - 'ctx': {'error': 'this is broken: test'}, + 'ctx': {'error': HasRepr(repr(ValueError('this is broken: test')))}, } ] diff --git a/tests/validators/test_model_fields.py b/tests/validators/test_model_fields.py index 515587ba2..c8b4b22ff 100644 --- a/tests/validators/test_model_fields.py +++ b/tests/validators/test_model_fields.py @@ -8,7 +8,7 @@ import pytest from dirty_equals import FunctionCheck, HasRepr, IsStr -from pydantic_core import CoreConfig, SchemaError, SchemaValidator, ValidationError, __version__, core_schema +from pydantic_core import CoreConfig, SchemaError, SchemaValidator, ValidationError, core_schema from ..conftest import Err, PyAndJson @@ -105,7 +105,7 @@ def test_with_default(): ) -def test_missing_error(): +def test_missing_error(pydantic_version): v = SchemaValidator( { 'type': 'model-fields', @@ -123,7 +123,7 @@ def test_missing_error(): 1 validation error for model-fields field_b Field required [type=missing, input_value={{'field_a': b'abc'}}, input_type=dict] - For further information visit https://errors.pydantic.dev/{__version__}/v/missing""" + For further information visit https://errors.pydantic.dev/{pydantic_version}/v/missing""" ) diff --git a/tests/validators/test_typed_dict.py b/tests/validators/test_typed_dict.py index 34ea48e8f..3757e08d7 100644 --- a/tests/validators/test_typed_dict.py +++ b/tests/validators/test_typed_dict.py @@ -5,7 +5,7 @@ import pytest from dirty_equals import FunctionCheck -from pydantic_core import CoreConfig, SchemaError, SchemaValidator, ValidationError, __version__, core_schema +from pydantic_core import CoreConfig, SchemaError, SchemaValidator, ValidationError, core_schema from ..conftest import Err, PyAndJson @@ -90,7 +90,7 @@ def test_with_default(): assert v.validate_python({'field_a': b'abc', 'field_b': 1}) == {'field_a': 'abc', 'field_b': 1} -def test_missing_error(): +def test_missing_error(pydantic_version): v = SchemaValidator( { 'type': 'typed-dict', @@ -107,7 +107,7 @@ def test_missing_error(): "1 validation error for typed-dict\n" "field_b\n" " Field required [type=missing, input_value={'field_a': b'abc'}, input_type=dict]\n" - f" For further information visit https://errors.pydantic.dev/{__version__}/v/missing" + f" For further information visit https://errors.pydantic.dev/{pydantic_version}/v/missing" ) diff --git a/tests/validators/test_union.py b/tests/validators/test_union.py index e580550d1..4f16b4479 100644 --- a/tests/validators/test_union.py +++ b/tests/validators/test_union.py @@ -1,7 +1,7 @@ import pytest from dirty_equals import IsFloat, IsInt -from pydantic_core import SchemaError, SchemaValidator, ValidationError, __version__, core_schema +from pydantic_core import SchemaError, SchemaValidator, ValidationError, core_schema from ..conftest import plain_repr @@ -232,7 +232,7 @@ def test_union_list_bool_int(): ] -def test_no_choices(): +def test_no_choices(pydantic_version): with pytest.raises(SchemaError) as exc_info: SchemaValidator({'type': 'union'}) @@ -240,7 +240,7 @@ def test_no_choices(): 'Invalid Schema:\n' 'union.choices\n' " Field required [type=missing, input_value={'type': 'union'}, input_type=dict]\n" - f' For further information visit https://errors.pydantic.dev/{__version__}/v/missing' + f' For further information visit https://errors.pydantic.dev/{pydantic_version}/v/missing' ) assert exc_info.value.error_count() == 1 assert exc_info.value.errors() == [