From 0194c954a2bf3b1bfdb814fadfea009007b7154b Mon Sep 17 00:00:00 2001 From: Markus Sintonen Date: Mon, 24 Jul 2023 10:20:07 +0300 Subject: [PATCH 1/5] Fix error context handling for predefined errors --- src/errors/mod.rs | 2 +- src/errors/types.rs | 702 +++++++++++++---------------- src/errors/validation_exception.rs | 4 +- src/errors/value_exception.rs | 9 +- src/input/datetime.rs | 8 + src/input/input_json.rs | 101 +++-- src/input/input_python.rs | 124 ++--- src/input/return_enums.rs | 25 +- src/input/shared.rs | 19 +- src/validators/arguments.rs | 16 +- src/validators/bytes.rs | 16 +- src/validators/callable.rs | 4 +- src/validators/dataclass.rs | 18 +- src/validators/date.rs | 12 +- src/validators/datetime.rs | 22 +- src/validators/definitions.rs | 10 +- src/validators/float.rs | 39 +- src/validators/function.rs | 1 + src/validators/generator.rs | 2 + src/validators/int.rs | 30 +- src/validators/is_instance.rs | 1 + src/validators/is_subclass.rs | 1 + src/validators/list.rs | 3 + src/validators/literal.rs | 1 + src/validators/model.rs | 5 +- src/validators/model_fields.rs | 15 +- src/validators/none.rs | 4 +- src/validators/string.rs | 17 +- src/validators/time.rs | 1 + src/validators/timedelta.rs | 1 + src/validators/tuple.rs | 5 +- src/validators/typed_dict.rs | 9 +- src/validators/union.rs | 2 + src/validators/url.rs | 65 ++- src/validators/uuid.rs | 43 +- tests/test_errors.py | 20 +- 36 files changed, 758 insertions(+), 599 deletions(-) diff --git a/src/errors/mod.rs b/src/errors/mod.rs index 0983108e8..c9181468a 100644 --- a/src/errors/mod.rs +++ b/src/errors/mod.rs @@ -8,7 +8,7 @@ mod value_exception; pub use self::line_error::{InputValue, ValError, ValLineError, ValResult}; pub use self::location::LocItem; -pub use self::types::{list_all_errors, ErrorMode, ErrorType}; +pub use self::types::{list_all_errors, ErrorMode, ErrorType, ErrorTypeDefaults}; pub use self::validation_exception::ValidationError; pub use self::value_exception::{PydanticCustomError, PydanticKnownError, PydanticOmit, PydanticUseDefault}; diff --git a/src/errors/types.rs b/src/errors/types.rs index 67a4da39b..78822c24b 100644 --- a/src/errors/types.rs +++ b/src/errors/types.rs @@ -58,263 +58,351 @@ pub fn list_all_errors(py: Python) -> PyResult<&PyList> { Ok(PyList::new(py, errors)) } -/// Definite each validation error. -/// NOTE: if an error has parameters: -/// * the variables in the message need to match the enum struct -/// * you need to add an entry to the `render` enum to render the error message as a template -/// * you need to add an entry to the `py_dict` enum to generate `ctx` for error messages -#[derive(Clone, Debug, Display, EnumMessage, EnumIter)] -#[strum(serialize_all = "snake_case")] -pub enum ErrorType { +fn do_nothing(v: T) -> T { + v +} + +macro_rules! basic_error_default { + ( + $item:ident $(,)? + ) => { + pub const $item: ErrorType = ErrorType::$item { context: None }; + }; + ( + $item:ident, $($key:ident),* $(,)? + ) => {}; // With more parameters enum item must be explicitly created +} + +macro_rules! error_types { + ( + $( + $item:ident { + $($key:ident: {ctx_type: $ctx_type:ty, ctx_fn: $ctx_fn:path, extract_type: $extract_type:ty}),* $(,)? + }, + )+ + ) => { + #[derive(Clone, Debug, Display, EnumMessage, EnumIter)] + #[strum(serialize_all = "snake_case")] + pub enum ErrorType { + $( + $item { + context: Option>, + $($key: $ctx_type,)* + } + ),+, + } + impl ErrorType { + pub fn new(py: Python, value: &str, context: Option<&PyDict>) -> PyResult { + let lookup = ERROR_TYPE_LOOKUP.get_or_init(py, Self::build_lookup); + let error_type = match lookup.get(value) { + Some(error_type) => error_type.clone(), + None => return py_err!(PyKeyError; "Invalid error type: '{}'", value), + }; + match error_type { + $( + Self::$item { .. } => { + Ok(Self::$item { + context: context.map(|c| c.into_py(py)), + $( + $key: $ctx_fn( + context + .ok_or(py_error_type!(PyTypeError; "{}: '{}' required in context", stringify!($item), stringify!($key)))? + .get_item(stringify!($key)) + .ok_or(py_error_type!(PyTypeError; "{}: '{}' required in context", stringify!($item), stringify!($key)))? + .extract::<$extract_type>() + .map_err(|_| py_error_type!(PyTypeError; "{}: '{}' context value must be a {}", stringify!($item), stringify!($key), stringify!($extract_type)))? + ), + )* + }) + }, + )+ + } + } + + fn py_dict_merge_ctx(&self, py: Python, dict: &PyDict) -> PyResult<()> { + match self { + $( + Self::$item { context, $($key,)* } => { + $( + dict.set_item::<&str, Py>(stringify!($key), $key.to_object(py))?; + )* + if let Some(ctx) = context { + dict.update(ctx.as_ref(py).downcast()?)? + } + Ok(()) + }, + )+ + } + } + } + + pub struct ErrorTypeDefaults {} + // Allow unused default constants as they are generated by macro. + // Also allow camel case as constants so we dont need to do case conversion of macro + // generated names. Enums are also then easier to find when searching. + #[allow(dead_code, non_upper_case_globals)] + impl ErrorTypeDefaults { + $( + basic_error_default!($item, $($key),*); + )+ + } + }; +} + +// Definite each validation error. +// NOTE: if an error has parameters: +// * the variables in the message need to match the enum struct +// * you need to add an entry to the `render` enum to render the error message as a template +error_types! { // --------------------- // Assignment errors NoSuchAttribute { - attribute: String, + attribute: {ctx_type: String, ctx_fn: do_nothing, extract_type: String}, }, // --------------------- // JSON errors JsonInvalid { - error: String, + error: {ctx_type: String, ctx_fn: do_nothing, extract_type: String}, }, - JsonType, + JsonType {}, // --------------------- // recursion error - RecursionLoop, + RecursionLoop {}, // --------------------- // typed dict specific errors - Missing, - FrozenField, - FrozenInstance, - ExtraForbidden, - InvalidKey, + Missing {}, + FrozenField {}, + FrozenInstance {}, + ExtraForbidden {}, + InvalidKey {}, GetAttributeError { - error: String, + error: {ctx_type: String, ctx_fn: do_nothing, extract_type: String}, }, // --------------------- // model class specific errors ModelType { - class_name: String, + class_name: {ctx_type: String, ctx_fn: do_nothing, extract_type: String}, }, - ModelAttributesType, + ModelAttributesType {}, // --------------------- // dataclass errors (we don't talk about ArgsKwargs here for simplicity) DataclassType { - class_name: String, + class_name: {ctx_type: String, ctx_fn: do_nothing, extract_type: String}, }, DataclassExactType { - class_name: String, + class_name: {ctx_type: String, ctx_fn: do_nothing, extract_type: String}, }, // --------------------- // None errors - NoneRequired, + NoneRequired {}, // --------------------- // generic comparison errors - used for all inequality comparisons except int and float which have their // own type, bounds arguments are Strings so they can be created from any type GreaterThan { - gt: Number, + gt: {ctx_type: Number, ctx_fn: do_nothing, extract_type: Number}, }, GreaterThanEqual { - ge: Number, + ge: {ctx_type: Number, ctx_fn: do_nothing, extract_type: Number}, }, LessThan { - lt: Number, + lt: {ctx_type: Number, ctx_fn: do_nothing, extract_type: Number}, }, LessThanEqual { - le: Number, + le: {ctx_type: Number, ctx_fn: do_nothing, extract_type: Number}, }, MultipleOf { - multiple_of: Number, + multiple_of: {ctx_type: Number, ctx_fn: do_nothing, extract_type: Number}, }, - FiniteNumber, + FiniteNumber {}, // --------------------- // generic length errors - used for everything with a length except strings and bytes which need custom messages TooShort { - field_type: String, - min_length: usize, - actual_length: usize, + field_type: {ctx_type: String, ctx_fn: do_nothing, extract_type: String}, + min_length: {ctx_type: usize, ctx_fn: do_nothing, extract_type: usize}, + actual_length: {ctx_type: usize, ctx_fn: do_nothing, extract_type: usize}, }, TooLong { - field_type: String, - max_length: usize, - actual_length: usize, + field_type: {ctx_type: String, ctx_fn: do_nothing, extract_type: String}, + max_length: {ctx_type: usize, ctx_fn: do_nothing, extract_type: usize}, + actual_length: {ctx_type: usize, ctx_fn: do_nothing, extract_type: usize}, }, // --------------------- // generic collection and iteration errors - IterableType, + IterableType {}, IterationError { - error: String, + error: {ctx_type: String, ctx_fn: do_nothing, extract_type: String}, }, // --------------------- // string errors - StringType, - StringSubType, - StringUnicode, + StringType {}, + StringSubType {}, + StringUnicode {}, StringTooShort { - min_length: usize, + min_length: {ctx_type: usize, ctx_fn: do_nothing, extract_type: usize}, }, StringTooLong { - max_length: usize, + max_length: {ctx_type: usize, ctx_fn: do_nothing, extract_type: usize}, }, StringPatternMismatch { - pattern: String, + pattern: {ctx_type: String, ctx_fn: do_nothing, extract_type: String}, }, // --------------------- // enum errors Enum { - expected: String, + expected: {ctx_type: String, ctx_fn: do_nothing, extract_type: String}, }, // --------------------- // dict errors - DictType, + DictType {}, MappingType { - error: Cow<'static, str>, + error: {ctx_type: Cow<'static, str>, ctx_fn: Cow::Owned, extract_type: String}, }, // --------------------- // list errors - ListType, + ListType {}, // --------------------- // tuple errors - TupleType, + TupleType {}, // --------------------- // set errors - SetType, + SetType {}, // --------------------- // bool errors - BoolType, - BoolParsing, + BoolType {}, + BoolParsing {}, // --------------------- // int errors - IntType, - IntParsing, - IntParsingSize, - IntFromFloat, + IntType {}, + IntParsing {}, + IntParsingSize {}, + IntFromFloat {}, // --------------------- // float errors - FloatType, - FloatParsing, + FloatType {}, + FloatParsing {}, // --------------------- // bytes errors - BytesType, + BytesType {}, BytesTooShort { - min_length: usize, + min_length: {ctx_type: usize, ctx_fn: do_nothing, extract_type: usize}, }, BytesTooLong { - max_length: usize, + max_length: {ctx_type: usize, ctx_fn: do_nothing, extract_type: usize}, }, // --------------------- // python errors from functions ValueError { - error: Option, // Use Option because EnumIter requires Default to be implemented + error: {ctx_type: Option, ctx_fn: do_nothing, extract_type: Option}, // Use Option because EnumIter requires Default to be implemented }, AssertionError { - error: Option, // Use Option because EnumIter requires Default to be implemented + error: {ctx_type: Option, ctx_fn: do_nothing, extract_type: Option}, // Use Option because EnumIter requires Default to be implemented }, // Note: strum message and serialize are not used here CustomError { - custom_error: PydanticCustomError, + custom_error: {ctx_type: PydanticCustomError, ctx_fn: do_nothing, extract_type: PydanticCustomError}, }, // --------------------- // literals LiteralError { - expected: String, + expected: {ctx_type: String, ctx_fn: do_nothing, extract_type: String}, }, // --------------------- // date errors - DateType, + DateType {}, DateParsing { - error: Cow<'static, str>, + error: {ctx_type: Cow<'static, str>, ctx_fn: Cow::Owned, extract_type: String}, }, DateFromDatetimeParsing { - error: String, + error: {ctx_type: String, ctx_fn: do_nothing, extract_type: String}, }, - DateFromDatetimeInexact, - DatePast, - DateFuture, + DateFromDatetimeInexact {}, + DatePast {}, + DateFuture {}, // --------------------- // date errors - TimeType, + TimeType {}, TimeParsing { - error: Cow<'static, str>, + error: {ctx_type: Cow<'static, str>, ctx_fn: Cow::Owned, extract_type: String}, }, // --------------------- // datetime errors - DatetimeType, + DatetimeType {}, DatetimeParsing { - error: Cow<'static, str>, + error: {ctx_type: Cow<'static, str>, ctx_fn: Cow::Owned, extract_type: String}, }, DatetimeObjectInvalid { - error: String, + error: {ctx_type: String, ctx_fn: do_nothing, extract_type: String}, }, - DatetimePast, - DatetimeFuture, + DatetimePast {}, + DatetimeFuture {}, // --------------------- // timezone errors - TimezoneNaive, - TimezoneAware, + TimezoneNaive {}, + TimezoneAware {}, TimezoneOffset { - tz_expected: i32, - tz_actual: i32, + tz_expected: {ctx_type: i32, ctx_fn: do_nothing, extract_type: i32}, + tz_actual: {ctx_type: i32, ctx_fn: do_nothing, extract_type: i32}, }, // --------------------- // timedelta errors - TimeDeltaType, + TimeDeltaType {}, TimeDeltaParsing { - error: Cow<'static, str>, + error: {ctx_type: Cow<'static, str>, ctx_fn: Cow::Owned, extract_type: String}, }, // --------------------- // frozenset errors - FrozenSetType, + FrozenSetType {}, // --------------------- // introspection types - e.g. isinstance, callable IsInstanceOf { - class: String, + class: {ctx_type: String, ctx_fn: do_nothing, extract_type: String}, }, IsSubclassOf { - class: String, + class: {ctx_type: String, ctx_fn: do_nothing, extract_type: String}, }, - CallableType, + CallableType {}, // --------------------- // union errors UnionTagInvalid { - discriminator: String, - tag: String, - expected_tags: String, + discriminator: {ctx_type: String, ctx_fn: do_nothing, extract_type: String}, + tag: {ctx_type: String, ctx_fn: do_nothing, extract_type: String}, + expected_tags: {ctx_type: String, ctx_fn: do_nothing, extract_type: String}, }, UnionTagNotFound { - discriminator: String, + discriminator: {ctx_type: String, ctx_fn: do_nothing, extract_type: String}, }, // --------------------- // argument errors - ArgumentsType, - MissingArgument, - UnexpectedKeywordArgument, - MissingKeywordOnlyArgument, - UnexpectedPositionalArgument, - MissingPositionalOnlyArgument, - MultipleArgumentValues, + ArgumentsType {}, + MissingArgument {}, + UnexpectedKeywordArgument {}, + MissingKeywordOnlyArgument {}, + UnexpectedPositionalArgument {}, + MissingPositionalOnlyArgument {}, + MultipleArgumentValues {}, // --------------------- // URL errors - UrlType, + UrlType {}, UrlParsing { // would be great if this could be a static cow, waiting for https://github.com/servo/rust-url/issues/801 - error: String, + error: {ctx_type: String, ctx_fn: do_nothing, extract_type: String}, }, UrlSyntaxViolation { - error: Cow<'static, str>, + error: {ctx_type: Cow<'static, str>, ctx_fn: Cow::Owned, extract_type: String}, }, UrlTooLong { - max_length: usize, + max_length: {ctx_type: usize, ctx_fn: do_nothing, extract_type: usize}, }, UrlScheme { - expected_schemes: String, + expected_schemes: {ctx_type: String, ctx_fn: do_nothing, extract_type: String}, }, // UUID errors, - UuidType, + UuidType {}, UuidParsing { - error: String, + error: {ctx_type: String, ctx_fn: do_nothing, extract_type: String}, }, UuidVersion { - expected_version: usize, + expected_version: {ctx_type: usize, ctx_fn: do_nothing, extract_type: usize}, }, } @@ -340,46 +428,6 @@ macro_rules! to_string_render { }; } -macro_rules! py_dict { - ($py:ident, $($value:expr),* $(,)?) => {{ - let dict = PyDict::new($py); - $( - dict.set_item::<&str, Py>(stringify!($value), $value.to_object($py))?; - )* - Ok(Some(dict.into_py($py))) - }}; -} - -fn do_nothing(v: T) -> T { - v -} - -macro_rules! extract_context { - ($type:ident, $context:ident, $($key:ident: $type_:ty),* $(,)?) => { - extract_context!(do_nothing, $type, $context, $($key: $type_,)*) - }; - ($function:path, $type:ident, $context:ident, $($key:ident: $type_:ty),* $(,)?) => {{ - let context = match $context { - Some(context) => context, - None => { - let context_parts = [$(format!("{}: {}", stringify!($key), stringify!($type_)),)*]; - return py_err!(PyTypeError; "{} requires context: {{{}}}", stringify!($type), context_parts.join(", ")); - } - }; - Ok(Self::$type{ - $( - $key: $function( - context - .get_item(stringify!($key)) - .ok_or(py_error_type!(PyTypeError; "{}: '{}' required in context", stringify!($type), stringify!($key)))? - .extract::<$type_>() - .map_err(|_| py_error_type!(PyTypeError; "{}: '{}' context value must be a {}", stringify!($type), stringify!($key), stringify!($type_)))? - ), - )* - }) - }}; -} - fn plural_s(value: usize) -> &'static str { if value == 1 { "" @@ -391,201 +439,125 @@ fn plural_s(value: usize) -> &'static str { static ERROR_TYPE_LOOKUP: GILOnceCell> = GILOnceCell::new(); impl ErrorType { - /// create an new ErrorType from python, no_coverage since coverage doesn't work properly here due to the macro - #[cfg_attr(has_no_coverage, no_coverage)] - pub fn new(py: Python, value: &str, ctx: Option<&PyDict>) -> PyResult { - let lookup = ERROR_TYPE_LOOKUP.get_or_init(py, Self::build_lookup); - let error_type = match lookup.get(value) { - Some(error_type) => error_type.clone(), - None => return py_err!(PyKeyError; "Invalid error type: '{}'", value), - }; - match error_type { - Self::NoSuchAttribute { .. } => extract_context!(NoSuchAttribute, ctx, attribute: String), - Self::JsonInvalid { .. } => extract_context!(JsonInvalid, ctx, error: String), - Self::GetAttributeError { .. } => extract_context!(GetAttributeError, ctx, error: String), - Self::ModelType { .. } => extract_context!(ModelType, ctx, class_name: String), - Self::DataclassType { .. } => extract_context!(DataclassType, ctx, class_name: String), - Self::DataclassExactType { .. } => extract_context!(DataclassExactType, ctx, class_name: String), - Self::GreaterThan { .. } => extract_context!(GreaterThan, ctx, gt: Number), - Self::GreaterThanEqual { .. } => extract_context!(GreaterThanEqual, ctx, ge: Number), - Self::LessThan { .. } => extract_context!(LessThan, ctx, lt: Number), - Self::LessThanEqual { .. } => extract_context!(LessThanEqual, ctx, le: Number), - Self::MultipleOf { .. } => extract_context!(MultipleOf, ctx, multiple_of: Number), - Self::TooShort { .. } => extract_context!( - TooShort, - ctx, - field_type: String, - min_length: usize, - actual_length: usize - ), - Self::TooLong { .. } => extract_context!( - TooLong, - ctx, - field_type: String, - max_length: usize, - actual_length: usize - ), - 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: 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), - Self::TimeParsing { .. } => extract_context!(Cow::Owned, TimeParsing, ctx, error: String), - Self::DatetimeParsing { .. } => extract_context!(Cow::Owned, DatetimeParsing, ctx, error: String), - Self::DatetimeObjectInvalid { .. } => extract_context!(DatetimeObjectInvalid, ctx, error: String), - Self::TimezoneOffset { .. } => { - extract_context!(TimezoneOffset, ctx, tz_expected: i32, tz_actual: i32) - } - Self::TimeDeltaParsing { .. } => extract_context!(Cow::Owned, TimeDeltaParsing, ctx, error: String), - Self::IsInstanceOf { .. } => extract_context!(IsInstanceOf, ctx, class: String), - Self::IsSubclassOf { .. } => extract_context!(IsSubclassOf, ctx, class: String), - Self::UnionTagInvalid { .. } => extract_context!( - UnionTagInvalid, - ctx, - discriminator: String, - tag: String, - expected_tags: String - ), - Self::UnionTagNotFound { .. } => extract_context!(UnionTagNotFound, ctx, discriminator: String), - Self::UrlParsing { .. } => extract_context!(UrlParsing, ctx, error: String), - Self::UrlSyntaxViolation { .. } => extract_context!(Cow::Owned, UrlSyntaxViolation, ctx, error: String), - Self::UrlTooLong { .. } => extract_context!(UrlTooLong, ctx, max_length: usize), - Self::UrlScheme { .. } => extract_context!(UrlScheme, ctx, expected_schemes: String), - Self::UuidParsing { .. } => extract_context!(UuidParsing, ctx, error: String), - Self::UuidVersion { .. } => { - extract_context!(UuidVersion, ctx, expected_version: usize) - } - _ => { - if ctx.is_some() { - py_err!(PyTypeError; "'{}' errors do not require context", value) - } else { - Ok(error_type) - } - } - } - } - pub fn new_custom_error(custom_error: PydanticCustomError) -> Self { - Self::CustomError { custom_error } + Self::CustomError { + custom_error, + context: None, + } } pub fn message_template_python(&self) -> &'static str { match self { Self::NoSuchAttribute {..} => "Object has no attribute '{attribute}'", Self::JsonInvalid {..} => "Invalid JSON: {error}", - Self::JsonType => "JSON input should be string, bytes or bytearray", - Self::RecursionLoop => "Recursion error - cyclic reference detected", - Self::Missing => "Field required", - Self::FrozenField => "Field is frozen", - Self::FrozenInstance => "Instance is frozen", - Self::ExtraForbidden => "Extra inputs are not permitted", - Self::InvalidKey => "Keys should be strings", + Self::JsonType {..} => "JSON input should be string, bytes or bytearray", + Self::RecursionLoop {..} => "Recursion error - cyclic reference detected", + Self::Missing {..} => "Field required", + Self::FrozenField {..} => "Field is frozen", + Self::FrozenInstance {..} => "Instance is frozen", + Self::ExtraForbidden {..} => "Extra inputs are not permitted", + Self::InvalidKey {..} => "Keys should be strings", Self::GetAttributeError {..} => "Error extracting attribute: {error}", Self::ModelType {..} => "Input should be a valid dictionary or instance of {class_name}", - Self::ModelAttributesType => "Input should be a valid dictionary or object to extract fields from", + Self::ModelAttributesType {..} => "Input should be a valid dictionary or object to extract fields from", Self::DataclassType {..} => "Input should be a dictionary or an instance of {class_name}", Self::DataclassExactType {..} => "Input should be an instance of {class_name}", - Self::NoneRequired => "Input should be None", + Self::NoneRequired {..} => "Input should be None", Self::GreaterThan {..} => "Input should be greater than {gt}", Self::GreaterThanEqual {..} => "Input should be greater than or equal to {ge}", Self::LessThan {..} => "Input should be less than {lt}", Self::LessThanEqual {..} => "Input should be less than or equal to {le}", Self::MultipleOf {..} => "Input should be a multiple of {multiple_of}", - Self::FiniteNumber => "Input should be a finite number", + Self::FiniteNumber {..} => "Input should be a finite number", Self::TooShort {..} => "{field_type} should have at least {min_length} item{expected_plural} after validation, not {actual_length}", Self::TooLong {..} => "{field_type} should have at most {max_length} item{expected_plural} after validation, not {actual_length}", - Self::IterableType => "Input should be iterable", + Self::IterableType {..} => "Input should be iterable", Self::IterationError {..} => "Error iterating over object, error: {error}", - Self::StringType => "Input should be a valid string", - Self::StringSubType => "Input should be a string, not an instance of a subclass of str", - Self::StringUnicode => "Input should be a valid string, unable to parse raw data as a unicode string", + Self::StringType {..} => "Input should be a valid string", + Self::StringSubType {..} => "Input should be a string, not an instance of a subclass of str", + Self::StringUnicode {..} => "Input should be a valid string, unable to parse raw data as a unicode string", 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::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", - Self::TupleType => "Input should be a valid tuple", - Self::SetType => "Input should be a valid set", - Self::BoolType => "Input should be a valid boolean", - Self::BoolParsing => "Input should be a valid boolean, unable to interpret input", - Self::IntType => "Input should be a valid integer", - Self::IntParsing => "Input should be a valid integer, unable to parse string as an integer", - Self::IntFromFloat => "Input should be a valid integer, got a number with a fractional part", - Self::IntParsingSize => "Unable to parse input string as an integer, exceeded maximum size", - Self::FloatType => "Input should be a valid number", - Self::FloatParsing => "Input should be a valid number, unable to parse string as a number", - Self::BytesType => "Input should be a valid bytes", + Self::ListType {..} => "Input should be a valid list", + Self::TupleType {..} => "Input should be a valid tuple", + Self::SetType {..} => "Input should be a valid set", + Self::BoolType {..} => "Input should be a valid boolean", + Self::BoolParsing {..} => "Input should be a valid boolean, unable to interpret input", + Self::IntType {..} => "Input should be a valid integer", + Self::IntParsing {..} => "Input should be a valid integer, unable to parse string as an integer", + Self::IntFromFloat {..} => "Input should be a valid integer, got a number with a fractional part", + Self::IntParsingSize {..} => "Unable to parse input string as an integer, exceeded maximum size", + Self::FloatType {..} => "Input should be a valid number", + Self::FloatParsing {..} => "Input should be a valid number, unable to parse string as a number", + Self::BytesType {..} => "Input should be a valid bytes", Self::BytesTooShort {..} => "Data should have at least {min_length} bytes", Self::BytesTooLong {..} => "Data should have at most {max_length} bytes", Self::ValueError {..} => "Value error, {error}", Self::AssertionError {..} => "Assertion failed, {error}", Self::CustomError {..} => "", // custom errors are handled separately Self::LiteralError {..} => "Input should be {expected}", - Self::DateType => "Input should be a valid date", + Self::DateType {..} => "Input should be a valid date", Self::DateParsing {..} => "Input should be a valid date in the format YYYY-MM-DD, {error}", Self::DateFromDatetimeParsing {..} => "Input should be a valid date or datetime, {error}", - Self::DateFromDatetimeInexact => "Datetimes provided to dates should have zero time - e.g. be exact dates", - Self::DatePast => "Date should be in the past", - Self::DateFuture => "Date should be in the future", - Self::TimeType => "Input should be a valid time", + Self::DateFromDatetimeInexact {..} => "Datetimes provided to dates should have zero time - e.g. be exact dates", + Self::DatePast {..} => "Date should be in the past", + Self::DateFuture {..} => "Date should be in the future", + Self::TimeType {..} => "Input should be a valid time", Self::TimeParsing {..} => "Input should be in a valid time format, {error}", - Self::DatetimeType => "Input should be a valid datetime", + Self::DatetimeType {..} => "Input should be a valid datetime", Self::DatetimeParsing {..} => "Input should be a valid datetime, {error}", Self::DatetimeObjectInvalid {..} => "Invalid datetime object, got {error}", - Self::DatetimePast => "Input should be in the past", - Self::DatetimeFuture => "Input should be in the future", - Self::TimezoneNaive => "Input should not have timezone info", - Self::TimezoneAware => "Input should have timezone info", + Self::DatetimePast {..} => "Input should be in the past", + Self::DatetimeFuture {..} => "Input should be in the future", + Self::TimezoneNaive {..} => "Input should not have timezone info", + Self::TimezoneAware {..} => "Input should have timezone info", Self::TimezoneOffset {..} => "Timezone offset of {tz_expected} required, got {tz_actual}", - Self::TimeDeltaType => "Input should be a valid timedelta", + Self::TimeDeltaType {..} => "Input should be a valid timedelta", Self::TimeDeltaParsing {..} => "Input should be a valid timedelta, {error}", - Self::FrozenSetType => "Input should be a valid frozenset", + Self::FrozenSetType {..} => "Input should be a valid frozenset", Self::IsInstanceOf {..} => "Input should be an instance of {class}", Self::IsSubclassOf {..} => "Input should be a subclass of {class}", - Self::CallableType => "Input should be callable", + Self::CallableType {..} => "Input should be callable", Self::UnionTagInvalid {..} => "Input tag '{tag}' found using {discriminator} does not match any of the expected tags: {expected_tags}", Self::UnionTagNotFound {..} => "Unable to extract tag using discriminator {discriminator}", - Self::ArgumentsType => "Arguments must be a tuple, list or a dictionary", - Self::MissingArgument => "Missing required argument", - Self::UnexpectedKeywordArgument => "Unexpected keyword argument", - Self::MissingKeywordOnlyArgument => "Missing required keyword only argument", - Self::UnexpectedPositionalArgument => "Unexpected positional argument", - Self::MissingPositionalOnlyArgument => "Missing required positional only argument", - Self::MultipleArgumentValues => "Got multiple values for argument", - Self::UrlType => "URL input should be a string or URL", + Self::ArgumentsType {..} => "Arguments must be a tuple, list or a dictionary", + Self::MissingArgument {..} => "Missing required argument", + Self::UnexpectedKeywordArgument {..} => "Unexpected keyword argument", + Self::MissingKeywordOnlyArgument {..} => "Missing required keyword only argument", + Self::UnexpectedPositionalArgument {..} => "Unexpected positional argument", + Self::MissingPositionalOnlyArgument {..} => "Missing required positional only argument", + Self::MultipleArgumentValues {..} => "Got multiple values for argument", + Self::UrlType {..} => "URL input should be a string or URL", Self::UrlParsing {..} => "Input should be a valid URL, {error}", Self::UrlSyntaxViolation {..} => "Input violated strict URL syntax rules, {error}", Self::UrlTooLong {..} => "URL should have at most {max_length} characters", Self::UrlScheme {..} => "URL scheme should be {expected_schemes}", - Self::UuidType => "UUID input should be a string, bytes or UUID object", - Self::UuidParsing { .. } => "Input should be a valid UUID, {error}", - Self::UuidVersion { .. } => "UUID version {expected_version} expected" + Self::UuidType{..} => "UUID input should be a string, bytes or UUID object", + Self::UuidParsing {..} => "Input should be a valid UUID, {error}", + Self::UuidVersion {..} => "UUID version {expected_version} expected" } } pub fn message_template_json(&self) -> &'static str { match self { - Self::NoneRequired => "Input should be null", - Self::ListType | Self::TupleType | Self::IterableType | Self::SetType | Self::FrozenSetType => { - "Input should be a valid array" - } - Self::ModelType { .. } | Self::ModelAttributesType | Self::DictType | Self::DataclassType { .. } => { - "Input should be an object" - } - Self::TimeDeltaType => "Input should be a valid duration", - Self::TimeDeltaParsing { .. } => "Input should be a valid duration, {error}", - Self::ArgumentsType => "Arguments must be an array or an object", + Self::NoneRequired {..} => "Input should be null", + Self::ListType {..} + | Self::TupleType {..} + | Self::IterableType {..} + | Self::SetType {..} + | Self::FrozenSetType {..} => "Input should be a valid array", + Self::ModelType {..} + | Self::ModelAttributesType {..} + | Self::DictType {..} + | Self::DataclassType {..} => "Input should be an object", + Self::TimeDeltaType {..} => "Input should be a valid duration", + Self::TimeDeltaParsing {..} => "Input should be a valid duration, {error}", + Self::ArgumentsType {..} => "Arguments must be an array or an object", _ => self.message_template_python(), } } @@ -609,6 +581,7 @@ impl ErrorType { match self { Self::CustomError { custom_error: value_error, + .. } => value_error.error_type(), _ => self.to_string(), } @@ -620,21 +593,22 @@ impl ErrorType { ErrorMode::Json => self.message_template_json(), }; match self { - Self::NoSuchAttribute { attribute } => render!(tmpl, attribute), - Self::JsonInvalid { error } => render!(tmpl, error), - Self::GetAttributeError { error } => render!(tmpl, error), - Self::ModelType { class_name } => render!(tmpl, class_name), - Self::DataclassType { class_name } => render!(tmpl, class_name), - Self::DataclassExactType { class_name } => render!(tmpl, class_name), - Self::GreaterThan { gt } => to_string_render!(tmpl, gt), - Self::GreaterThanEqual { ge } => to_string_render!(tmpl, ge), - Self::LessThan { lt } => to_string_render!(tmpl, lt), - Self::LessThanEqual { le } => to_string_render!(tmpl, le), - Self::MultipleOf { multiple_of } => to_string_render!(tmpl, multiple_of), + Self::NoSuchAttribute { attribute, .. } => render!(tmpl, attribute), + Self::JsonInvalid { error, .. } => render!(tmpl, error), + Self::GetAttributeError { error, .. } => render!(tmpl, error), + Self::ModelType { class_name, .. } => render!(tmpl, class_name), + Self::DataclassType { class_name, .. } => render!(tmpl, class_name), + Self::DataclassExactType { class_name, .. } => render!(tmpl, class_name), + Self::GreaterThan { gt, .. } => to_string_render!(tmpl, gt), + Self::GreaterThanEqual { ge, .. } => to_string_render!(tmpl, ge), + Self::LessThan { lt, .. } => to_string_render!(tmpl, lt), + Self::LessThanEqual { le, .. } => to_string_render!(tmpl, le), + Self::MultipleOf { multiple_of, .. } => to_string_render!(tmpl, multiple_of), Self::TooShort { field_type, min_length, actual_length, + .. } => { let expected_plural = plural_s(*min_length); to_string_render!(tmpl, field_type, min_length, actual_length, expected_plural) @@ -643,18 +617,19 @@ impl ErrorType { field_type, max_length, actual_length, + .. } => { let expected_plural = plural_s(*max_length); to_string_render!(tmpl, field_type, max_length, actual_length, expected_plural) } - Self::IterationError { error } => render!(tmpl, error), - 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::IterationError { error, .. } => render!(tmpl, error), + 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, .. } => { let error = &error .as_ref() @@ -669,94 +644,53 @@ impl ErrorType { } Self::CustomError { custom_error: value_error, + .. } => value_error.message(py), - Self::LiteralError { expected } => render!(tmpl, expected), - Self::DateParsing { error } => render!(tmpl, error), - Self::DateFromDatetimeParsing { error } => render!(tmpl, error), - Self::TimeParsing { error } => render!(tmpl, error), - Self::DatetimeParsing { error } => render!(tmpl, error), - Self::DatetimeObjectInvalid { error } => render!(tmpl, error), - Self::TimezoneOffset { tz_expected, tz_actual } => to_string_render!(tmpl, tz_expected, tz_actual), - Self::TimeDeltaParsing { error } => render!(tmpl, error), - Self::IsInstanceOf { class } => render!(tmpl, class), - Self::IsSubclassOf { class } => render!(tmpl, class), + Self::LiteralError { expected, .. } => render!(tmpl, expected), + Self::DateParsing { error, .. } => render!(tmpl, error), + Self::DateFromDatetimeParsing { error, .. } => render!(tmpl, error), + Self::TimeParsing { error, .. } => render!(tmpl, error), + Self::DatetimeParsing { error, .. } => render!(tmpl, error), + Self::DatetimeObjectInvalid { error, .. } => render!(tmpl, error), + Self::TimezoneOffset { + tz_expected, tz_actual, .. + } => to_string_render!(tmpl, tz_expected, tz_actual), + Self::TimeDeltaParsing { error, .. } => render!(tmpl, error), + Self::IsInstanceOf { class, .. } => render!(tmpl, class), + Self::IsSubclassOf { class, .. } => render!(tmpl, class), Self::UnionTagInvalid { discriminator, tag, expected_tags, + .. } => render!(tmpl, discriminator, tag, expected_tags), - Self::UnionTagNotFound { discriminator } => render!(tmpl, discriminator), - Self::UrlParsing { error } => render!(tmpl, error), - Self::UrlSyntaxViolation { error } => render!(tmpl, error), - Self::UrlTooLong { max_length } => to_string_render!(tmpl, max_length), - Self::UrlScheme { expected_schemes } => render!(tmpl, expected_schemes), - Self::UuidParsing { error } => render!(tmpl, error), - Self::UuidVersion { expected_version } => to_string_render!(tmpl, expected_version), + Self::UnionTagNotFound { discriminator, .. } => render!(tmpl, discriminator), + Self::UrlParsing { error, .. } => render!(tmpl, error), + Self::UrlSyntaxViolation { error, .. } => render!(tmpl, error), + Self::UrlTooLong { max_length, .. } => to_string_render!(tmpl, max_length), + Self::UrlScheme { expected_schemes, .. } => render!(tmpl, expected_schemes), + Self::UuidParsing { error, .. } => render!(tmpl, error), + Self::UuidVersion { expected_version, .. } => to_string_render!(tmpl, expected_version), _ => Ok(tmpl.to_string()), } } pub fn py_dict(&self, py: Python) -> PyResult>> { + let dict = PyDict::new(py); + self.py_dict_merge_ctx(py, dict)?; match self { - Self::NoSuchAttribute { attribute } => py_dict!(py, attribute), - Self::JsonInvalid { error } => py_dict!(py, error), - Self::GetAttributeError { error } => py_dict!(py, error), - Self::ModelType { class_name } => py_dict!(py, class_name), - Self::DataclassType { class_name } => py_dict!(py, class_name), - Self::DataclassExactType { class_name } => py_dict!(py, class_name), - Self::GreaterThan { gt } => py_dict!(py, gt), - Self::GreaterThanEqual { ge } => py_dict!(py, ge), - Self::LessThan { lt } => py_dict!(py, lt), - Self::LessThanEqual { le } => py_dict!(py, le), - Self::MultipleOf { multiple_of } => py_dict!(py, multiple_of), - Self::TooShort { - field_type, - min_length, - actual_length, - } => py_dict!(py, field_type, min_length, actual_length), - Self::TooLong { - field_type, - max_length, - actual_length, - } => py_dict!(py, field_type, max_length, actual_length), - Self::IterationError { error } => py_dict!(py, error), - 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), - Self::ValueError { error } => py_dict!(py, error), - Self::AssertionError { error } => py_dict!(py, error), - Self::CustomError { - custom_error: value_error, - } => Ok(value_error.context(py)), - Self::LiteralError { expected } => py_dict!(py, expected), - Self::DateParsing { error } => py_dict!(py, error), - Self::DateFromDatetimeParsing { error } => py_dict!(py, error), - Self::TimeParsing { error } => py_dict!(py, error), - Self::DatetimeParsing { error } => py_dict!(py, error), - Self::DatetimeObjectInvalid { error } => py_dict!(py, error), - Self::TimezoneOffset { tz_expected, tz_actual } => py_dict!(py, tz_expected, tz_actual), - Self::TimeDeltaParsing { error } => py_dict!(py, error), - Self::IsInstanceOf { class } => py_dict!(py, class), - Self::IsSubclassOf { class } => py_dict!(py, class), - Self::UnionTagInvalid { - discriminator, - tag, - expected_tags, - } => py_dict!(py, discriminator, tag, expected_tags), - Self::UnionTagNotFound { discriminator } => py_dict!(py, discriminator), - Self::UrlParsing { error } => py_dict!(py, error), - Self::UrlSyntaxViolation { error } => py_dict!(py, error), - Self::UrlTooLong { max_length } => py_dict!(py, max_length), - Self::UrlScheme { expected_schemes } => py_dict!(py, expected_schemes), - - Self::UuidParsing { error } => py_dict!(py, error), - Self::UuidVersion { expected_version } => py_dict!(py, expected_version), - _ => Ok(None), + Self::CustomError { custom_error, .. } => { + dict.del_item("custom_error")?; // Custom error data is merged to the root of ctx + if let Some(custom_ctx) = custom_error.context(py) { + dict.update(custom_ctx.as_ref(py).downcast()?)? + } + } + _ => {} + }; + if dict.is_empty() { + return Ok(None); } + return Ok(Some(dict.into())); } } diff --git a/src/errors/validation_exception.rs b/src/errors/validation_exception.rs index 7ad2ce5e5..67db6651a 100644 --- a/src/errors/validation_exception.rs +++ b/src/errors/validation_exception.rs @@ -390,7 +390,7 @@ impl PyLineError { } if let Some(url_prefix) = url_prefix { match self.error_type { - ErrorType::CustomError { custom_error: _ } => { + ErrorType::CustomError { custom_error: _, .. } => { // Don't add URLs for custom errors } _ => { @@ -428,7 +428,7 @@ impl PyLineError { } if let Some(url_prefix) = url_prefix { match self.error_type { - ErrorType::CustomError { custom_error: _ } => { + ErrorType::CustomError { custom_error: _, .. } => { // Don't display URLs for custom errors output.push(']'); } diff --git a/src/errors/value_exception.rs b/src/errors/value_exception.rs index 9544fe896..69c2f6a11 100644 --- a/src/errors/value_exception.rs +++ b/src/errors/value_exception.rs @@ -73,6 +73,10 @@ impl PydanticCustomError { } } + pub fn to_object(&self, py: Python<'_>) -> PyObject { + self.context(py).into_py(py) + } + #[getter(type)] pub fn error_type(&self) -> String { self.error_type.clone() @@ -121,7 +125,10 @@ impl PydanticCustomError { impl PydanticCustomError { pub fn into_val_error<'a>(self, input: &'a impl Input<'a>) -> ValError<'a> { - let error_type = ErrorType::CustomError { custom_error: self }; + let error_type = ErrorType::CustomError { + custom_error: self, + context: None, + }; ValError::new(error_type, input) } } diff --git a/src/input/datetime.rs b/src/input/datetime.rs index b430e8712..693f9c39b 100644 --- a/src/input/datetime.rs +++ b/src/input/datetime.rs @@ -260,6 +260,7 @@ pub fn bytes_as_date<'a>(input: &'a impl Input<'a>, bytes: &[u8]) -> ValResult<' Err(err) => Err(ValError::new( ErrorType::DateParsing { error: Cow::Borrowed(err.get_documentation().unwrap_or_default()), + context: None, }, input, )), @@ -282,6 +283,7 @@ pub fn bytes_as_time<'a>( Err(err) => Err(ValError::new( ErrorType::TimeParsing { error: Cow::Borrowed(err.get_documentation().unwrap_or_default()), + context: None, }, input, )), @@ -304,6 +306,7 @@ pub fn bytes_as_datetime<'a, 'b>( Err(err) => Err(ValError::new( ErrorType::DatetimeParsing { error: Cow::Borrowed(err.get_documentation().unwrap_or_default()), + context: None, }, input, )), @@ -327,6 +330,7 @@ pub fn int_as_datetime<'a>( Err(err) => Err(ValError::new( ErrorType::DatetimeParsing { error: Cow::Borrowed(err.get_documentation().unwrap_or_default()), + context: None, }, input, )), @@ -339,6 +343,7 @@ macro_rules! nan_check { return Err(ValError::new( ErrorType::$error_type { error: Cow::Borrowed("NaN values not permitted"), + context: None, }, $input, )); @@ -382,6 +387,7 @@ pub fn int_as_time<'a>( return Err(ValError::new( ErrorType::TimeParsing { error: Cow::Borrowed("time in seconds should be positive"), + context: None, }, input, )); @@ -403,6 +409,7 @@ pub fn int_as_time<'a>( Err(err) => Err(ValError::new( ErrorType::TimeParsing { error: Cow::Borrowed(err.get_documentation().unwrap_or_default()), + context: None, }, input, )), @@ -420,6 +427,7 @@ fn map_timedelta_err<'a>(input: &'a impl Input<'a>, err: ParseError) -> ValError ValError::new( ErrorType::TimeDeltaParsing { error: Cow::Borrowed(err.get_documentation().unwrap_or_default()), + context: None, }, input, ) diff --git a/src/input/input_json.rs b/src/input/input_json.rs index 218baf6b6..a39830d48 100644 --- a/src/input/input_json.rs +++ b/src/input/input_json.rs @@ -5,7 +5,7 @@ use pyo3::types::PyDict; use speedate::MicrosecondsPrecisionOverflowBehavior; use strum::EnumMessage; -use crate::errors::{ErrorType, InputValue, LocItem, ValError, ValResult}; +use crate::errors::{ErrorType, ErrorTypeDefaults, InputValue, LocItem, ValError, ValResult}; use super::datetime::{ bytes_as_date, bytes_as_datetime, bytes_as_time, bytes_as_timedelta, float_as_datetime, float_as_duration, @@ -54,7 +54,7 @@ impl<'a> Input<'a> for JsonInput { match self { JsonInput::Object(object) => Ok(JsonArgs::new(None, Some(object)).into()), JsonInput::Array(array) => Ok(JsonArgs::new(Some(array), None).into()), - _ => Err(ValError::new(ErrorType::ArgumentsType, self)), + _ => Err(ValError::new(ErrorTypeDefaults::ArgumentsType, self)), } } @@ -63,7 +63,13 @@ impl<'a> Input<'a> for JsonInput { JsonInput::Object(object) => Ok(JsonArgs::new(None, Some(object)).into()), _ => { let class_name = class_name.to_string(); - Err(ValError::new(ErrorType::DataclassType { class_name }, self)) + Err(ValError::new( + ErrorType::DataclassType { + class_name, + context: None, + }, + self, + )) } } } @@ -71,27 +77,27 @@ impl<'a> Input<'a> for JsonInput { fn parse_json(&'a self) -> ValResult<'a, JsonInput> { match self { JsonInput::String(s) => serde_json::from_str(s.as_str()).map_err(|e| map_json_err(self, e)), - _ => Err(ValError::new(ErrorType::JsonType, self)), + _ => Err(ValError::new(ErrorTypeDefaults::JsonType, self)), } } fn strict_str(&'a self) -> ValResult> { match self { JsonInput::String(s) => Ok(s.as_str().into()), - _ => Err(ValError::new(ErrorType::StringType, self)), + _ => Err(ValError::new(ErrorTypeDefaults::StringType, self)), } } fn lax_str(&'a self) -> ValResult> { match self { JsonInput::String(s) => Ok(s.as_str().into()), - _ => Err(ValError::new(ErrorType::StringType, self)), + _ => Err(ValError::new(ErrorTypeDefaults::StringType, self)), } } fn validate_bytes(&'a self, _strict: bool) -> ValResult> { match self { JsonInput::String(s) => Ok(s.as_bytes().into()), - _ => Err(ValError::new(ErrorType::BytesType, self)), + _ => Err(ValError::new(ErrorTypeDefaults::BytesType, self)), } } #[cfg_attr(has_no_coverage, no_coverage)] @@ -102,7 +108,7 @@ impl<'a> Input<'a> for JsonInput { fn strict_bool(&self) -> ValResult { match self { JsonInput::Bool(b) => Ok(*b), - _ => Err(ValError::new(ErrorType::BoolType, self)), + _ => Err(ValError::new(ErrorTypeDefaults::BoolType, self)), } } fn lax_bool(&self) -> ValResult { @@ -111,10 +117,12 @@ impl<'a> Input<'a> for JsonInput { JsonInput::String(s) => str_as_bool(self, s), JsonInput::Int(int) => int_as_bool(self, *int), JsonInput::Float(float) => match float_as_int(self, *float) { - Ok(int) => int.as_bool().ok_or_else(|| ValError::new(ErrorType::BoolParsing, self)), - _ => Err(ValError::new(ErrorType::BoolType, self)), + Ok(int) => int + .as_bool() + .ok_or_else(|| ValError::new(ErrorTypeDefaults::BoolParsing, self)), + _ => Err(ValError::new(ErrorTypeDefaults::BoolType, self)), }, - _ => Err(ValError::new(ErrorType::BoolType, self)), + _ => Err(ValError::new(ErrorTypeDefaults::BoolType, self)), } } @@ -123,7 +131,7 @@ impl<'a> Input<'a> for JsonInput { JsonInput::Int(i) => Ok(EitherInt::I64(*i)), JsonInput::Uint(u) => Ok(EitherInt::U64(*u)), JsonInput::BigInt(b) => Ok(EitherInt::BigInt(b.clone())), - _ => Err(ValError::new(ErrorType::IntType, self)), + _ => Err(ValError::new(ErrorTypeDefaults::IntType, self)), } } fn lax_int(&'a self) -> ValResult> { @@ -137,14 +145,14 @@ impl<'a> Input<'a> for JsonInput { JsonInput::BigInt(b) => Ok(EitherInt::BigInt(b.clone())), JsonInput::Float(f) => float_as_int(self, *f), JsonInput::String(str) => str_as_int(self, str), - _ => Err(ValError::new(ErrorType::IntType, self)), + _ => Err(ValError::new(ErrorTypeDefaults::IntType, self)), } } fn ultra_strict_float(&'a self) -> ValResult> { match self { JsonInput::Float(f) => Ok(EitherFloat::F64(*f)), - _ => Err(ValError::new(ErrorType::FloatType, self)), + _ => Err(ValError::new(ErrorTypeDefaults::FloatType, self)), } } fn strict_float(&'a self) -> ValResult> { @@ -152,7 +160,7 @@ impl<'a> Input<'a> for JsonInput { JsonInput::Float(f) => Ok(EitherFloat::F64(*f)), JsonInput::Int(i) => Ok(EitherFloat::F64(*i as f64)), JsonInput::Uint(u) => Ok(EitherFloat::F64(*u as f64)), - _ => Err(ValError::new(ErrorType::FloatType, self)), + _ => Err(ValError::new(ErrorTypeDefaults::FloatType, self)), } } fn lax_float(&'a self) -> ValResult> { @@ -166,16 +174,16 @@ impl<'a> Input<'a> for JsonInput { JsonInput::Uint(u) => Ok(EitherFloat::F64(*u as f64)), JsonInput::String(str) => match str.parse::() { Ok(i) => Ok(EitherFloat::F64(i)), - Err(_) => Err(ValError::new(ErrorType::FloatParsing, self)), + Err(_) => Err(ValError::new(ErrorTypeDefaults::FloatParsing, self)), }, - _ => Err(ValError::new(ErrorType::FloatType, self)), + _ => Err(ValError::new(ErrorTypeDefaults::FloatType, self)), } } fn validate_dict(&'a self, _strict: bool) -> ValResult> { match self { JsonInput::Object(dict) => Ok(dict.into()), - _ => Err(ValError::new(ErrorType::DictType, self)), + _ => Err(ValError::new(ErrorTypeDefaults::DictType, self)), } } #[cfg_attr(has_no_coverage, no_coverage)] @@ -186,7 +194,7 @@ impl<'a> Input<'a> for JsonInput { fn validate_list(&'a self, _strict: bool) -> ValResult> { match self { JsonInput::Array(a) => Ok(GenericIterable::JsonArray(a)), - _ => Err(ValError::new(ErrorType::ListType, self)), + _ => Err(ValError::new(ErrorTypeDefaults::ListType, self)), } } #[cfg_attr(has_no_coverage, no_coverage)] @@ -198,7 +206,7 @@ impl<'a> Input<'a> for JsonInput { // just as in set's case, List has to be allowed match self { JsonInput::Array(a) => Ok(GenericIterable::JsonArray(a)), - _ => Err(ValError::new(ErrorType::TupleType, self)), + _ => Err(ValError::new(ErrorTypeDefaults::TupleType, self)), } } #[cfg_attr(has_no_coverage, no_coverage)] @@ -210,7 +218,7 @@ impl<'a> Input<'a> for JsonInput { // we allow a list here since otherwise it would be impossible to create a set from JSON match self { JsonInput::Array(a) => Ok(GenericIterable::JsonArray(a)), - _ => Err(ValError::new(ErrorType::SetType, self)), + _ => Err(ValError::new(ErrorTypeDefaults::SetType, self)), } } #[cfg_attr(has_no_coverage, no_coverage)] @@ -222,7 +230,7 @@ impl<'a> Input<'a> for JsonInput { // we allow a list here since otherwise it would be impossible to create a frozenset from JSON match self { JsonInput::Array(a) => Ok(GenericIterable::JsonArray(a)), - _ => Err(ValError::new(ErrorType::FrozenSetType, self)), + _ => Err(ValError::new(ErrorTypeDefaults::FrozenSetType, self)), } } #[cfg_attr(has_no_coverage, no_coverage)] @@ -235,7 +243,7 @@ impl<'a> Input<'a> for JsonInput { JsonInput::Array(a) => Ok(GenericIterable::JsonArray(a)), JsonInput::String(s) => Ok(GenericIterable::JsonString(s)), JsonInput::Object(object) => Ok(GenericIterable::JsonObject(object)), - _ => Err(ValError::new(ErrorType::IterableType, self)), + _ => Err(ValError::new(ErrorTypeDefaults::IterableType, self)), } } @@ -248,14 +256,14 @@ impl<'a> Input<'a> for JsonInput { let keys: Vec = object.keys().map(|k| JsonInput::String(k.clone())).collect(); Ok(keys.into()) } - _ => Err(ValError::new(ErrorType::IterableType, self)), + _ => Err(ValError::new(ErrorTypeDefaults::IterableType, self)), } } fn validate_date(&self, _strict: bool) -> ValResult { match self { JsonInput::String(v) => bytes_as_date(self, v.as_bytes()), - _ => Err(ValError::new(ErrorType::DateType, self)), + _ => Err(ValError::new(ErrorTypeDefaults::DateType, self)), } } // NO custom `lax_date` implementation, if strict_date fails, the validator will fallback to lax_datetime @@ -271,7 +279,7 @@ impl<'a> Input<'a> for JsonInput { ) -> ValResult { match self { JsonInput::String(v) => bytes_as_time(self, v.as_bytes(), microseconds_overflow_behavior), - _ => Err(ValError::new(ErrorType::TimeType, self)), + _ => Err(ValError::new(ErrorTypeDefaults::TimeType, self)), } } fn lax_time(&self, microseconds_overflow_behavior: MicrosecondsPrecisionOverflowBehavior) -> ValResult { @@ -286,10 +294,11 @@ impl<'a> Input<'a> for JsonInput { .get_documentation() .unwrap_or_default(), ), + context: None, }, self, )), - _ => Err(ValError::new(ErrorType::TimeType, self)), + _ => Err(ValError::new(ErrorTypeDefaults::TimeType, self)), } } @@ -299,7 +308,7 @@ impl<'a> Input<'a> for JsonInput { ) -> ValResult { match self { JsonInput::String(v) => bytes_as_datetime(self, v.as_bytes(), microseconds_overflow_behavior), - _ => Err(ValError::new(ErrorType::DatetimeType, self)), + _ => Err(ValError::new(ErrorTypeDefaults::DatetimeType, self)), } } fn lax_datetime( @@ -310,7 +319,7 @@ impl<'a> Input<'a> for JsonInput { JsonInput::String(v) => bytes_as_datetime(self, v.as_bytes(), microseconds_overflow_behavior), JsonInput::Int(v) => int_as_datetime(self, *v, 0), JsonInput::Float(v) => float_as_datetime(self, *v), - _ => Err(ValError::new(ErrorType::DatetimeType, self)), + _ => Err(ValError::new(ErrorTypeDefaults::DatetimeType, self)), } } @@ -320,7 +329,7 @@ impl<'a> Input<'a> for JsonInput { ) -> ValResult { match self { JsonInput::String(v) => bytes_as_timedelta(self, v.as_bytes(), microseconds_overflow_behavior), - _ => Err(ValError::new(ErrorType::TimeDeltaType, self)), + _ => Err(ValError::new(ErrorTypeDefaults::TimeDeltaType, self)), } } fn lax_timedelta( @@ -331,7 +340,7 @@ impl<'a> Input<'a> for JsonInput { JsonInput::String(v) => bytes_as_timedelta(self, v.as_bytes(), microseconds_overflow_behavior), JsonInput::Int(v) => Ok(int_as_duration(self, *v)?.into()), JsonInput::Float(v) => Ok(float_as_duration(self, *v)?.into()), - _ => Err(ValError::new(ErrorType::TimeDeltaType, self)), + _ => Err(ValError::new(ErrorTypeDefaults::TimeDeltaType, self)), } } } @@ -357,13 +366,19 @@ impl<'a> Input<'a> for String { #[cfg_attr(has_no_coverage, no_coverage)] fn validate_args(&'a self) -> ValResult<'a, GenericArguments<'a>> { - Err(ValError::new(ErrorType::ArgumentsType, self)) + Err(ValError::new(ErrorTypeDefaults::ArgumentsType, self)) } #[cfg_attr(has_no_coverage, no_coverage)] fn validate_dataclass_args(&'a self, class_name: &str) -> ValResult<'a, GenericArguments<'a>> { let class_name = class_name.to_string(); - Err(ValError::new(ErrorType::DataclassType { class_name }, self)) + Err(ValError::new( + ErrorType::DataclassType { + class_name, + context: None, + }, + self, + )) } fn parse_json(&'a self) -> ValResult<'a, JsonInput> { @@ -386,19 +401,19 @@ impl<'a> Input<'a> for String { } fn strict_bool(&self) -> ValResult { - Err(ValError::new(ErrorType::BoolType, self)) + Err(ValError::new(ErrorTypeDefaults::BoolType, self)) } fn lax_bool(&self) -> ValResult { str_as_bool(self, self) } fn strict_int(&'a self) -> ValResult> { - Err(ValError::new(ErrorType::IntType, self)) + Err(ValError::new(ErrorTypeDefaults::IntType, self)) } fn lax_int(&'a self) -> ValResult> { match self.parse() { Ok(i) => Ok(EitherInt::I64(i)), - Err(_) => Err(ValError::new(ErrorType::IntParsing, self)), + Err(_) => Err(ValError::new(ErrorTypeDefaults::IntParsing, self)), } } @@ -408,18 +423,18 @@ impl<'a> Input<'a> for String { } #[cfg_attr(has_no_coverage, no_coverage)] fn strict_float(&'a self) -> ValResult> { - Err(ValError::new(ErrorType::FloatType, self)) + Err(ValError::new(ErrorTypeDefaults::FloatType, self)) } fn lax_float(&'a self) -> ValResult> { match self.parse() { Ok(f) => Ok(EitherFloat::F64(f)), - Err(_) => Err(ValError::new(ErrorType::FloatParsing, self)), + Err(_) => Err(ValError::new(ErrorTypeDefaults::FloatParsing, self)), } } #[cfg_attr(has_no_coverage, no_coverage)] fn validate_dict(&'a self, _strict: bool) -> ValResult> { - Err(ValError::new(ErrorType::DictType, self)) + Err(ValError::new(ErrorTypeDefaults::DictType, self)) } #[cfg_attr(has_no_coverage, no_coverage)] fn strict_dict(&'a self) -> ValResult> { @@ -428,7 +443,7 @@ impl<'a> Input<'a> for String { #[cfg_attr(has_no_coverage, no_coverage)] fn validate_list(&'a self, _strict: bool) -> ValResult> { - Err(ValError::new(ErrorType::ListType, self)) + Err(ValError::new(ErrorTypeDefaults::ListType, self)) } #[cfg_attr(has_no_coverage, no_coverage)] fn strict_list(&'a self) -> ValResult> { @@ -437,7 +452,7 @@ impl<'a> Input<'a> for String { #[cfg_attr(has_no_coverage, no_coverage)] fn validate_tuple(&'a self, _strict: bool) -> ValResult> { - Err(ValError::new(ErrorType::TupleType, self)) + Err(ValError::new(ErrorTypeDefaults::TupleType, self)) } #[cfg_attr(has_no_coverage, no_coverage)] fn strict_tuple(&'a self) -> ValResult> { @@ -446,7 +461,7 @@ impl<'a> Input<'a> for String { #[cfg_attr(has_no_coverage, no_coverage)] fn validate_set(&'a self, _strict: bool) -> ValResult> { - Err(ValError::new(ErrorType::SetType, self)) + Err(ValError::new(ErrorTypeDefaults::SetType, self)) } #[cfg_attr(has_no_coverage, no_coverage)] fn strict_set(&'a self) -> ValResult> { @@ -455,7 +470,7 @@ impl<'a> Input<'a> for String { #[cfg_attr(has_no_coverage, no_coverage)] fn validate_frozenset(&'a self, _strict: bool) -> ValResult> { - Err(ValError::new(ErrorType::FrozenSetType, self)) + Err(ValError::new(ErrorTypeDefaults::FrozenSetType, self)) } #[cfg_attr(has_no_coverage, no_coverage)] fn strict_frozenset(&'a self) -> ValResult> { diff --git a/src/input/input_python.rs b/src/input/input_python.rs index 204792fcd..076777d95 100644 --- a/src/input/input_python.rs +++ b/src/input/input_python.rs @@ -11,7 +11,7 @@ use pyo3::types::{PyDictItems, PyDictKeys, PyDictValues}; use pyo3::{intern, AsPyPointer, PyTypeInfo}; use speedate::MicrosecondsPrecisionOverflowBehavior; -use crate::errors::{ErrorType, InputValue, LocItem, ValError, ValResult}; +use crate::errors::{ErrorType, ErrorTypeDefaults, InputValue, LocItem, ValError, ValResult}; use crate::tools::{extract_i64, safe_repr}; use crate::{ArgsKwargs, PyMultiHostUrl, PyUrl}; @@ -156,7 +156,7 @@ impl<'a> Input<'a> for PyAny { } else if let Ok(list) = self.downcast::() { Ok(PyArgs::new(Some(list.to_tuple()), None).into()) } else { - Err(ValError::new(ErrorType::ArgumentsType, self)) + Err(ValError::new(ErrorTypeDefaults::ArgumentsType, self)) } } @@ -169,7 +169,13 @@ impl<'a> Input<'a> for PyAny { Ok(PyArgs::new(Some(args), kwargs).into()) } else { let class_name = class_name.to_string(); - Err(ValError::new(ErrorType::DataclassType { class_name }, self)) + Err(ValError::new( + ErrorType::DataclassType { + class_name, + context: None, + }, + self, + )) } } @@ -182,7 +188,7 @@ impl<'a> Input<'a> for PyAny { } else if let Ok(py_byte_array) = self.downcast::() { serde_json::from_slice(unsafe { py_byte_array.as_bytes() }).map_err(|e| map_json_err(self, e)) } else { - Err(ValError::new(ErrorType::JsonType, self)) + Err(ValError::new(ErrorTypeDefaults::JsonType, self)) } } @@ -194,7 +200,7 @@ impl<'a> Input<'a> for PyAny { // rust string in StrConstrainedValidator - e.g. to_lower Ok(py_string_str(py_str)?.into()) } else { - Err(ValError::new(ErrorType::StringType, self)) + Err(ValError::new(ErrorTypeDefaults::StringType, self)) } } @@ -202,7 +208,7 @@ impl<'a> Input<'a> for PyAny { if let Ok(py_str) = ::try_from_exact(self) { Ok(EitherString::Py(py_str)) } else { - Err(ValError::new(ErrorType::IntType, self)) + Err(ValError::new(ErrorTypeDefaults::IntType, self)) } } @@ -210,7 +216,7 @@ impl<'a> Input<'a> for PyAny { if PyInt::is_exact_type_of(self) { Ok(EitherInt::Py(self)) } else { - Err(ValError::new(ErrorType::IntType, self)) + Err(ValError::new(ErrorTypeDefaults::IntType, self)) } } @@ -224,7 +230,7 @@ impl<'a> Input<'a> for PyAny { } else if let Ok(bytes) = self.downcast::() { let str = match from_utf8(bytes.as_bytes()) { Ok(s) => s, - Err(_) => return Err(ValError::new(ErrorType::StringUnicode, self)), + Err(_) => return Err(ValError::new(ErrorTypeDefaults::StringUnicode, self)), }; Ok(str.into()) } else if let Ok(py_byte_array) = self.downcast::() { @@ -232,11 +238,11 @@ impl<'a> Input<'a> for PyAny { // for why this is marked unsafe let str = match from_utf8(unsafe { py_byte_array.as_bytes() }) { Ok(s) => s, - Err(_) => return Err(ValError::new(ErrorType::StringUnicode, self)), + Err(_) => return Err(ValError::new(ErrorTypeDefaults::StringUnicode, self)), }; Ok(str.into()) } else { - Err(ValError::new(ErrorType::StringType, self)) + Err(ValError::new(ErrorTypeDefaults::StringType, self)) } } @@ -244,7 +250,7 @@ impl<'a> Input<'a> for PyAny { if let Ok(py_bytes) = self.downcast::() { Ok(py_bytes.into()) } else { - Err(ValError::new(ErrorType::BytesType, self)) + Err(ValError::new(ErrorTypeDefaults::BytesType, self)) } } @@ -257,7 +263,7 @@ impl<'a> Input<'a> for PyAny { } else if let Ok(py_byte_array) = self.downcast::() { Ok(py_byte_array.to_vec().into()) } else { - Err(ValError::new(ErrorType::BytesType, self)) + Err(ValError::new(ErrorTypeDefaults::BytesType, self)) } } @@ -265,24 +271,26 @@ impl<'a> Input<'a> for PyAny { if let Ok(bool) = self.downcast::() { Ok(bool.is_true()) } else { - Err(ValError::new(ErrorType::BoolType, self)) + Err(ValError::new(ErrorTypeDefaults::BoolType, self)) } } fn lax_bool(&self) -> ValResult { if let Ok(bool) = self.downcast::() { Ok(bool.is_true()) - } else if let Some(cow_str) = maybe_as_string(self, ErrorType::BoolParsing)? { + } else if let Some(cow_str) = maybe_as_string(self, ErrorTypeDefaults::BoolParsing)? { str_as_bool(self, &cow_str) } else if let Ok(int) = extract_i64(self) { int_as_bool(self, int) } else if let Ok(float) = self.extract::() { match float_as_int(self, float) { - Ok(int) => int.as_bool().ok_or_else(|| ValError::new(ErrorType::BoolParsing, self)), - _ => Err(ValError::new(ErrorType::BoolType, self)), + Ok(int) => int + .as_bool() + .ok_or_else(|| ValError::new(ErrorTypeDefaults::BoolParsing, self)), + _ => Err(ValError::new(ErrorTypeDefaults::BoolType, self)), } } else { - Err(ValError::new(ErrorType::BoolType, self)) + Err(ValError::new(ErrorTypeDefaults::BoolType, self)) } } @@ -292,34 +300,34 @@ impl<'a> Input<'a> for PyAny { } else if PyInt::is_type_of(self) { // bools are a subclass of int, so check for bool type in this specific case if PyBool::is_exact_type_of(self) { - Err(ValError::new(ErrorType::IntType, self)) + Err(ValError::new(ErrorTypeDefaults::IntType, self)) } else { Ok(EitherInt::Py(self)) } } else { - Err(ValError::new(ErrorType::IntType, self)) + Err(ValError::new(ErrorTypeDefaults::IntType, self)) } } fn lax_int(&'a self) -> ValResult> { if PyInt::is_exact_type_of(self) { Ok(EitherInt::Py(self)) - } else if let Some(cow_str) = maybe_as_string(self, ErrorType::IntParsing)? { + } else if let Some(cow_str) = maybe_as_string(self, ErrorTypeDefaults::IntParsing)? { str_as_int(self, &cow_str) } else if let Ok(float) = self.extract::() { float_as_int(self, float) } else { - Err(ValError::new(ErrorType::IntType, self)) + Err(ValError::new(ErrorTypeDefaults::IntType, self)) } } fn ultra_strict_float(&'a self) -> ValResult> { if self.is_instance_of::() { - Err(ValError::new(ErrorType::FloatType, self)) + Err(ValError::new(ErrorTypeDefaults::FloatType, self)) } else if let Ok(float) = self.downcast::() { Ok(EitherFloat::Py(float)) } else { - Err(ValError::new(ErrorType::FloatType, self)) + Err(ValError::new(ErrorTypeDefaults::FloatType, self)) } } fn strict_float(&'a self) -> ValResult> { @@ -329,12 +337,12 @@ impl<'a> Input<'a> for PyAny { } else if let Ok(float) = self.extract::() { // bools are cast to floats as either 0.0 or 1.0, so check for bool type in this specific case if (float == 0.0 || float == 1.0) && PyBool::is_exact_type_of(self) { - Err(ValError::new(ErrorType::FloatType, self)) + Err(ValError::new(ErrorTypeDefaults::FloatType, self)) } else { Ok(EitherFloat::F64(float)) } } else { - Err(ValError::new(ErrorType::FloatType, self)) + Err(ValError::new(ErrorTypeDefaults::FloatType, self)) } } @@ -342,15 +350,15 @@ impl<'a> Input<'a> for PyAny { if PyFloat::is_exact_type_of(self) { // Safety: self is PyFloat Ok(EitherFloat::Py(unsafe { self.downcast_unchecked::() })) - } else if let Some(cow_str) = maybe_as_string(self, ErrorType::FloatParsing)? { + } else if let Some(cow_str) = maybe_as_string(self, ErrorTypeDefaults::FloatParsing)? { match cow_str.as_ref().parse::() { Ok(i) => Ok(EitherFloat::F64(i)), - Err(_) => Err(ValError::new(ErrorType::FloatParsing, self)), + Err(_) => Err(ValError::new(ErrorTypeDefaults::FloatParsing, self)), } } else if let Ok(float) = self.extract::() { Ok(EitherFloat::F64(float)) } else { - Err(ValError::new(ErrorType::FloatType, self)) + Err(ValError::new(ErrorTypeDefaults::FloatType, self)) } } @@ -358,7 +366,7 @@ impl<'a> Input<'a> for PyAny { if let Ok(dict) = self.downcast::() { Ok(dict.into()) } else { - Err(ValError::new(ErrorType::DictType, self)) + Err(ValError::new(ErrorTypeDefaults::DictType, self)) } } @@ -368,7 +376,7 @@ impl<'a> Input<'a> for PyAny { } else if let Ok(mapping) = self.downcast::() { Ok(mapping.into()) } else { - Err(ValError::new(ErrorType::DictType, self)) + Err(ValError::new(ErrorTypeDefaults::DictType, self)) } } @@ -389,11 +397,11 @@ impl<'a> Input<'a> for PyAny { if from_attributes_applicable(obj) { Ok(GenericMapping::PyGetAttr(obj, Some(kwargs))) } else { - Err(ValError::new(ErrorType::ModelAttributesType, self)) + Err(ValError::new(ErrorTypeDefaults::ModelAttributesType, self)) } } else { // note the error here gives a hint about from_attributes - Err(ValError::new(ErrorType::ModelAttributesType, self)) + Err(ValError::new(ErrorTypeDefaults::ModelAttributesType, self)) } } else { // otherwise we just call back to validate_dict if from_mapping is allowed, note that errors in this @@ -405,19 +413,19 @@ impl<'a> Input<'a> for PyAny { fn strict_list(&'a self) -> ValResult> { match self.lax_list()? { GenericIterable::List(iter) => Ok(GenericIterable::List(iter)), - _ => Err(ValError::new(ErrorType::ListType, self)), + _ => Err(ValError::new(ErrorTypeDefaults::ListType, self)), } } fn lax_list(&'a self) -> ValResult> { match self .extract_generic_iterable() - .map_err(|_| ValError::new(ErrorType::ListType, self))? + .map_err(|_| ValError::new(ErrorTypeDefaults::ListType, self))? { GenericIterable::PyString(_) | GenericIterable::Bytes(_) | GenericIterable::Dict(_) - | GenericIterable::Mapping(_) => Err(ValError::new(ErrorType::ListType, self)), + | GenericIterable::Mapping(_) => Err(ValError::new(ErrorTypeDefaults::ListType, self)), other => Ok(other), } } @@ -425,19 +433,19 @@ impl<'a> Input<'a> for PyAny { fn strict_tuple(&'a self) -> ValResult> { match self.lax_tuple()? { GenericIterable::Tuple(iter) => Ok(GenericIterable::Tuple(iter)), - _ => Err(ValError::new(ErrorType::TupleType, self)), + _ => Err(ValError::new(ErrorTypeDefaults::TupleType, self)), } } fn lax_tuple(&'a self) -> ValResult> { match self .extract_generic_iterable() - .map_err(|_| ValError::new(ErrorType::TupleType, self))? + .map_err(|_| ValError::new(ErrorTypeDefaults::TupleType, self))? { GenericIterable::PyString(_) | GenericIterable::Bytes(_) | GenericIterable::Dict(_) - | GenericIterable::Mapping(_) => Err(ValError::new(ErrorType::TupleType, self)), + | GenericIterable::Mapping(_) => Err(ValError::new(ErrorTypeDefaults::TupleType, self)), other => Ok(other), } } @@ -445,19 +453,19 @@ impl<'a> Input<'a> for PyAny { fn strict_set(&'a self) -> ValResult> { match self.lax_set()? { GenericIterable::Set(iter) => Ok(GenericIterable::Set(iter)), - _ => Err(ValError::new(ErrorType::SetType, self)), + _ => Err(ValError::new(ErrorTypeDefaults::SetType, self)), } } fn lax_set(&'a self) -> ValResult> { match self .extract_generic_iterable() - .map_err(|_| ValError::new(ErrorType::SetType, self))? + .map_err(|_| ValError::new(ErrorTypeDefaults::SetType, self))? { GenericIterable::PyString(_) | GenericIterable::Bytes(_) | GenericIterable::Dict(_) - | GenericIterable::Mapping(_) => Err(ValError::new(ErrorType::SetType, self)), + | GenericIterable::Mapping(_) => Err(ValError::new(ErrorTypeDefaults::SetType, self)), other => Ok(other), } } @@ -465,19 +473,19 @@ impl<'a> Input<'a> for PyAny { fn strict_frozenset(&'a self) -> ValResult> { match self.lax_frozenset()? { GenericIterable::FrozenSet(iter) => Ok(GenericIterable::FrozenSet(iter)), - _ => Err(ValError::new(ErrorType::FrozenSetType, self)), + _ => Err(ValError::new(ErrorTypeDefaults::FrozenSetType, self)), } } fn lax_frozenset(&'a self) -> ValResult> { match self .extract_generic_iterable() - .map_err(|_| ValError::new(ErrorType::FrozenSetType, self))? + .map_err(|_| ValError::new(ErrorTypeDefaults::FrozenSetType, self))? { GenericIterable::PyString(_) | GenericIterable::Bytes(_) | GenericIterable::Dict(_) - | GenericIterable::Mapping(_) => Err(ValError::new(ErrorType::FrozenSetType, self)), + | GenericIterable::Mapping(_) => Err(ValError::new(ErrorTypeDefaults::FrozenSetType, self)), other => Ok(other), } } @@ -513,7 +521,7 @@ impl<'a> Input<'a> for PyAny { } else if let Ok(iterable) = self.iter() { Ok(GenericIterable::Iterator(iterable)) } else { - Err(ValError::new(ErrorType::IterableType, self)) + Err(ValError::new(ErrorTypeDefaults::IterableType, self)) } } @@ -521,18 +529,18 @@ impl<'a> Input<'a> for PyAny { if self.iter().is_ok() { Ok(self.into()) } else { - Err(ValError::new(ErrorType::IterableType, self)) + Err(ValError::new(ErrorTypeDefaults::IterableType, self)) } } fn strict_date(&self) -> ValResult { if PyDateTime::is_type_of(self) { // have to check if it's a datetime first, otherwise the line below converts to a date - Err(ValError::new(ErrorType::DateType, self)) + Err(ValError::new(ErrorTypeDefaults::DateType, self)) } else if let Ok(date) = self.downcast::() { Ok(date.into()) } else { - Err(ValError::new(ErrorType::DateType, self)) + Err(ValError::new(ErrorTypeDefaults::DateType, self)) } } @@ -540,7 +548,7 @@ impl<'a> Input<'a> for PyAny { if PyDateTime::is_type_of(self) { // have to check if it's a datetime first, otherwise the line below converts to a date // even if we later try coercion from a datetime, we don't want to return a datetime now - Err(ValError::new(ErrorType::DateType, self)) + Err(ValError::new(ErrorTypeDefaults::DateType, self)) } else if let Ok(date) = self.downcast::() { Ok(date.into()) } else if let Ok(py_str) = self.downcast::() { @@ -549,7 +557,7 @@ impl<'a> Input<'a> for PyAny { } else if let Ok(py_bytes) = self.downcast::() { bytes_as_date(self, py_bytes.as_bytes()) } else { - Err(ValError::new(ErrorType::DateType, self)) + Err(ValError::new(ErrorTypeDefaults::DateType, self)) } } @@ -560,7 +568,7 @@ impl<'a> Input<'a> for PyAny { if let Ok(time) = self.downcast::() { Ok(time.into()) } else { - Err(ValError::new(ErrorType::TimeType, self)) + Err(ValError::new(ErrorTypeDefaults::TimeType, self)) } } @@ -573,13 +581,13 @@ impl<'a> Input<'a> for PyAny { } else if let Ok(py_bytes) = self.downcast::() { bytes_as_time(self, py_bytes.as_bytes(), microseconds_overflow_behavior) } else if PyBool::is_exact_type_of(self) { - Err(ValError::new(ErrorType::TimeType, self)) + Err(ValError::new(ErrorTypeDefaults::TimeType, self)) } else if let Ok(int) = extract_i64(self) { int_as_time(self, int, 0) } else if let Ok(float) = self.extract::() { float_as_time(self, float) } else { - Err(ValError::new(ErrorType::TimeType, self)) + Err(ValError::new(ErrorTypeDefaults::TimeType, self)) } } @@ -590,7 +598,7 @@ impl<'a> Input<'a> for PyAny { if let Ok(dt) = self.downcast::() { Ok(dt.into()) } else { - Err(ValError::new(ErrorType::DatetimeType, self)) + Err(ValError::new(ErrorTypeDefaults::DatetimeType, self)) } } @@ -606,7 +614,7 @@ impl<'a> Input<'a> for PyAny { } else if let Ok(py_bytes) = self.downcast::() { bytes_as_datetime(self, py_bytes.as_bytes(), microseconds_overflow_behavior) } else if PyBool::is_exact_type_of(self) { - Err(ValError::new(ErrorType::DatetimeType, self)) + Err(ValError::new(ErrorTypeDefaults::DatetimeType, self)) } else if let Ok(int) = extract_i64(self) { int_as_datetime(self, int, 0) } else if let Ok(float) = self.extract::() { @@ -614,7 +622,7 @@ impl<'a> Input<'a> for PyAny { } else if let Ok(date) = self.downcast::() { Ok(date_as_datetime(date)?) } else { - Err(ValError::new(ErrorType::DatetimeType, self)) + Err(ValError::new(ErrorTypeDefaults::DatetimeType, self)) } } @@ -625,7 +633,7 @@ impl<'a> Input<'a> for PyAny { if let Ok(dt) = self.downcast::() { Ok(dt.into()) } else { - Err(ValError::new(ErrorType::TimeDeltaType, self)) + Err(ValError::new(ErrorTypeDefaults::TimeDeltaType, self)) } } @@ -645,7 +653,7 @@ impl<'a> Input<'a> for PyAny { } else if let Ok(float) = self.extract::() { Ok(float_as_duration(self, float)?.into()) } else { - Err(ValError::new(ErrorType::TimeDeltaType, self)) + Err(ValError::new(ErrorTypeDefaults::TimeDeltaType, self)) } } } diff --git a/src/input/return_enums.rs b/src/input/return_enums.rs index 416f1af37..35341de82 100644 --- a/src/input/return_enums.rs +++ b/src/input/return_enums.rs @@ -21,7 +21,7 @@ use pyo3::types::PyFunction; use pyo3::PyTypeInfo; use serde::{ser::Error, Serialize, Serializer}; -use crate::errors::{py_err_string, ErrorType, InputValue, ValError, ValLineError, ValResult}; +use crate::errors::{py_err_string, ErrorType, ErrorTypeDefaults, InputValue, ValError, ValLineError, ValResult}; use crate::recursion_guard::RecursionGuard; use crate::tools::py_err; use crate::validators::{CombinedValidator, Extra, Validator}; @@ -127,6 +127,7 @@ impl<'a, INPUT: Input<'a>> MaxLengthCheck<'a, INPUT> { field_type: self.field_type.to_string(), max_length, actual_length: self.current_length, + context: None, }, self.input, )); @@ -141,6 +142,7 @@ macro_rules! any_next_error { ValError::new_with_loc( ErrorType::IterationError { error: py_err_string($py, $err), + context: None, }, $input, $index, @@ -242,6 +244,7 @@ fn validate_iter_to_set<'a, 's>( field_type: field_type.to_string(), max_length, actual_length, + context: None, }, input, )); @@ -467,6 +470,7 @@ fn mapping_err<'py>(err: PyErr, py: Python<'py>, input: &'py impl Input<'py>) -> ValError::new( ErrorType::MappingType { error: py_err_string(py, err).into(), + context: None, }, input, ) @@ -502,6 +506,7 @@ impl<'py> Iterator for MappingGenericIterator<'py> { return Some(Err(ValError::new( ErrorType::MappingType { error: MAPPING_TUPLE_ERROR.into(), + context: None, }, self.input, ))) @@ -511,6 +516,7 @@ impl<'py> Iterator for MappingGenericIterator<'py> { return Some(Err(ValError::new( ErrorType::MappingType { error: MAPPING_TUPLE_ERROR.into(), + context: None, }, self.input, ))); @@ -782,7 +788,7 @@ impl<'a> IntoPy for EitherString<'a> { pub fn py_string_str(py_str: &PyString) -> ValResult<&str> { py_str .to_str() - .map_err(|_| ValError::new_custom_input(ErrorType::StringUnicode, InputValue::PyAny(py_str as &PyAny))) + .map_err(|_| ValError::new_custom_input(ErrorTypeDefaults::StringUnicode, InputValue::PyAny(py_str as &PyAny))) } #[cfg_attr(debug_assertions, derive(Debug))] @@ -848,16 +854,21 @@ impl<'a> EitherInt<'a> { EitherInt::I64(i) => Ok(i), EitherInt::U64(u) => match i64::try_from(u) { Ok(u) => Ok(u), - Err(_) => Err(ValError::new(ErrorType::IntParsingSize, u.into_py(py).into_ref(py))), + Err(_) => Err(ValError::new( + ErrorTypeDefaults::IntParsingSize, + u.into_py(py).into_ref(py), + )), }, EitherInt::BigInt(u) => match i64::try_from(u) { Ok(u) => Ok(u), Err(e) => Err(ValError::new( - ErrorType::IntParsingSize, + ErrorTypeDefaults::IntParsingSize, e.into_original().into_py(py).into_ref(py), )), }, - EitherInt::Py(i) => i.extract().map_err(|_| ValError::new(ErrorType::IntParsingSize, i)), + EitherInt::Py(i) => i + .extract() + .map_err(|_| ValError::new(ErrorTypeDefaults::IntParsingSize, i)), } } @@ -869,7 +880,9 @@ impl<'a> EitherInt<'a> { Err(_) => Ok(Int::Big(BigInt::from(*u))), }, EitherInt::BigInt(b) => Ok(Int::Big(b.clone())), - EitherInt::Py(i) => i.extract().map_err(|_| ValError::new(ErrorType::IntParsingSize, *i)), + EitherInt::Py(i) => i + .extract() + .map_err(|_| ValError::new(ErrorTypeDefaults::IntParsingSize, *i)), } } diff --git a/src/input/shared.rs b/src/input/shared.rs index 965eae463..6d250828c 100644 --- a/src/input/shared.rs +++ b/src/input/shared.rs @@ -1,6 +1,6 @@ use num_bigint::BigInt; -use crate::errors::{ErrorType, ValError, ValResult}; +use crate::errors::{ErrorType, ErrorTypeDefaults, ValError, ValResult}; use crate::input::EitherInt; use super::Input; @@ -9,6 +9,7 @@ pub fn map_json_err<'a>(input: &'a impl Input<'a>, error: serde_json::Error) -> ValError::new( ErrorType::JsonInvalid { error: error.to_string(), + context: None, }, input, ) @@ -32,7 +33,7 @@ pub fn str_as_bool<'a>(input: &'a impl Input<'a>, str: &str) -> ValResult<'a, bo { Ok(true) } else { - Err(ValError::new(ErrorType::BoolParsing, input)) + Err(ValError::new(ErrorTypeDefaults::BoolParsing, input)) } } @@ -42,7 +43,7 @@ pub fn int_as_bool<'a>(input: &'a impl Input<'a>, int: i64) -> ValResult<'a, boo } else if int == 1 { Ok(true) } else { - Err(ValError::new(ErrorType::BoolParsing, input)) + Err(ValError::new(ErrorTypeDefaults::BoolParsing, input)) } } @@ -54,17 +55,17 @@ pub fn int_as_bool<'a>(input: &'a impl Input<'a>, int: i64) -> ValResult<'a, boo pub fn str_as_int<'s, 'l>(input: &'s impl Input<'s>, str: &'l str) -> ValResult<'s, EitherInt<'s>> { let len = str.len(); if len > 4300 { - Err(ValError::new(ErrorType::IntParsingSize, input)) + Err(ValError::new(ErrorTypeDefaults::IntParsingSize, input)) } else if let Some(int) = _parse_str(input, str, len) { Ok(int) } else if let Some(str_stripped) = strip_decimal_zeros(str) { if let Some(int) = _parse_str(input, str_stripped, len) { Ok(int) } else { - Err(ValError::new(ErrorType::IntParsing, input)) + Err(ValError::new(ErrorTypeDefaults::IntParsing, input)) } } else { - Err(ValError::new(ErrorType::IntParsing, input)) + Err(ValError::new(ErrorTypeDefaults::IntParsing, input)) } } @@ -94,12 +95,12 @@ fn strip_decimal_zeros(s: &str) -> Option<&str> { pub fn float_as_int<'a>(input: &'a impl Input<'a>, float: f64) -> ValResult<'a, EitherInt<'a>> { if float == f64::INFINITY || float == f64::NEG_INFINITY || float.is_nan() { - Err(ValError::new(ErrorType::FiniteNumber, input)) + Err(ValError::new(ErrorTypeDefaults::FiniteNumber, input)) } else if float % 1.0 != 0.0 { - Err(ValError::new(ErrorType::IntFromFloat, input)) + Err(ValError::new(ErrorTypeDefaults::IntFromFloat, input)) } else if (i64::MIN as f64) < float && float < (i64::MAX as f64) { Ok(EitherInt::I64(float as i64)) } else { - Err(ValError::new(ErrorType::IntParsingSize, input)) + Err(ValError::new(ErrorTypeDefaults::IntParsingSize, input)) } } diff --git a/src/validators/arguments.rs b/src/validators/arguments.rs index bc7290b1d..6496cfe8a 100644 --- a/src/validators/arguments.rs +++ b/src/validators/arguments.rs @@ -6,7 +6,7 @@ use ahash::AHashSet; use crate::build_tools::py_schema_err; use crate::build_tools::schema_or_config_same; -use crate::errors::{ErrorType, ValError, ValLineError, ValResult}; +use crate::errors::{ErrorTypeDefaults, ValError, ValLineError, ValResult}; use crate::input::{GenericArguments, Input}; use crate::lookup_key::LookupKey; @@ -199,7 +199,7 @@ impl Validator for ArgumentsValidator { match (pos_value, kw_value) { (Some(_), Some((_, kw_value))) => { errors.push(ValLineError::new_with_loc( - ErrorType::MultipleArgumentValues, + ErrorTypeDefaults::MultipleArgumentValues, kw_value, parameter.name.clone(), )); @@ -239,9 +239,9 @@ impl Validator for ArgumentsValidator { } } else if let Some(ref lookup_key) = parameter.kw_lookup_key { let error_type = if parameter.positional { - ErrorType::MissingArgument + ErrorTypeDefaults::MissingArgument } else { - ErrorType::MissingKeywordOnlyArgument + ErrorTypeDefaults::MissingKeywordOnlyArgument }; errors.push(lookup_key.error( error_type, @@ -250,7 +250,7 @@ impl Validator for ArgumentsValidator { ¶meter.name, )); } else { - errors.push(ValLineError::new_with_loc(ErrorType::MissingPositionalOnlyArgument, input, index)); + errors.push(ValLineError::new_with_loc(ErrorTypeDefaults::MissingPositionalOnlyArgument, input, index)); }; } } @@ -276,7 +276,7 @@ impl Validator for ArgumentsValidator { } else { for (index, item) in $slice_macro!(args, self.positional_params_count, len).iter().enumerate() { errors.push(ValLineError::new_with_loc( - ErrorType::UnexpectedPositionalArgument, + ErrorTypeDefaults::UnexpectedPositionalArgument, item, index + self.positional_params_count, )); @@ -294,7 +294,7 @@ impl Validator for ArgumentsValidator { for err in line_errors { errors.push( err.with_outer_location(raw_key.as_loc_item()) - .with_type(ErrorType::InvalidKey), + .with_type(ErrorTypeDefaults::InvalidKey), ); } continue; @@ -314,7 +314,7 @@ impl Validator for ArgumentsValidator { }, None => { errors.push(ValLineError::new_with_loc( - ErrorType::UnexpectedKeywordArgument, + ErrorTypeDefaults::UnexpectedKeywordArgument, value, raw_key.as_loc_item(), )); diff --git a/src/validators/bytes.rs b/src/validators/bytes.rs index c4bc89886..454d06020 100644 --- a/src/validators/bytes.rs +++ b/src/validators/bytes.rs @@ -93,12 +93,24 @@ impl Validator for BytesConstrainedValidator { if let Some(min_length) = self.min_length { if len < min_length { - return Err(ValError::new(ErrorType::BytesTooShort { min_length }, input)); + return Err(ValError::new( + ErrorType::BytesTooShort { + min_length, + context: None, + }, + input, + )); } } if let Some(max_length) = self.max_length { if len > max_length { - return Err(ValError::new(ErrorType::BytesTooLong { max_length }, input)); + return Err(ValError::new( + ErrorType::BytesTooLong { + max_length, + context: None, + }, + input, + )); } } diff --git a/src/validators/callable.rs b/src/validators/callable.rs index 9e1c1f9cc..dc57612f5 100644 --- a/src/validators/callable.rs +++ b/src/validators/callable.rs @@ -1,7 +1,7 @@ use pyo3::prelude::*; use pyo3::types::PyDict; -use crate::errors::{ErrorType, ValError, ValResult}; +use crate::errors::{ErrorTypeDefaults, ValError, ValResult}; use crate::input::Input; use crate::recursion_guard::RecursionGuard; @@ -36,7 +36,7 @@ impl Validator for CallableValidator { ) -> ValResult<'data, PyObject> { match input.callable() { true => Ok(input.to_object(py)), - false => Err(ValError::new(ErrorType::CallableType, input)), + false => Err(ValError::new(ErrorTypeDefaults::CallableType, input)), } } diff --git a/src/validators/dataclass.rs b/src/validators/dataclass.rs index e0209ad2a..823a61b8a 100644 --- a/src/validators/dataclass.rs +++ b/src/validators/dataclass.rs @@ -7,7 +7,7 @@ use ahash::AHashSet; use crate::build_tools::py_schema_err; use crate::build_tools::{is_strict, schema_or_config_same, ExtraBehavior}; -use crate::errors::{ErrorType, ValError, ValLineError, ValResult}; +use crate::errors::{ErrorType, ErrorTypeDefaults, ValError, ValLineError, ValResult}; use crate::input::{GenericArguments, Input}; use crate::lookup_key::LookupKey; use crate::recursion_guard::RecursionGuard; @@ -185,7 +185,7 @@ impl Validator for DataclassArgsValidator { // found both positional and keyword arguments, error (Some(_), Some((_, kw_value))) => { errors.push(ValLineError::new_with_loc( - ErrorType::MultipleArgumentValues, + ErrorTypeDefaults::MultipleArgumentValues, kw_value, field.name.clone(), )); @@ -236,7 +236,7 @@ impl Validator for DataclassArgsValidator { set_item!(field, value); } else { errors.push(field.lookup_key.error( - ErrorType::Missing, + ErrorTypeDefaults::Missing, input, self.loc_by_alias, &field.name, @@ -251,7 +251,7 @@ impl Validator for DataclassArgsValidator { if len > self.positional_count { for (index, item) in $slice_macro!(args, self.positional_count, len).iter().enumerate() { errors.push(ValLineError::new_with_loc( - ErrorType::UnexpectedPositionalArgument, + ErrorTypeDefaults::UnexpectedPositionalArgument, item, index + self.positional_count, )); @@ -269,7 +269,7 @@ impl Validator for DataclassArgsValidator { match self.extra_behavior { ExtraBehavior::Forbid => { errors.push(ValLineError::new_with_loc( - ErrorType::UnexpectedKeywordArgument, + ErrorTypeDefaults::UnexpectedKeywordArgument, value, raw_key.as_loc_item(), )); @@ -285,7 +285,7 @@ impl Validator for DataclassArgsValidator { for err in line_errors { errors.push( err.with_outer_location(raw_key.as_loc_item()) - .with_type(ErrorType::InvalidKey), + .with_type(ErrorTypeDefaults::InvalidKey), ); } } @@ -335,7 +335,7 @@ impl Validator for DataclassArgsValidator { if let Some(field) = self.fields.iter().find(|f| f.name == field_name) { if field.frozen { return Err(ValError::new_with_loc( - ErrorType::FrozenField, + ErrorTypeDefaults::FrozenField, field_value, field.name.to_string(), )); @@ -377,6 +377,7 @@ impl Validator for DataclassArgsValidator { _ => Err(ValError::new_with_loc( ErrorType::NoSuchAttribute { attribute: field_name.to_string(), + context: None, }, field_value, field_name.to_string(), @@ -505,6 +506,7 @@ impl Validator for DataclassValidator { Err(ValError::new( ErrorType::DataclassExactType { class_name: self.get_name().to_string(), + context: None, }, input, )) @@ -529,7 +531,7 @@ impl Validator for DataclassValidator { recursion_guard: &'s mut RecursionGuard, ) -> ValResult<'data, PyObject> { if self.frozen { - return Err(ValError::new(ErrorType::FrozenInstance, field_value)); + return Err(ValError::new(ErrorTypeDefaults::FrozenInstance, field_value)); } let new_dict = self.dataclass_to_dict(py, obj)?; diff --git a/src/validators/date.rs b/src/validators/date.rs index aba818ce4..53d8a8177 100644 --- a/src/validators/date.rs +++ b/src/validators/date.rs @@ -5,7 +5,7 @@ use speedate::{Date, Time}; use strum::EnumMessage; use crate::build_tools::{is_strict, py_schema_error_type}; -use crate::errors::{ErrorType, ValError, ValResult}; +use crate::errors::{ErrorType, ErrorTypeDefaults, ValError, ValResult}; use crate::input::{EitherDate, Input}; use crate::recursion_guard::RecursionGuard; @@ -68,6 +68,7 @@ impl Validator for DateValidator { return Err(ValError::new( ErrorType::$error { $constraint: constraint.to_string().into(), + context: None, }, input, )); @@ -91,8 +92,8 @@ impl Validator for DateValidator { let date_compliant = today_constraint.op.compare(c); if !date_compliant { let error_type = match today_constraint.op { - NowOp::Past => ErrorType::DatePast, - NowOp::Future => ErrorType::DateFuture, + NowOp::Past => ErrorTypeDefaults::DatePast, + NowOp::Future => ErrorTypeDefaults::DateFuture, }; return Err(ValError::new(error_type, input)); } @@ -134,9 +135,10 @@ fn date_from_datetime<'data>( // convert DateTimeParsing -> DateFromDatetimeParsing but keep the rest of the error unchanged for line_error in &mut line_errors { match line_error.error_type { - ErrorType::DatetimeParsing { ref error } => { + ErrorType::DatetimeParsing { ref error, .. } => { line_error.error_type = ErrorType::DateFromDatetimeParsing { error: error.to_string(), + context: None, }; } _ => { @@ -161,7 +163,7 @@ fn date_from_datetime<'data>( if dt.time == zero_time { Ok(EitherDate::Raw(dt.date)) } else { - Err(ValError::new(ErrorType::DateFromDatetimeInexact, input)) + Err(ValError::new(ErrorTypeDefaults::DateFromDatetimeInexact, input)) } } diff --git a/src/validators/datetime.rs b/src/validators/datetime.rs index 44f6a1a20..56b380cf5 100644 --- a/src/validators/datetime.rs +++ b/src/validators/datetime.rs @@ -8,7 +8,7 @@ use strum::EnumMessage; use crate::build_tools::{is_strict, py_schema_error_type}; use crate::build_tools::{py_schema_err, schema_or_config_same}; -use crate::errors::{py_err_string, ErrorType, ValError, ValResult}; +use crate::errors::{py_err_string, ErrorType, ErrorTypeDefaults, ValError, ValResult}; use crate::input::{EitherDateTime, Input}; use crate::recursion_guard::RecursionGuard; @@ -75,7 +75,10 @@ impl Validator for DateTimeValidator { Ok(dt) => dt, Err(err) => { let error = py_err_string(py, err); - return Err(ValError::new(ErrorType::DatetimeObjectInvalid { error }, input)); + return Err(ValError::new( + ErrorType::DatetimeObjectInvalid { error, context: None }, + input, + )); } }; macro_rules! check_constraint { @@ -85,6 +88,7 @@ impl Validator for DateTimeValidator { return Err(ValError::new( ErrorType::$error { $constraint: constraint.to_string().into(), + context: None, }, input, )); @@ -108,8 +112,8 @@ impl Validator for DateTimeValidator { let dt_compliant = now_constraint.op.compare(c); if !dt_compliant { let error_type = match now_constraint.op { - NowOp::Past => ErrorType::DatetimePast, - NowOp::Future => ErrorType::DatetimeFuture, + NowOp::Past => ErrorTypeDefaults::DatetimePast, + NowOp::Future => ErrorTypeDefaults::DatetimeFuture, }; return Err(ValError::new(error_type, input)); } @@ -273,17 +277,21 @@ impl TZConstraint { pub(super) fn tz_check<'d>(&self, tz_offset: Option, input: &'d impl Input<'d>) -> ValResult<'d, ()> { match (self, tz_offset) { - (TZConstraint::Aware(_), None) => return Err(ValError::new(ErrorType::TimezoneAware, input)), + (TZConstraint::Aware(_), None) => return Err(ValError::new(ErrorTypeDefaults::TimezoneAware, input)), (TZConstraint::Aware(Some(tz_expected)), Some(tz_actual)) => { let tz_expected = *tz_expected; if tz_expected != tz_actual { return Err(ValError::new( - ErrorType::TimezoneOffset { tz_expected, tz_actual }, + ErrorType::TimezoneOffset { + tz_expected, + tz_actual, + context: None, + }, input, )); } } - (TZConstraint::Naive, Some(_)) => return Err(ValError::new(ErrorType::TimezoneNaive, input)), + (TZConstraint::Naive, Some(_)) => return Err(ValError::new(ErrorTypeDefaults::TimezoneNaive, input)), _ => (), } Ok(()) diff --git a/src/validators/definitions.rs b/src/validators/definitions.rs index 298df91a9..2c4b70175 100644 --- a/src/validators/definitions.rs +++ b/src/validators/definitions.rs @@ -2,7 +2,7 @@ use pyo3::intern; use pyo3::prelude::*; use pyo3::types::{PyDict, PyList}; -use crate::errors::{ErrorType, ValError, ValResult}; +use crate::errors::{ErrorTypeDefaults, ValError, ValResult}; use crate::input::Input; use crate::recursion_guard::RecursionGuard; @@ -85,10 +85,10 @@ impl Validator for DefinitionRefValidator { if let Some(id) = input.identity() { if recursion_guard.contains_or_insert(id, self.validator_id) { // we don't remove id here, we leave that to the validator which originally added id to `recursion_guard` - Err(ValError::new(ErrorType::RecursionLoop, input)) + Err(ValError::new(ErrorTypeDefaults::RecursionLoop, input)) } else { if recursion_guard.incr_depth() { - return Err(ValError::new(ErrorType::RecursionLoop, input)); + return Err(ValError::new(ErrorTypeDefaults::RecursionLoop, input)); } let output = validate(self.validator_id, py, input, extra, definitions, recursion_guard); recursion_guard.remove(id, self.validator_id); @@ -113,10 +113,10 @@ impl Validator for DefinitionRefValidator { if let Some(id) = obj.identity() { if recursion_guard.contains_or_insert(id, self.validator_id) { // we don't remove id here, we leave that to the validator which originally added id to `recursion_guard` - Err(ValError::new(ErrorType::RecursionLoop, obj)) + Err(ValError::new(ErrorTypeDefaults::RecursionLoop, obj)) } else { if recursion_guard.incr_depth() { - return Err(ValError::new(ErrorType::RecursionLoop, obj)); + return Err(ValError::new(ErrorTypeDefaults::RecursionLoop, obj)); } let output = validate_assignment( self.validator_id, diff --git a/src/validators/float.rs b/src/validators/float.rs index bc5a3ea45..0a9ab51f5 100644 --- a/src/validators/float.rs +++ b/src/validators/float.rs @@ -3,7 +3,7 @@ use pyo3::prelude::*; use pyo3::types::PyDict; use crate::build_tools::{is_strict, schema_or_config_same}; -use crate::errors::{ErrorType, ValError, ValResult}; +use crate::errors::{ErrorType, ErrorTypeDefaults, ValError, ValResult}; use crate::input::Input; use crate::recursion_guard::RecursionGuard; use crate::tools::SchemaDict; @@ -73,7 +73,7 @@ impl Validator for FloatValidator { ) -> ValResult<'data, PyObject> { let either_float = input.validate_float(extra.strict.unwrap_or(self.strict), extra.ultra_strict)?; if !self.allow_inf_nan && !either_float.as_f64().is_finite() { - return Err(ValError::new(ErrorType::FiniteNumber, input)); + return Err(ValError::new(ErrorTypeDefaults::FiniteNumber, input)); } Ok(either_float.into_py(py)) } @@ -120,7 +120,7 @@ impl Validator for ConstrainedFloatValidator { let either_float = input.validate_float(extra.strict.unwrap_or(self.strict), extra.ultra_strict)?; let float: f64 = either_float.as_f64(); if !self.allow_inf_nan && !float.is_finite() { - return Err(ValError::new(ErrorType::FiniteNumber, input)); + return Err(ValError::new(ErrorTypeDefaults::FiniteNumber, input)); } if let Some(multiple_of) = self.multiple_of { let rem = float % multiple_of; @@ -129,6 +129,7 @@ impl Validator for ConstrainedFloatValidator { return Err(ValError::new( ErrorType::MultipleOf { multiple_of: multiple_of.into(), + context: None, }, input, )); @@ -136,22 +137,46 @@ impl Validator for ConstrainedFloatValidator { } if let Some(le) = self.le { if float > le { - return Err(ValError::new(ErrorType::LessThanEqual { le: le.into() }, input)); + return Err(ValError::new( + ErrorType::LessThanEqual { + le: le.into(), + context: None, + }, + input, + )); } } if let Some(lt) = self.lt { if float >= lt { - return Err(ValError::new(ErrorType::LessThan { lt: lt.into() }, input)); + return Err(ValError::new( + ErrorType::LessThan { + lt: lt.into(), + context: None, + }, + input, + )); } } if let Some(ge) = self.ge { if float < ge { - return Err(ValError::new(ErrorType::GreaterThanEqual { ge: ge.into() }, input)); + return Err(ValError::new( + ErrorType::GreaterThanEqual { + ge: ge.into(), + context: None, + }, + input, + )); } } if let Some(gt) = self.gt { if float <= gt { - return Err(ValError::new(ErrorType::GreaterThan { gt: gt.into() }, input)); + return Err(ValError::new( + ErrorType::GreaterThan { + gt: gt.into(), + context: None, + }, + input, + )); } } Ok(either_float.into_py(py)) diff --git a/src/validators/function.rs b/src/validators/function.rs index 9b62d61f9..b6fbc153e 100644 --- a/src/validators/function.rs +++ b/src/validators/function.rs @@ -499,6 +499,7 @@ macro_rules! py_err_string { Ok(_) => ValError::new( ErrorType::$type_member { error: Some($error_value.into()), + context: None, }, $input, ), diff --git a/src/validators/generator.rs b/src/validators/generator.rs index 6369af778..87947f1cb 100644 --- a/src/validators/generator.rs +++ b/src/validators/generator.rs @@ -141,6 +141,7 @@ impl ValidatorIterator { field_type: "Generator".to_string(), max_length, actual_length: index + 1, + context: None, }, $iter.input(py), ); @@ -166,6 +167,7 @@ impl ValidatorIterator { field_type: "Generator".to_string(), min_length, actual_length: $iter.index(), + context: None, }, $iter.input(py), ); diff --git a/src/validators/int.rs b/src/validators/int.rs index 8001df895..7c802a016 100644 --- a/src/validators/int.rs +++ b/src/validators/int.rs @@ -101,6 +101,7 @@ impl Validator for ConstrainedIntValidator { return Err(ValError::new( ErrorType::MultipleOf { multiple_of: multiple_of.clone().into(), + context: None, }, input, )); @@ -108,25 +109,46 @@ impl Validator for ConstrainedIntValidator { } if let Some(ref le) = self.le { if &int_value > le { - return Err(ValError::new(ErrorType::LessThanEqual { le: le.clone().into() }, input)); + return Err(ValError::new( + ErrorType::LessThanEqual { + le: le.clone().into(), + context: None, + }, + input, + )); } } if let Some(ref lt) = self.lt { if &int_value >= lt { - return Err(ValError::new(ErrorType::LessThan { lt: lt.clone().into() }, input)); + return Err(ValError::new( + ErrorType::LessThan { + lt: lt.clone().into(), + context: None, + }, + input, + )); } } if let Some(ref ge) = self.ge { if &int_value < ge { return Err(ValError::new( - ErrorType::GreaterThanEqual { ge: ge.clone().into() }, + ErrorType::GreaterThanEqual { + ge: ge.clone().into(), + context: None, + }, input, )); } } if let Some(ref gt) = self.gt { if &int_value <= gt { - return Err(ValError::new(ErrorType::GreaterThan { gt: gt.clone().into() }, input)); + return Err(ValError::new( + ErrorType::GreaterThan { + gt: gt.clone().into(), + context: None, + }, + input, + )); } } Ok(either_int.into_py(py)) diff --git a/src/validators/is_instance.rs b/src/validators/is_instance.rs index 212406f82..86487c9eb 100644 --- a/src/validators/is_instance.rs +++ b/src/validators/is_instance.rs @@ -78,6 +78,7 @@ impl Validator for IsInstanceValidator { false => Err(ValError::new( ErrorType::IsInstanceOf { class: self.class_repr.clone(), + context: None, }, input, )), diff --git a/src/validators/is_subclass.rs b/src/validators/is_subclass.rs index 732f624db..2e2c92d51 100644 --- a/src/validators/is_subclass.rs +++ b/src/validators/is_subclass.rs @@ -57,6 +57,7 @@ impl Validator for IsSubclassValidator { false => Err(ValError::new( ErrorType::IsSubclassOf { class: self.class_repr.clone(), + context: None, }, input, )), diff --git a/src/validators/list.rs b/src/validators/list.rs index 36baad5b8..ae3a0a0a7 100644 --- a/src/validators/list.rs +++ b/src/validators/list.rs @@ -45,6 +45,7 @@ macro_rules! length_check { field_type: $field_type.to_string(), min_length, actual_length, + context: None, }, $input, )); @@ -59,6 +60,7 @@ macro_rules! length_check { field_type: $field_type.to_string(), max_length, actual_length, + context: None, }, $input, )); @@ -78,6 +80,7 @@ macro_rules! min_length_check { field_type: $field_type.to_string(), min_length, actual_length, + context: None, }, $input, )); diff --git a/src/validators/literal.rs b/src/validators/literal.rs index 680fcadb2..4db95be47 100644 --- a/src/validators/literal.rs +++ b/src/validators/literal.rs @@ -194,6 +194,7 @@ impl Validator for LiteralValidator { None => Err(ValError::new( ErrorType::LiteralError { expected: self.expected_repr.clone(), + context: None, }, input, )), diff --git a/src/validators/model.rs b/src/validators/model.rs index 1f8733fca..e1f4327a6 100644 --- a/src/validators/model.rs +++ b/src/validators/model.rs @@ -10,7 +10,7 @@ use super::function::convert_err; use super::{build_validator, BuildValidator, CombinedValidator, Definitions, DefinitionsBuilder, Extra, Validator}; use crate::build_tools::py_schema_err; use crate::build_tools::schema_or_config_same; -use crate::errors::{ErrorType, ValError, ValResult}; +use crate::errors::{ErrorType, ErrorTypeDefaults, ValError, ValResult}; use crate::input::{py_error_on_minusone, Input}; use crate::recursion_guard::RecursionGuard; use crate::tools::{py_err, SchemaDict}; @@ -159,12 +159,13 @@ impl Validator for ModelValidator { recursion_guard: &'s mut RecursionGuard, ) -> ValResult<'data, PyObject> { if self.frozen { - return Err(ValError::new(ErrorType::FrozenInstance, field_value)); + return Err(ValError::new(ErrorTypeDefaults::FrozenInstance, field_value)); } else if self.root_model { return if field_name != ROOT_FIELD { Err(ValError::new_with_loc( ErrorType::NoSuchAttribute { attribute: field_name.to_string(), + context: None, }, field_value, field_name.to_string(), diff --git a/src/validators/model_fields.rs b/src/validators/model_fields.rs index ee2e99daa..8a1076ae7 100644 --- a/src/validators/model_fields.rs +++ b/src/validators/model_fields.rs @@ -7,7 +7,7 @@ use ahash::AHashSet; use crate::build_tools::py_schema_err; use crate::build_tools::{is_strict, schema_or_config_same, ExtraBehavior}; -use crate::errors::{py_err_string, ErrorType, ValError, ValLineError, ValResult}; +use crate::errors::{py_err_string, ErrorType, ErrorTypeDefaults, ValError, ValLineError, ValResult}; use crate::input::{ AttributesGenericIterator, DictGenericIterator, GenericMapping, Input, JsonObjectGenericIterator, MappingGenericIterator, @@ -133,10 +133,11 @@ impl Validator for ModelFieldsValidator { let errors: Vec = errors .into_iter() .map(|e| match e.error_type { - ErrorType::DictType => { + ErrorType::DictType { .. } => { let mut e = e; e.error_type = ErrorType::ModelType { class_name: self.model_name.clone(), + context: None, }; e } @@ -174,6 +175,7 @@ impl Validator for ModelFieldsValidator { errors.push(ValLineError::new_with_loc( ErrorType::GetAttributeError { error: py_err_string(py, err), + context: None, }, input, field.name.clone(), @@ -208,7 +210,7 @@ impl Validator for ModelFieldsValidator { model_dict.set_item(&field.name_py, value)?; } else { errors.push(field.lookup_key.error( - ErrorType::Missing, + ErrorTypeDefaults::Missing, input, self.loc_by_alias, &field.name @@ -226,7 +228,7 @@ impl Validator for ModelFieldsValidator { for err in line_errors { errors.push( err.with_outer_location(raw_key.as_loc_item()) - .with_type(ErrorType::InvalidKey), + .with_type(ErrorTypeDefaults::InvalidKey), ); } continue; @@ -241,7 +243,7 @@ impl Validator for ModelFieldsValidator { match self.extra_behavior { ExtraBehavior::Forbid => { errors.push(ValLineError::new_with_loc( - ErrorType::ExtraForbidden, + ErrorTypeDefaults::ExtraForbidden, value, raw_key.as_loc_item(), )); @@ -343,7 +345,7 @@ impl Validator for ModelFieldsValidator { let new_data = if let Some(field) = self.fields.iter().find(|f| f.name == field_name) { if field.frozen { Err(ValError::new_with_loc( - ErrorType::FrozenField, + ErrorTypeDefaults::FrozenField, field_value, field.name.to_string(), )) @@ -371,6 +373,7 @@ impl Validator for ModelFieldsValidator { return Err(ValError::new_with_loc( ErrorType::NoSuchAttribute { attribute: field_name.to_string(), + context: None, }, field_value, field_name.to_string(), diff --git a/src/validators/none.rs b/src/validators/none.rs index 380549d5c..ff1c94b35 100644 --- a/src/validators/none.rs +++ b/src/validators/none.rs @@ -1,7 +1,7 @@ use pyo3::prelude::*; use pyo3::types::PyDict; -use crate::errors::{ErrorType, ValError, ValResult}; +use crate::errors::{ErrorTypeDefaults, ValError, ValResult}; use crate::input::Input; use crate::recursion_guard::RecursionGuard; @@ -35,7 +35,7 @@ impl Validator for NoneValidator { ) -> ValResult<'data, PyObject> { match input.is_none() { true => Ok(py.None()), - false => Err(ValError::new(ErrorType::NoneRequired, input)), + false => Err(ValError::new(ErrorTypeDefaults::NoneRequired, input)), } } diff --git a/src/validators/string.rs b/src/validators/string.rs index 38d74cc20..4af1fb078 100644 --- a/src/validators/string.rs +++ b/src/validators/string.rs @@ -105,12 +105,24 @@ impl Validator for StrConstrainedValidator { }; if let Some(min_length) = self.min_length { if str_len.unwrap() < min_length { - return Err(ValError::new(ErrorType::StringTooShort { min_length }, input)); + return Err(ValError::new( + ErrorType::StringTooShort { + min_length, + context: None, + }, + input, + )); } } if let Some(max_length) = self.max_length { if str_len.unwrap() > max_length { - return Err(ValError::new(ErrorType::StringTooLong { max_length }, input)); + return Err(ValError::new( + ErrorType::StringTooLong { + max_length, + context: None, + }, + input, + )); } } @@ -119,6 +131,7 @@ impl Validator for StrConstrainedValidator { return Err(ValError::new( ErrorType::StringPatternMismatch { pattern: pattern.to_string(), + context: None, }, input, )); diff --git a/src/validators/time.rs b/src/validators/time.rs index d37f85af0..774b588f0 100644 --- a/src/validators/time.rs +++ b/src/validators/time.rs @@ -60,6 +60,7 @@ impl Validator for TimeValidator { return Err(ValError::new( ErrorType::$error { $constraint: constraint.to_string().into(), + context: None, }, input, )); diff --git a/src/validators/timedelta.rs b/src/validators/timedelta.rs index 03ac98025..fe8865462 100644 --- a/src/validators/timedelta.rs +++ b/src/validators/timedelta.rs @@ -86,6 +86,7 @@ impl Validator for TimeDeltaValidator { if !raw_timedelta.$constraint(constraint) { return Err(ValError::new( ErrorType::$error { + context: None, $constraint: duration_as_pytimedelta(py, constraint)? .repr()? .to_string() diff --git a/src/validators/tuple.rs b/src/validators/tuple.rs index 5e03e9d73..de5fb13c9 100644 --- a/src/validators/tuple.rs +++ b/src/validators/tuple.rs @@ -3,7 +3,7 @@ use pyo3::prelude::*; use pyo3::types::{PyDict, PyList, PyTuple}; use crate::build_tools::is_strict; -use crate::errors::{ErrorType, ValError, ValLineError, ValResult}; +use crate::errors::{ErrorType, ErrorTypeDefaults, ValError, ValLineError, ValResult}; use crate::input::{GenericIterable, Input}; use crate::recursion_guard::RecursionGuard; use crate::tools::SchemaDict; @@ -167,7 +167,7 @@ fn validate_tuple_positional<'s, 'data, T: Iterator>, if let Some(value) = validator.default_value(py, Some(index), extra, definitions, recursion_guard)? { output.push(value); } else { - errors.push(ValLineError::new_with_loc(ErrorType::Missing, input, index)); + errors.push(ValLineError::new_with_loc(ErrorTypeDefaults::Missing, input, index)); } } } @@ -195,6 +195,7 @@ fn validate_tuple_positional<'s, 'data, T: Iterator>, field_type: "Tuple".to_string(), max_length: expected_length, actual_length: collection_len.unwrap_or(index), + context: None, }, input, )); diff --git a/src/validators/typed_dict.rs b/src/validators/typed_dict.rs index e49ebce3b..698b33fd3 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::{py_err_string, ErrorType, ValError, ValLineError, ValResult}; +use crate::errors::{py_err_string, ErrorType, ErrorTypeDefaults, ValError, ValLineError, ValResult}; use crate::input::{ AttributesGenericIterator, DictGenericIterator, GenericMapping, Input, JsonObjectGenericIterator, MappingGenericIterator, @@ -175,6 +175,7 @@ impl Validator for TypedDictValidator { errors.push(ValLineError::new_with_loc( ErrorType::GetAttributeError { error: py_err_string(py, err), + context: None, }, input, field.name.clone(), @@ -208,7 +209,7 @@ impl Validator for TypedDictValidator { output_dict.set_item(&field.name_py, value)?; } else if field.required { errors.push(field.lookup_key.error( - ErrorType::Missing, + ErrorTypeDefaults::Missing, input, self.loc_by_alias, &field.name @@ -225,7 +226,7 @@ impl Validator for TypedDictValidator { for err in line_errors { errors.push( err.with_outer_location(raw_key.as_loc_item()) - .with_type(ErrorType::InvalidKey), + .with_type(ErrorTypeDefaults::InvalidKey), ); } continue; @@ -240,7 +241,7 @@ impl Validator for TypedDictValidator { match self.extra_behavior { ExtraBehavior::Forbid => { errors.push(ValLineError::new_with_loc( - ErrorType::ExtraForbidden, + ErrorTypeDefaults::ExtraForbidden, value, raw_key.as_loc_item(), )); diff --git a/src/validators/union.rs b/src/validators/union.rs index 33ca600ff..11b790bf3 100644 --- a/src/validators/union.rs +++ b/src/validators/union.rs @@ -445,6 +445,7 @@ impl TaggedUnionValidator { discriminator: self.discriminator_repr.clone(), tag: tag.to_string(), expected_tags: self.tags_repr.clone(), + context: None, }, input, )), @@ -457,6 +458,7 @@ impl TaggedUnionValidator { None => ValError::new( ErrorType::UnionTagNotFound { discriminator: self.discriminator_repr.clone(), + context: None, }, input, ), diff --git a/src/validators/url.rs b/src/validators/url.rs index 022f880d2..81bb8ba27 100644 --- a/src/validators/url.rs +++ b/src/validators/url.rs @@ -10,7 +10,7 @@ use ahash::AHashSet; use url::{ParseError, SyntaxViolation, Url}; use crate::build_tools::{is_strict, py_schema_err}; -use crate::errors::{ErrorType, ValError, ValResult}; +use crate::errors::{ErrorType, ErrorTypeDefaults, ValError, ValResult}; use crate::input::Input; use crate::recursion_guard::RecursionGuard; use crate::tools::SchemaDict; @@ -73,7 +73,13 @@ impl Validator for UrlValidator { if let Some((ref allowed_schemes, ref expected_schemes_repr)) = self.allowed_schemes { if !allowed_schemes.contains(lib_url.scheme()) { let expected_schemes = expected_schemes_repr.clone(); - return Err(ValError::new(ErrorType::UrlScheme { expected_schemes }, input)); + return Err(ValError::new( + ErrorType::UrlScheme { + expected_schemes, + context: None, + }, + input, + )); } } @@ -130,7 +136,7 @@ impl UrlValidator { parse_url(&url_str, input, strict) } else { - Err(ValError::new(ErrorType::UrlType, input)) + Err(ValError::new(ErrorTypeDefaults::UrlType, input)) } } } @@ -139,7 +145,13 @@ impl UrlValidator { fn check_length<'s, 'data>(&self, input: &'data impl Input<'data>, url_str: &str) -> ValResult<'data, ()> { if let Some(max_length) = self.max_length { if url_str.len() > max_length { - return Err(ValError::new(ErrorType::UrlTooLong { max_length }, input)); + return Err(ValError::new( + ErrorType::UrlTooLong { + max_length, + context: None, + }, + input, + )); } } Ok(()) @@ -204,7 +216,13 @@ impl Validator for MultiHostUrlValidator { if let Some((ref allowed_schemes, ref expected_schemes_repr)) = self.allowed_schemes { if !allowed_schemes.contains(multi_url.scheme()) { let expected_schemes = expected_schemes_repr.clone(); - return Err(ValError::new(ErrorType::UrlScheme { expected_schemes }, input)); + return Err(ValError::new( + ErrorType::UrlScheme { + expected_schemes, + context: None, + }, + input, + )); } } match check_sub_defaults( @@ -258,7 +276,7 @@ impl MultiHostUrlValidator { self.check_length(input, || lib_url.as_str().len())?; Ok(PyMultiHostUrl::new(lib_url, None)) } else { - Err(ValError::new(ErrorType::UrlType, input)) + Err(ValError::new(ErrorTypeDefaults::UrlType, input)) } } } @@ -270,7 +288,13 @@ impl MultiHostUrlValidator { { if let Some(max_length) = self.max_length { if func() > max_length { - return Err(ValError::new(ErrorType::UrlTooLong { max_length }, input)); + return Err(ValError::new( + ErrorType::UrlTooLong { + max_length, + context: None, + }, + input, + )); } } Ok(()) @@ -287,6 +311,7 @@ fn parse_multihost_url<'url, 'input>( Err(ValError::new( ErrorType::UrlParsing { error: $parse_error.to_string(), + context: None, }, input, )) @@ -404,6 +429,7 @@ fn parse_url<'url, 'input>( return Err(ValError::new( ErrorType::UrlParsing { error: EMPTY_INPUT.into(), + context: None, }, input, )); @@ -430,6 +456,7 @@ fn parse_url<'url, 'input>( Err(ValError::new( ErrorType::UrlSyntaxViolation { error: vio.description().into(), + context: None, }, input, )) @@ -437,10 +464,24 @@ fn parse_url<'url, 'input>( Ok(url) } } - Err(e) => Err(ValError::new(ErrorType::UrlParsing { error: e.to_string() }, input)), + Err(e) => Err(ValError::new( + ErrorType::UrlParsing { + error: e.to_string(), + context: None, + }, + input, + )), } } else { - Url::parse(url_str).map_err(move |e| ValError::new(ErrorType::UrlParsing { error: e.to_string() }, input)) + Url::parse(url_str).map_err(move |e| { + ValError::new( + ErrorType::UrlParsing { + error: e.to_string(), + context: None, + }, + input, + ) + }) } } @@ -452,13 +493,17 @@ fn check_sub_defaults( default_port: Option, default_path: &Option, ) -> Result<(), ErrorType> { - let map_parse_err = |e: ParseError| ErrorType::UrlParsing { error: e.to_string() }; + let map_parse_err = |e: ParseError| ErrorType::UrlParsing { + error: e.to_string(), + context: None, + }; if !lib_url.has_host() { if let Some(ref default_host) = default_host { lib_url.set_host(Some(default_host)).map_err(map_parse_err)?; } else if host_required { return Err(ErrorType::UrlParsing { error: ParseError::EmptyHost.to_string(), + context: None, }); } } diff --git a/src/validators/uuid.rs b/src/validators/uuid.rs index 6a5cd9c05..6a15080f5 100644 --- a/src/validators/uuid.rs +++ b/src/validators/uuid.rs @@ -7,7 +7,7 @@ use pyo3::types::{PyDict, PyType}; use uuid::Uuid; use crate::build_tools::is_strict; -use crate::errors::{ErrorType, ValError, ValResult}; +use crate::errors::{ErrorType, ErrorTypeDefaults, ValError, ValResult}; use crate::input::Input; use crate::recursion_guard::RecursionGuard; use crate::tools::SchemaDict; @@ -98,7 +98,13 @@ impl Validator for UuidValidator { let py_input_version: usize = py_input.getattr(intern!(py, "version"))?.extract()?; let expected_version = usize::from(expected_version); if expected_version != py_input_version { - return Err(ValError::new(ErrorType::UuidVersion { expected_version }, input)); + return Err(ValError::new( + ErrorType::UuidVersion { + expected_version, + context: None, + }, + input, + )); } } Ok(py_input.to_object(py)) @@ -106,6 +112,7 @@ impl Validator for UuidValidator { Err(ValError::new( ErrorType::IsInstanceOf { class: class.name().unwrap_or("UUID").to_string(), + context: None, }, input, )) @@ -138,13 +145,20 @@ impl UuidValidator { Some(either_string) => { let cow = either_string.as_cow()?; let uuid_str = cow.as_ref(); - Uuid::parse_str(uuid_str) - .map_err(|e| ValError::new(ErrorType::UuidParsing { error: e.to_string() }, input))? + Uuid::parse_str(uuid_str).map_err(|e| { + ValError::new( + ErrorType::UuidParsing { + error: e.to_string(), + context: None, + }, + input, + ) + })? } None => { let either_bytes = input .validate_bytes(true) - .map_err(|_| ValError::new(ErrorType::UuidType, input))?; + .map_err(|_| ValError::new(ErrorTypeDefaults::UuidType, input))?; let bytes_slice = either_bytes.as_slice(); 'parse: { // Try parsing as utf8, but don't care if it fails @@ -153,8 +167,15 @@ impl UuidValidator { break 'parse uuid; } } - Uuid::from_slice(bytes_slice) - .map_err(|e| ValError::new(ErrorType::UuidParsing { error: e.to_string() }, input))? + Uuid::from_slice(bytes_slice).map_err(|e| { + ValError::new( + ErrorType::UuidParsing { + error: e.to_string(), + context: None, + }, + input, + ) + })? } } }; @@ -163,7 +184,13 @@ impl UuidValidator { let v1 = uuid.get_version_num(); let expected_version = usize::from(expected_version); if v1 != expected_version { - return Err(ValError::new(ErrorType::UuidVersion { expected_version }, input)); + return Err(ValError::new( + ErrorType::UuidVersion { + expected_version, + context: None, + }, + input, + )); } }; Ok(uuid) diff --git a/tests/test_errors.py b/tests/test_errors.py index 59d3153fd..71d773fff 100644 --- a/tests/test_errors.py +++ b/tests/test_errors.py @@ -158,15 +158,13 @@ def test_pydantic_error_type(): def test_pydantic_error_type_nested_ctx(): - e = PydanticKnownError('json_invalid', {'error': 'Test', 'foo': {'bar': []}}) + ctx = {'error': 'Test', 'foo': {'bar': []}} + e = PydanticKnownError('json_invalid', ctx) assert e.message() == 'Invalid JSON: Test' assert e.type == 'json_invalid' - # TODO fix inconsistency here with context. It should include "foo" key - # assert e.context == {'error': 'Test', 'foo': {'bar': []}} - assert e.context == {'error': 'Test'} + assert e.context == ctx assert str(e) == 'Invalid JSON: Test' - # assert repr(e) == "Invalid JSON: Test [type=json_invalid, context={'error': 'Test', 'foo': {'bar': []}}]" - assert repr(e) == "Invalid JSON: Test [type=json_invalid, context={'error': 'Test'}]" + assert repr(e) == f"Invalid JSON: Test [type=json_invalid, context={ctx}]" def test_pydantic_error_type_raise_no_ctx(): @@ -332,7 +330,7 @@ def test_error_decimal(): e = PydanticKnownError('greater_than', {'gt': Decimal('42.1')}) assert e.message() == 'Input should be greater than 42.1' assert e.type == 'greater_than' - assert e.context == {'gt': 42.1} + assert e.context == {'gt': Decimal("42.1")} def test_custom_error_decimal(): @@ -382,9 +380,9 @@ def test_type_error_error(): PydanticKnownError('greater_than', {'gt': []}) -def test_does_not_require_context(): - with pytest.raises(TypeError, match="^'json_type' errors do not require context$"): - PydanticKnownError('json_type', {'gt': 123}) +def test_custom_context_for_simple_error(): + err = PydanticKnownError('json_type', {'foo': 'bar'}) + assert err.context == {'foo': 'bar'} def test_all_errors(): @@ -637,7 +635,7 @@ def test_raise_validation_error(): assert exc_info.value.errors(include_url=False) == [ {'type': 'greater_than', 'loc': ('a', 2), 'msg': 'Input should be greater than 5', 'input': 4, 'ctx': {'gt': 5}} ] - with pytest.raises(TypeError, match='GreaterThan requires context: {gt: Number}'): + with pytest.raises(TypeError, match="GreaterThan: 'gt' required in context"): raise ValidationError.from_exception_data('Foobar', [{'type': 'greater_than', 'loc': ('a', 2), 'input': 4}]) From 9e9ca5ee3cf9e8f45d5e49e2108ac1c5dfa972fb Mon Sep 17 00:00:00 2001 From: Markus Sintonen Date: Fri, 28 Jul 2023 16:29:51 +0300 Subject: [PATCH 2/5] Remove extra wrapping layer in custom errors, add more tests --- src/errors/types.rs | 68 +++++++++++++++--------------- src/errors/validation_exception.rs | 6 +-- src/errors/value_exception.rs | 43 ++++++++++--------- tests/test_errors.py | 58 ++++++++++++++++++++++--- 4 files changed, 112 insertions(+), 63 deletions(-) diff --git a/src/errors/types.rs b/src/errors/types.rs index 78822c24b..e62bfb217 100644 --- a/src/errors/types.rs +++ b/src/errors/types.rs @@ -119,7 +119,7 @@ macro_rules! error_types { } } - fn py_dict_merge_ctx(&self, py: Python, dict: &PyDict) -> PyResult<()> { + fn py_dict_update_ctx(&self, py: Python, dict: &PyDict) -> PyResult<()> { match self { $( Self::$item { context, $($key,)* } => { @@ -299,7 +299,9 @@ error_types! { }, // Note: strum message and serialize are not used here CustomError { - custom_error: {ctx_type: PydanticCustomError, ctx_fn: do_nothing, extract_type: PydanticCustomError}, + // context is a common field in all enums + error_type: {ctx_type: String, ctx_fn: do_nothing, extract_type: String}, + message_template: {ctx_type: String, ctx_fn: do_nothing, extract_type: String}, }, // --------------------- // literals @@ -439,10 +441,11 @@ fn plural_s(value: usize) -> &'static str { static ERROR_TYPE_LOOKUP: GILOnceCell> = GILOnceCell::new(); impl ErrorType { - pub fn new_custom_error(custom_error: PydanticCustomError) -> Self { + pub fn new_custom_error(py: Python, custom_error: PydanticCustomError) -> Self { Self::CustomError { - custom_error, - context: None, + error_type: custom_error.error_type(), + message_template: custom_error.message_template(), + context: custom_error.context(py), } } @@ -545,19 +548,19 @@ impl ErrorType { pub fn message_template_json(&self) -> &'static str { match self { - Self::NoneRequired {..} => "Input should be null", - Self::ListType {..} - | Self::TupleType {..} - | Self::IterableType {..} - | Self::SetType {..} - | Self::FrozenSetType {..} => "Input should be a valid array", - Self::ModelType {..} - | Self::ModelAttributesType {..} - | Self::DictType {..} - | Self::DataclassType {..} => "Input should be an object", - Self::TimeDeltaType {..} => "Input should be a valid duration", - Self::TimeDeltaParsing {..} => "Input should be a valid duration, {error}", - Self::ArgumentsType {..} => "Arguments must be an array or an object", + Self::NoneRequired { .. } => "Input should be null", + Self::ListType { .. } + | Self::TupleType { .. } + | Self::IterableType { .. } + | Self::SetType { .. } + | Self::FrozenSetType { .. } => "Input should be a valid array", + Self::ModelType { .. } + | Self::ModelAttributesType { .. } + | Self::DictType { .. } + | Self::DataclassType { .. } => "Input should be an object", + Self::TimeDeltaType { .. } => "Input should be a valid duration", + Self::TimeDeltaParsing { .. } => "Input should be a valid duration, {error}", + Self::ArgumentsType { .. } => "Arguments must be an array or an object", _ => self.message_template_python(), } } @@ -579,10 +582,7 @@ impl ErrorType { pub fn type_string(&self) -> String { match self { - Self::CustomError { - custom_error: value_error, - .. - } => value_error.error_type(), + Self::CustomError { error_type, .. } => error_type.clone(), _ => self.to_string(), } } @@ -643,9 +643,10 @@ impl ErrorType { render!(tmpl, error) } Self::CustomError { - custom_error: value_error, + message_template, + context, .. - } => value_error.message(py), + } => PydanticCustomError::format_message(message_template, context.as_ref().map(|c| c.as_ref(py))), Self::LiteralError { expected, .. } => render!(tmpl, expected), Self::DateParsing { error, .. } => render!(tmpl, error), Self::DateFromDatetimeParsing { error, .. } => render!(tmpl, error), @@ -677,16 +678,15 @@ impl ErrorType { pub fn py_dict(&self, py: Python) -> PyResult>> { let dict = PyDict::new(py); - self.py_dict_merge_ctx(py, dict)?; - match self { - Self::CustomError { custom_error, .. } => { - dict.del_item("custom_error")?; // Custom error data is merged to the root of ctx - if let Some(custom_ctx) = custom_error.context(py) { - dict.update(custom_ctx.as_ref(py).downcast()?)? - } - } - _ => {} - }; + self.py_dict_update_ctx(py, dict)?; + + if let Self::CustomError { .. } = self { + // Custom error type and message are handled separately by the caller. + // They are added to the root of the ErrorDetails. + dict.del_item("error_type")?; + dict.del_item("message_template")?; + } + if dict.is_empty() { return Ok(None); } diff --git a/src/errors/validation_exception.rs b/src/errors/validation_exception.rs index 67db6651a..daec6b910 100644 --- a/src/errors/validation_exception.rs +++ b/src/errors/validation_exception.rs @@ -344,7 +344,7 @@ impl TryFrom<&PyAny> for PyLineError { let context: Option<&PyDict> = dict.get_as(intern!(py, "ctx"))?; ErrorType::new(py, type_str.to_str()?, context)? } else if let Ok(custom_error) = type_raw.extract::() { - ErrorType::new_custom_error(custom_error) + ErrorType::new_custom_error(py, custom_error) } else { return Err(PyTypeError::new_err( "`type` should be a `str` or `PydanticCustomError`", @@ -390,7 +390,7 @@ impl PyLineError { } if let Some(url_prefix) = url_prefix { match self.error_type { - ErrorType::CustomError { custom_error: _, .. } => { + ErrorType::CustomError { .. } => { // Don't add URLs for custom errors } _ => { @@ -428,7 +428,7 @@ impl PyLineError { } if let Some(url_prefix) = url_prefix { match self.error_type { - ErrorType::CustomError { custom_error: _, .. } => { + ErrorType::CustomError { .. } => { // Don't display URLs for custom errors output.push(']'); } diff --git a/src/errors/value_exception.rs b/src/errors/value_exception.rs index 69c2f6a11..d37c48c9d 100644 --- a/src/errors/value_exception.rs +++ b/src/errors/value_exception.rs @@ -73,10 +73,6 @@ impl PydanticCustomError { } } - pub fn to_object(&self, py: Python<'_>) -> PyObject { - self.context(py).into_py(py) - } - #[getter(type)] pub fn error_type(&self) -> String { self.error_type.clone() @@ -93,21 +89,7 @@ impl PydanticCustomError { } pub fn message(&self, py: Python) -> PyResult { - let mut message = self.message_template.clone(); - if let Some(ref context) = self.context { - for (key, value) in context.as_ref(py) { - let key: &PyString = key.downcast()?; - if let Ok(py_str) = value.downcast::() { - message = message.replace(&format!("{{{}}}", key.to_str()?), py_str.to_str()?); - } else if let Ok(value_int) = extract_i64(value) { - message = message.replace(&format!("{{{}}}", key.to_str()?), &value_int.to_string()); - } else { - // fallback for anything else just in case - message = message.replace(&format!("{{{}}}", key.to_str()?), &value.to_string()); - } - } - } - Ok(message) + Self::format_message(&self.message_template, self.context.as_ref().map(|c| c.as_ref(py))) } fn __str__(&self, py: Python) -> PyResult { @@ -126,11 +108,30 @@ impl PydanticCustomError { impl PydanticCustomError { pub fn into_val_error<'a>(self, input: &'a impl Input<'a>) -> ValError<'a> { let error_type = ErrorType::CustomError { - custom_error: self, - context: None, + error_type: self.error_type, + message_template: self.message_template, + context: self.context, }; ValError::new(error_type, input) } + + pub fn format_message(message_template: &String, context: Option<&PyDict>) -> PyResult { + let mut message = message_template.clone(); + if let Some(ctx) = context { + for (key, value) in ctx { + let key: &PyString = key.downcast()?; + if let Ok(py_str) = value.downcast::() { + message = message.replace(&format!("{{{}}}", key.to_str()?), py_str.to_str()?); + } else if let Ok(value_int) = extract_i64(value) { + message = message.replace(&format!("{{{}}}", key.to_str()?), &value_int.to_string()); + } else { + // fallback for anything else just in case + message = message.replace(&format!("{{{}}}", key.to_str()?), &value.to_string()); + } + } + } + Ok(message) + } } #[pyclass(extends=PyValueError, module="pydantic_core._pydantic_core")] diff --git a/tests/test_errors.py b/tests/test_errors.py index 71d773fff..029df1a6a 100644 --- a/tests/test_errors.py +++ b/tests/test_errors.py @@ -164,7 +164,7 @@ def test_pydantic_error_type_nested_ctx(): assert e.type == 'json_invalid' assert e.context == ctx assert str(e) == 'Invalid JSON: Test' - assert repr(e) == f"Invalid JSON: Test [type=json_invalid, context={ctx}]" + assert repr(e) == f'Invalid JSON: Test [type=json_invalid, context={ctx}]' def test_pydantic_error_type_raise_no_ctx(): @@ -183,9 +183,35 @@ def f(input_value, info): ] -def test_pydantic_error_type_raise_ctx(): +@pytest.mark.parametrize( + 'extra', [{}, {'foo': 1}, {'foo': {'bar': []}}, {'foo': {'bar': object()}}, {'foo': Decimal('42.1')}] +) +def test_pydantic_error_type_raise_ctx(extra: dict): + ctx = {'gt': 42, **extra} + + def f(input_value, info): + raise PydanticKnownError('greater_than', ctx) + + v = SchemaValidator( + {'type': 'function-before', 'function': {'type': 'general', 'function': f}, 'schema': {'type': 'int'}} + ) + + with pytest.raises(ValidationError) as exc_info: + v.validate_python(4) + # insert_assert(exc_info.value.errors(include_url=False)) + assert exc_info.value.errors(include_url=False) == [ + {'type': 'greater_than', 'loc': (), 'msg': 'Input should be greater than 42', 'input': 4, 'ctx': ctx} + ] + + +@pytest.mark.parametrize( + 'extra', [{}, {'foo': 1}, {'foo': {'bar': []}}, {'foo': {'bar': object()}}, {'foo': Decimal('42.1')}] +) +def test_pydantic_custom_error_type_raise_custom_ctx(extra: dict): + ctx = {'val': 42, **extra} + def f(input_value, info): - raise PydanticKnownError('greater_than', {'gt': 42}) + raise PydanticCustomError('my_error', 'my message with {val}', ctx) v = SchemaValidator( {'type': 'function-before', 'function': {'type': 'general', 'function': f}, 'schema': {'type': 'int'}} @@ -195,7 +221,7 @@ def f(input_value, info): v.validate_python(4) # insert_assert(exc_info.value.errors(include_url=False)) assert exc_info.value.errors(include_url=False) == [ - {'type': 'greater_than', 'loc': (), 'msg': 'Input should be greater than 42', 'input': 4, 'ctx': {'gt': 42.0}} + {'type': 'my_error', 'loc': (), 'msg': 'my message with 42', 'input': 4, 'ctx': ctx} ] @@ -330,7 +356,7 @@ def test_error_decimal(): e = PydanticKnownError('greater_than', {'gt': Decimal('42.1')}) assert e.message() == 'Input should be greater than 42.1' assert e.type == 'greater_than' - assert e.context == {'gt': Decimal("42.1")} + assert e.context == {'gt': Decimal('42.1')} def test_custom_error_decimal(): @@ -707,6 +733,28 @@ def test_raise_validation_error_custom_nested_ctx(msg: str, result_msg: str): assert exc_info.value.json(include_url=False) == IsJson([{**expected_error_detail, 'loc': []}]) +def test_raise_validation_error_known_class_ctx(): + custom_data = Foobar() + ctx = {'gt': 10, 'foo': {'bar': custom_data}} + + with pytest.raises(ValidationError) as exc_info: + raise ValidationError.from_exception_data('MyTitle', [{'type': 'greater_than', 'input': 9, 'ctx': ctx}]) + + expected_error_detail = { + 'type': 'greater_than', + 'loc': (), + 'msg': 'Input should be greater than 10', + 'input': 9, + 'ctx': ctx, + } + + # insert_assert(exc_info.value.errors(include_url=False)) + assert exc_info.value.errors(include_url=False) == [expected_error_detail] + assert exc_info.value.json(include_url=False) == IsJson( + [{**expected_error_detail, 'loc': [], 'ctx': {'gt': 10, 'foo': {'bar': str(custom_data)}}}] + ) + + def test_raise_validation_error_custom_class_ctx(): custom_data = Foobar() ctx = {'foo': {'bar': custom_data}} From 38497d9eb850ece690f5a1d12a35a82f060f540f Mon Sep 17 00:00:00 2001 From: Markus Sintonen Date: Fri, 28 Jul 2023 20:29:44 +0300 Subject: [PATCH 3/5] None and empty ctx handling consistent --- src/errors/types.rs | 32 ++++++++++++++++------------ src/errors/value_exception.rs | 4 ++-- tests/test_errors.py | 40 ++++++++++++++++++++++++++++++++++- 3 files changed, 60 insertions(+), 16 deletions(-) diff --git a/src/errors/types.rs b/src/errors/types.rs index e62bfb217..238cbf50d 100644 --- a/src/errors/types.rs +++ b/src/errors/types.rs @@ -119,7 +119,7 @@ macro_rules! error_types { } } - fn py_dict_update_ctx(&self, py: Python, dict: &PyDict) -> PyResult<()> { + fn py_dict_update_ctx(&self, py: Python, dict: &PyDict) -> PyResult { match self { $( Self::$item { context, $($key,)* } => { @@ -127,9 +127,11 @@ macro_rules! error_types { dict.set_item::<&str, Py>(stringify!($key), $key.to_object(py))?; )* if let Some(ctx) = context { - dict.update(ctx.as_ref(py).downcast()?)? + dict.update(ctx.as_ref(py).downcast()?)?; + Ok(true) + } else { + Ok(false) } - Ok(()) }, )+ } @@ -678,19 +680,23 @@ impl ErrorType { pub fn py_dict(&self, py: Python) -> PyResult>> { let dict = PyDict::new(py); - self.py_dict_update_ctx(py, dict)?; + let custom_ctx_used = self.py_dict_update_ctx(py, dict)?; if let Self::CustomError { .. } = self { - // Custom error type and message are handled separately by the caller. - // They are added to the root of the ErrorDetails. - dict.del_item("error_type")?; - dict.del_item("message_template")?; - } - - if dict.is_empty() { - return Ok(None); + if custom_ctx_used { + // Custom error type and message are handled separately by the caller. + // They are added to the root of the ErrorDetails. + dict.del_item("error_type")?; + dict.del_item("message_template")?; + Ok(Some(dict.into())) + } else { + Ok(None) + } + } else if custom_ctx_used || !dict.is_empty() { + Ok(Some(dict.into())) + } else { + Ok(None) } - return Ok(Some(dict.into())); } } diff --git a/src/errors/value_exception.rs b/src/errors/value_exception.rs index d37c48c9d..d0c08bf5f 100644 --- a/src/errors/value_exception.rs +++ b/src/errors/value_exception.rs @@ -115,8 +115,8 @@ impl PydanticCustomError { ValError::new(error_type, input) } - pub fn format_message(message_template: &String, context: Option<&PyDict>) -> PyResult { - let mut message = message_template.clone(); + pub fn format_message(message_template: &str, context: Option<&PyDict>) -> PyResult { + let mut message = message_template.to_string(); if let Some(ctx) = context { for (key, value) in ctx { let key: &PyString = key.downcast()?; diff --git a/tests/test_errors.py b/tests/test_errors.py index 029df1a6a..0637fc956 100644 --- a/tests/test_errors.py +++ b/tests/test_errors.py @@ -1,6 +1,6 @@ import re from decimal import Decimal -from typing import Any +from typing import Any, Optional import pytest from dirty_equals import HasRepr, IsInstance, IsJson, IsStr @@ -204,6 +204,25 @@ def f(input_value, info): ] +@pytest.mark.parametrize('ctx', [None, {}]) +def test_pydantic_error_type_raise_custom_no_ctx(ctx: Optional[dict]): + def f(input_value, info): + raise PydanticKnownError('int_type', ctx) + + v = SchemaValidator( + {'type': 'function-before', 'function': {'type': 'general', 'function': f}, 'schema': {'type': 'int'}} + ) + + expect_ctx = {'ctx': {}} if ctx is not None else {} + + with pytest.raises(ValidationError) as exc_info: + v.validate_python(4) + # insert_assert(exc_info.value.errors(include_url=False)) + assert exc_info.value.errors(include_url=False) == [ + {'type': 'int_type', 'loc': (), 'msg': 'Input should be a valid integer', 'input': 4, **expect_ctx} + ] + + @pytest.mark.parametrize( 'extra', [{}, {'foo': 1}, {'foo': {'bar': []}}, {'foo': {'bar': object()}}, {'foo': Decimal('42.1')}] ) @@ -225,6 +244,25 @@ def f(input_value, info): ] +@pytest.mark.parametrize('ctx', [None, {}]) +def test_pydantic_custom_error_type_raise_custom_no_ctx(ctx: Optional[dict]): + def f(input_value, info): + raise PydanticCustomError('my_error', 'my message', ctx) + + v = SchemaValidator( + {'type': 'function-before', 'function': {'type': 'general', 'function': f}, 'schema': {'type': 'int'}} + ) + + expect_ctx = {'ctx': {}} if ctx is not None else {} + + with pytest.raises(ValidationError) as exc_info: + v.validate_python(4) + # insert_assert(exc_info.value.errors(include_url=False)) + assert exc_info.value.errors(include_url=False) == [ + {'type': 'my_error', 'loc': (), 'msg': 'my message', 'input': 4, **expect_ctx} + ] + + all_errors = [ ('no_such_attribute', "Object has no attribute 'wrong_name'", {'attribute': 'wrong_name'}), ('json_invalid', 'Invalid JSON: foobar', {'error': 'foobar'}), From ba85411826d3df894364a4ce6121b4778527d130 Mon Sep 17 00:00:00 2001 From: Markus Sintonen Date: Sat, 29 Jul 2023 21:44:52 +0200 Subject: [PATCH 4/5] Move ctx field handling to fn --- src/errors/types.rs | 138 ++++++++++++++++++++++++------------------- tests/test_errors.py | 15 ++++- 2 files changed, 88 insertions(+), 65 deletions(-) diff --git a/src/errors/types.rs b/src/errors/types.rs index 238cbf50d..fb712459d 100644 --- a/src/errors/types.rs +++ b/src/errors/types.rs @@ -1,3 +1,4 @@ +use std::any::type_name; use std::borrow::Cow; use std::fmt; @@ -58,8 +59,29 @@ pub fn list_all_errors(py: Python) -> PyResult<&PyList> { Ok(PyList::new(py, errors)) } -fn do_nothing(v: T) -> T { - v +fn field_from_context<'py, T: FromPyObject<'py>>( + context: Option<&'py PyDict>, + field_name: &str, + enum_name: &str, +) -> PyResult { + context + .ok_or(py_error_type!(PyTypeError; "{}: '{}' required in context", enum_name, field_name))? + .get_item(field_name) + .ok_or(py_error_type!(PyTypeError; "{}: '{}' required in context", enum_name, field_name))? + .extract::() + .map_err(|_| { + let type_name = type_name::().split("::").last().unwrap(); + py_error_type!(PyTypeError; "{}: '{}' context value must be a {}", enum_name, field_name, type_name) + }) +} + +fn str_cow_field_from_context( + context: Option<&PyDict>, + field_name: &str, + enum_name: &str, +) -> PyResult> { + let res: String = field_from_context(context, field_name, enum_name)?; + Ok(Cow::Owned(res)) } macro_rules! basic_error_default { @@ -77,7 +99,7 @@ macro_rules! error_types { ( $( $item:ident { - $($key:ident: {ctx_type: $ctx_type:ty, ctx_fn: $ctx_fn:path, extract_type: $extract_type:ty}),* $(,)? + $($key:ident: {ctx_type: $ctx_type:ty, ctx_fn: $ctx_fn:path}),* $(,)? }, )+ ) => { @@ -104,14 +126,7 @@ macro_rules! error_types { Ok(Self::$item { context: context.map(|c| c.into_py(py)), $( - $key: $ctx_fn( - context - .ok_or(py_error_type!(PyTypeError; "{}: '{}' required in context", stringify!($item), stringify!($key)))? - .get_item(stringify!($key)) - .ok_or(py_error_type!(PyTypeError; "{}: '{}' required in context", stringify!($item), stringify!($key)))? - .extract::<$extract_type>() - .map_err(|_| py_error_type!(PyTypeError; "{}: '{}' context value must be a {}", stringify!($item), stringify!($key), stringify!($extract_type)))? - ), + $key: $ctx_fn(context, stringify!($key), stringify!($item))?, )* }) }, @@ -159,12 +174,12 @@ error_types! { // --------------------- // Assignment errors NoSuchAttribute { - attribute: {ctx_type: String, ctx_fn: do_nothing, extract_type: String}, + attribute: {ctx_type: String, ctx_fn: field_from_context}, }, // --------------------- // JSON errors JsonInvalid { - error: {ctx_type: String, ctx_fn: do_nothing, extract_type: String}, + error: {ctx_type: String, ctx_fn: field_from_context}, }, JsonType {}, // --------------------- @@ -178,21 +193,21 @@ error_types! { ExtraForbidden {}, InvalidKey {}, GetAttributeError { - error: {ctx_type: String, ctx_fn: do_nothing, extract_type: String}, + error: {ctx_type: String, ctx_fn: field_from_context}, }, // --------------------- // model class specific errors ModelType { - class_name: {ctx_type: String, ctx_fn: do_nothing, extract_type: String}, + class_name: {ctx_type: String, ctx_fn: field_from_context}, }, ModelAttributesType {}, // --------------------- // dataclass errors (we don't talk about ArgsKwargs here for simplicity) DataclassType { - class_name: {ctx_type: String, ctx_fn: do_nothing, extract_type: String}, + class_name: {ctx_type: String, ctx_fn: field_from_context}, }, DataclassExactType { - class_name: {ctx_type: String, ctx_fn: do_nothing, extract_type: String}, + class_name: {ctx_type: String, ctx_fn: field_from_context}, }, // --------------------- // None errors @@ -201,38 +216,38 @@ error_types! { // generic comparison errors - used for all inequality comparisons except int and float which have their // own type, bounds arguments are Strings so they can be created from any type GreaterThan { - gt: {ctx_type: Number, ctx_fn: do_nothing, extract_type: Number}, + gt: {ctx_type: Number, ctx_fn: field_from_context}, }, GreaterThanEqual { - ge: {ctx_type: Number, ctx_fn: do_nothing, extract_type: Number}, + ge: {ctx_type: Number, ctx_fn: field_from_context}, }, LessThan { - lt: {ctx_type: Number, ctx_fn: do_nothing, extract_type: Number}, + lt: {ctx_type: Number, ctx_fn: field_from_context}, }, LessThanEqual { - le: {ctx_type: Number, ctx_fn: do_nothing, extract_type: Number}, + le: {ctx_type: Number, ctx_fn: field_from_context}, }, MultipleOf { - multiple_of: {ctx_type: Number, ctx_fn: do_nothing, extract_type: Number}, + multiple_of: {ctx_type: Number, ctx_fn: field_from_context}, }, FiniteNumber {}, // --------------------- // generic length errors - used for everything with a length except strings and bytes which need custom messages TooShort { - field_type: {ctx_type: String, ctx_fn: do_nothing, extract_type: String}, - min_length: {ctx_type: usize, ctx_fn: do_nothing, extract_type: usize}, - actual_length: {ctx_type: usize, ctx_fn: do_nothing, extract_type: usize}, + field_type: {ctx_type: String, ctx_fn: field_from_context}, + min_length: {ctx_type: usize, ctx_fn: field_from_context}, + actual_length: {ctx_type: usize, ctx_fn: field_from_context}, }, TooLong { - field_type: {ctx_type: String, ctx_fn: do_nothing, extract_type: String}, - max_length: {ctx_type: usize, ctx_fn: do_nothing, extract_type: usize}, - actual_length: {ctx_type: usize, ctx_fn: do_nothing, extract_type: usize}, + field_type: {ctx_type: String, ctx_fn: field_from_context}, + max_length: {ctx_type: usize, ctx_fn: field_from_context}, + actual_length: {ctx_type: usize, ctx_fn: field_from_context}, }, // --------------------- // generic collection and iteration errors IterableType {}, IterationError { - error: {ctx_type: String, ctx_fn: do_nothing, extract_type: String}, + error: {ctx_type: String, ctx_fn: field_from_context}, }, // --------------------- // string errors @@ -240,24 +255,24 @@ error_types! { StringSubType {}, StringUnicode {}, StringTooShort { - min_length: {ctx_type: usize, ctx_fn: do_nothing, extract_type: usize}, + min_length: {ctx_type: usize, ctx_fn: field_from_context}, }, StringTooLong { - max_length: {ctx_type: usize, ctx_fn: do_nothing, extract_type: usize}, + max_length: {ctx_type: usize, ctx_fn: field_from_context}, }, StringPatternMismatch { - pattern: {ctx_type: String, ctx_fn: do_nothing, extract_type: String}, + pattern: {ctx_type: String, ctx_fn: field_from_context}, }, // --------------------- // enum errors Enum { - expected: {ctx_type: String, ctx_fn: do_nothing, extract_type: String}, + expected: {ctx_type: String, ctx_fn: field_from_context}, }, // --------------------- // dict errors DictType {}, MappingType { - error: {ctx_type: Cow<'static, str>, ctx_fn: Cow::Owned, extract_type: String}, + error: {ctx_type: Cow<'static, str>, ctx_fn: str_cow_field_from_context}, }, // --------------------- // list errors @@ -286,38 +301,38 @@ error_types! { // bytes errors BytesType {}, BytesTooShort { - min_length: {ctx_type: usize, ctx_fn: do_nothing, extract_type: usize}, + min_length: {ctx_type: usize, ctx_fn: field_from_context}, }, BytesTooLong { - max_length: {ctx_type: usize, ctx_fn: do_nothing, extract_type: usize}, + max_length: {ctx_type: usize, ctx_fn: field_from_context}, }, // --------------------- // python errors from functions ValueError { - error: {ctx_type: Option, ctx_fn: do_nothing, extract_type: Option}, // Use Option because EnumIter requires Default to be implemented + error: {ctx_type: Option, ctx_fn: field_from_context}, // Use Option because EnumIter requires Default to be implemented }, AssertionError { - error: {ctx_type: Option, ctx_fn: do_nothing, extract_type: Option}, // Use Option because EnumIter requires Default to be implemented + error: {ctx_type: Option, ctx_fn: field_from_context}, // Use Option because EnumIter requires Default to be implemented }, // Note: strum message and serialize are not used here CustomError { // context is a common field in all enums - error_type: {ctx_type: String, ctx_fn: do_nothing, extract_type: String}, - message_template: {ctx_type: String, ctx_fn: do_nothing, extract_type: String}, + error_type: {ctx_type: String, ctx_fn: field_from_context}, + message_template: {ctx_type: String, ctx_fn: field_from_context}, }, // --------------------- // literals LiteralError { - expected: {ctx_type: String, ctx_fn: do_nothing, extract_type: String}, + expected: {ctx_type: String, ctx_fn: field_from_context}, }, // --------------------- // date errors DateType {}, DateParsing { - error: {ctx_type: Cow<'static, str>, ctx_fn: Cow::Owned, extract_type: String}, + error: {ctx_type: Cow<'static, str>, ctx_fn: str_cow_field_from_context}, }, DateFromDatetimeParsing { - error: {ctx_type: String, ctx_fn: do_nothing, extract_type: String}, + error: {ctx_type: String, ctx_fn: field_from_context}, }, DateFromDatetimeInexact {}, DatePast {}, @@ -326,16 +341,16 @@ error_types! { // date errors TimeType {}, TimeParsing { - error: {ctx_type: Cow<'static, str>, ctx_fn: Cow::Owned, extract_type: String}, + error: {ctx_type: Cow<'static, str>, ctx_fn: str_cow_field_from_context}, }, // --------------------- // datetime errors DatetimeType {}, DatetimeParsing { - error: {ctx_type: Cow<'static, str>, ctx_fn: Cow::Owned, extract_type: String}, + error: {ctx_type: Cow<'static, str>, ctx_fn: str_cow_field_from_context}, }, DatetimeObjectInvalid { - error: {ctx_type: String, ctx_fn: do_nothing, extract_type: String}, + error: {ctx_type: String, ctx_fn: field_from_context}, }, DatetimePast {}, DatetimeFuture {}, @@ -344,14 +359,14 @@ error_types! { TimezoneNaive {}, TimezoneAware {}, TimezoneOffset { - tz_expected: {ctx_type: i32, ctx_fn: do_nothing, extract_type: i32}, - tz_actual: {ctx_type: i32, ctx_fn: do_nothing, extract_type: i32}, + tz_expected: {ctx_type: i32, ctx_fn: field_from_context}, + tz_actual: {ctx_type: i32, ctx_fn: field_from_context}, }, // --------------------- // timedelta errors TimeDeltaType {}, TimeDeltaParsing { - error: {ctx_type: Cow<'static, str>, ctx_fn: Cow::Owned, extract_type: String}, + error: {ctx_type: Cow<'static, str>, ctx_fn: str_cow_field_from_context}, }, // --------------------- // frozenset errors @@ -359,21 +374,21 @@ error_types! { // --------------------- // introspection types - e.g. isinstance, callable IsInstanceOf { - class: {ctx_type: String, ctx_fn: do_nothing, extract_type: String}, + class: {ctx_type: String, ctx_fn: field_from_context}, }, IsSubclassOf { - class: {ctx_type: String, ctx_fn: do_nothing, extract_type: String}, + class: {ctx_type: String, ctx_fn: field_from_context}, }, CallableType {}, // --------------------- // union errors UnionTagInvalid { - discriminator: {ctx_type: String, ctx_fn: do_nothing, extract_type: String}, - tag: {ctx_type: String, ctx_fn: do_nothing, extract_type: String}, - expected_tags: {ctx_type: String, ctx_fn: do_nothing, extract_type: String}, + discriminator: {ctx_type: String, ctx_fn: field_from_context}, + tag: {ctx_type: String, ctx_fn: field_from_context}, + expected_tags: {ctx_type: String, ctx_fn: field_from_context}, }, UnionTagNotFound { - discriminator: {ctx_type: String, ctx_fn: do_nothing, extract_type: String}, + discriminator: {ctx_type: String, ctx_fn: field_from_context}, }, // --------------------- // argument errors @@ -389,24 +404,24 @@ error_types! { UrlType {}, UrlParsing { // would be great if this could be a static cow, waiting for https://github.com/servo/rust-url/issues/801 - error: {ctx_type: String, ctx_fn: do_nothing, extract_type: String}, + error: {ctx_type: String, ctx_fn: field_from_context}, }, UrlSyntaxViolation { - error: {ctx_type: Cow<'static, str>, ctx_fn: Cow::Owned, extract_type: String}, + error: {ctx_type: Cow<'static, str>, ctx_fn: str_cow_field_from_context}, }, UrlTooLong { - max_length: {ctx_type: usize, ctx_fn: do_nothing, extract_type: usize}, + max_length: {ctx_type: usize, ctx_fn: field_from_context}, }, UrlScheme { - expected_schemes: {ctx_type: String, ctx_fn: do_nothing, extract_type: String}, + expected_schemes: {ctx_type: String, ctx_fn: field_from_context}, }, // UUID errors, UuidType {}, UuidParsing { - error: {ctx_type: String, ctx_fn: do_nothing, extract_type: String}, + error: {ctx_type: String, ctx_fn: field_from_context}, }, UuidVersion { - expected_version: {ctx_type: usize, ctx_fn: do_nothing, extract_type: usize}, + expected_version: {ctx_type: usize, ctx_fn: field_from_context}, }, } @@ -544,7 +559,6 @@ impl ErrorType { Self::UuidType{..} => "UUID input should be a string, bytes or UUID object", Self::UuidParsing {..} => "Input should be a valid UUID, {error}", Self::UuidVersion {..} => "UUID version {expected_version} expected" - } } diff --git a/tests/test_errors.py b/tests/test_errors.py index 0637fc956..7ba010ea6 100644 --- a/tests/test_errors.py +++ b/tests/test_errors.py @@ -439,9 +439,18 @@ def test_omit_exc_repr(): assert str(PydanticOmit()) == 'PydanticOmit()' -def test_type_error_error(): - with pytest.raises(TypeError, match="^GreaterThan: 'gt' context value must be a Number$"): - PydanticKnownError('greater_than', {'gt': []}) +@pytest.mark.parametrize( + 'error,ctx,expect', + [ + ('greater_than', {'gt': []}, "GreaterThan: 'gt' context value must be a Number"), + ('model_type', {'class_name': []}, "ModelType: 'class_name' context value must be a String"), + ('date_parsing', {'error': []}, "DateParsing: 'error' context value must be a String"), + ('string_too_short', {'min_length': []}, "StringTooShort: 'min_length' context value must be a usize"), + ], +) +def test_type_error_error(error: str, ctx: dict, expect: str): + with pytest.raises(TypeError, match=f'^{expect}$'): + PydanticKnownError(error, ctx) def test_custom_context_for_simple_error(): From 460b4a691e70a0833114778b8d0e20eec7e23857 Mon Sep 17 00:00:00 2001 From: Markus Sintonen Date: Sun, 30 Jul 2023 21:07:52 +0200 Subject: [PATCH 5/5] Lazy field ctx errors, generic cow fields --- src/errors/types.rs | 38 +++++++++++++++++++++----------------- 1 file changed, 21 insertions(+), 17 deletions(-) diff --git a/src/errors/types.rs b/src/errors/types.rs index fb712459d..4986ac313 100644 --- a/src/errors/types.rs +++ b/src/errors/types.rs @@ -63,24 +63,28 @@ fn field_from_context<'py, T: FromPyObject<'py>>( context: Option<&'py PyDict>, field_name: &str, enum_name: &str, + type_name_fn: fn() -> &'static str, ) -> PyResult { context - .ok_or(py_error_type!(PyTypeError; "{}: '{}' required in context", enum_name, field_name))? + .ok_or_else(|| py_error_type!(PyTypeError; "{}: '{}' required in context", enum_name, field_name))? .get_item(field_name) - .ok_or(py_error_type!(PyTypeError; "{}: '{}' required in context", enum_name, field_name))? + .ok_or_else(|| py_error_type!(PyTypeError; "{}: '{}' required in context", enum_name, field_name))? .extract::() - .map_err(|_| { - let type_name = type_name::().split("::").last().unwrap(); - py_error_type!(PyTypeError; "{}: '{}' context value must be a {}", enum_name, field_name, type_name) - }) + .map_err(|_| py_error_type!(PyTypeError; "{}: '{}' context value must be a {}", enum_name, field_name, type_name_fn())) } -fn str_cow_field_from_context( - context: Option<&PyDict>, +fn cow_field_from_context<'py, T: FromPyObject<'py>, B: ?Sized + 'static>( + context: Option<&'py PyDict>, field_name: &str, enum_name: &str, -) -> PyResult> { - let res: String = field_from_context(context, field_name, enum_name)?; + _type_name_fn: fn() -> &'static str, +) -> PyResult> +where + B: ToOwned, +{ + let res: T = field_from_context(context, field_name, enum_name, || { + type_name::().split("::").last().unwrap() + })?; Ok(Cow::Owned(res)) } @@ -126,7 +130,7 @@ macro_rules! error_types { Ok(Self::$item { context: context.map(|c| c.into_py(py)), $( - $key: $ctx_fn(context, stringify!($key), stringify!($item))?, + $key: $ctx_fn(context, stringify!($key), stringify!($item), || stringify!($ctx_type))?, )* }) }, @@ -272,7 +276,7 @@ error_types! { // dict errors DictType {}, MappingType { - error: {ctx_type: Cow<'static, str>, ctx_fn: str_cow_field_from_context}, + error: {ctx_type: Cow<'static, str>, ctx_fn: cow_field_from_context}, }, // --------------------- // list errors @@ -329,7 +333,7 @@ error_types! { // date errors DateType {}, DateParsing { - error: {ctx_type: Cow<'static, str>, ctx_fn: str_cow_field_from_context}, + error: {ctx_type: Cow<'static, str>, ctx_fn: cow_field_from_context}, }, DateFromDatetimeParsing { error: {ctx_type: String, ctx_fn: field_from_context}, @@ -341,13 +345,13 @@ error_types! { // date errors TimeType {}, TimeParsing { - error: {ctx_type: Cow<'static, str>, ctx_fn: str_cow_field_from_context}, + error: {ctx_type: Cow<'static, str>, ctx_fn: cow_field_from_context}, }, // --------------------- // datetime errors DatetimeType {}, DatetimeParsing { - error: {ctx_type: Cow<'static, str>, ctx_fn: str_cow_field_from_context}, + error: {ctx_type: Cow<'static, str>, ctx_fn: cow_field_from_context}, }, DatetimeObjectInvalid { error: {ctx_type: String, ctx_fn: field_from_context}, @@ -366,7 +370,7 @@ error_types! { // timedelta errors TimeDeltaType {}, TimeDeltaParsing { - error: {ctx_type: Cow<'static, str>, ctx_fn: str_cow_field_from_context}, + error: {ctx_type: Cow<'static, str>, ctx_fn: cow_field_from_context}, }, // --------------------- // frozenset errors @@ -407,7 +411,7 @@ error_types! { error: {ctx_type: String, ctx_fn: field_from_context}, }, UrlSyntaxViolation { - error: {ctx_type: Cow<'static, str>, ctx_fn: str_cow_field_from_context}, + error: {ctx_type: Cow<'static, str>, ctx_fn: cow_field_from_context}, }, UrlTooLong { max_length: {ctx_type: usize, ctx_fn: field_from_context},