Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Allow ordering of constraints #825

Draft
wants to merge 3 commits into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
1 change: 1 addition & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,7 @@ features = ["pyo3/extension-module"]
[tool.ruff]
line-length = 120
extend-select = ['Q', 'RUF100', 'C90', 'I']
ignore = ['E501']
flake8-quotes = {inline-quotes = 'single', multiline-quotes = 'double'}
mccabe = { max-complexity = 13 }
isort = { known-first-party = ['pydantic_core', 'tests'] }
Expand Down
105 changes: 51 additions & 54 deletions src/errors/types.rs
Original file line number Diff line number Diff line change
Expand Up @@ -129,12 +129,10 @@ pub enum ErrorType {
// ---------------------
// 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,
},
TooLong {
field_type: String,
max_length: usize,
actual_length: usize,
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Removing this from the context may be a breaking change? Maybe can use the class name as the error message? This will also be nicer to see list or tuple instead of data.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I tried that and had some trouble. Happy to revert this part of the change once we've gotten most of this PR into shape.

},
Expand Down Expand Up @@ -414,14 +412,12 @@ impl ErrorType {
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
),
Expand Down Expand Up @@ -479,40 +475,44 @@ impl ErrorType {

pub fn message_template_python(&self) -> &'static str {
match self {
Self::NoSuchAttribute {..} => "Object has no attribute '{attribute}'",
Self::JsonInvalid {..} => "Invalid JSON: {error}",
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::GetAttributeError {..} => "Error extracting attribute: {error}",
Self::ModelType {..} => "Input should be a valid dictionary or instance of {class_name}",
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::DataclassType {..} => "Input should be a dictionary or an instance of {class_name}",
Self::DataclassExactType {..} => "Input should be an instance of {class_name}",
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::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::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::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::TooShort { .. } => {
"Data should have at least {min_length} item{expected_plural} after validation, not {actual_length}"
}
Self::TooLong { .. } => {
"Data should have at most {max_length} item{expected_plural} after validation, not {actual_length}"
}
Self::IterableType => "Input should be iterable",
Self::IterationError {..} => "Error iterating over object, error: {error}",
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::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::StringTooShort { .. } => "String should have at least {min_length} characters",
Self::StringTooLong { .. } => "String should have at most {max_length} characters",
Self::StringPatternMismatch { .. } => "String should match pattern '{pattern}'",
Self::Enum { .. } => "Input should be {expected}",
Self::DictType => "Input should be a valid dictionary",
Self::MappingType {..} => "Input should be a valid mapping, error: {error}",
Self::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",
Expand All @@ -525,36 +525,38 @@ impl ErrorType {
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::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::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::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::TimeParsing {..} => "Input should be in a valid time format, {error}",
Self::TimeParsing { .. } => "Input should be in a valid time format, {error}",
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::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::TimezoneOffset {..} => "Timezone offset of {tz_expected} required, got {tz_actual}",
Self::TimezoneOffset { .. } => "Timezone offset of {tz_expected} required, got {tz_actual}",
Self::TimeDeltaType => "Input should be a valid timedelta",
Self::TimeDeltaParsing {..} => "Input should be a valid timedelta, {error}",
Self::TimeDeltaParsing { .. } => "Input should be a valid timedelta, {error}",
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::IsInstanceOf { .. } => "Input should be an instance of {class}",
Self::IsSubclassOf { .. } => "Input should be a subclass of {class}",
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::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",
Expand All @@ -563,14 +565,13 @@ impl ErrorType {
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::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::UuidVersion { .. } => "UUID version {expected_version} expected",
}
}

Expand Down Expand Up @@ -632,20 +633,18 @@ impl ErrorType {
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)
to_string_render!(tmpl, min_length, actual_length, expected_plural)
}
Self::TooLong {
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)
to_string_render!(tmpl, max_length, actual_length, expected_plural)
}
Self::IterationError { error } => render!(tmpl, error),
Self::StringTooShort { min_length } => to_string_render!(tmpl, min_length),
Expand Down Expand Up @@ -710,15 +709,13 @@ impl ErrorType {
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),
} => py_dict!(py, min_length, actual_length),
Self::TooLong {
field_type,
max_length,
actual_length,
} => py_dict!(py, field_type, max_length, actual_length),
} => py_dict!(py, 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),
Expand Down
1 change: 1 addition & 0 deletions src/input/input_abstract.rs
Original file line number Diff line number Diff line change
Expand Up @@ -306,4 +306,5 @@ pub trait Input<'a>: fmt::Debug + ToPyObject {
) -> ValResult<EitherTimedelta> {
self.strict_timedelta(microseconds_overflow_behavior)
}
fn len(&self, py: Python<'_>) -> PyResult<usize>;
}
11 changes: 11 additions & 0 deletions src/input/input_json.rs
Original file line number Diff line number Diff line change
Expand Up @@ -334,6 +334,13 @@ impl<'a> Input<'a> for JsonInput {
_ => Err(ValError::new(ErrorType::TimeDeltaType, self)),
}
}

fn len(&self, py: Python<'_>) -> PyResult<usize> {
match self.len() {
Some(len) => Ok(len),
None => self.to_object(py).as_ref(py).len(),
}
}
}

/// Required for Dict keys so the string can behave like an Input
Expand Down Expand Up @@ -522,6 +529,10 @@ impl<'a> Input<'a> for String {
) -> ValResult<EitherTimedelta> {
self.validate_timedelta(false, microseconds_overflow_behavior)
}

fn len(&self, _py: Python<'_>) -> PyResult<usize> {
Ok(self.len())
}
}

fn string_to_vec(s: &str) -> JsonArray {
Expand Down
4 changes: 4 additions & 0 deletions src/input/input_python.rs
Original file line number Diff line number Diff line change
Expand Up @@ -646,6 +646,10 @@ impl<'a> Input<'a> for PyAny {
Err(ValError::new(ErrorType::TimeDeltaType, self))
}
}

fn len(&self, _py: Python) -> PyResult<usize> {
self.len()
}
}

/// Best effort check of whether it's likely to make sense to inspect obj for attributes and iterate over it
Expand Down
11 changes: 11 additions & 0 deletions src/input/parse_json.rs
Original file line number Diff line number Diff line change
Expand Up @@ -218,3 +218,14 @@ impl<'de> Visitor<'de> for KeyDeserializer {
unreachable!()
}
}

impl JsonInput {
pub fn len(&self) -> Option<usize> {
match self {
Self::Array(v) => Some(v.len()),
Self::Object(o) => Some(o.len()),
Self::String(s) => Some(s.len()),
_ => None,
}
}
}