Skip to content
Merged
3 changes: 3 additions & 0 deletions .codecov.yml
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
codecov:
require_ci_to_pass: false

coverage:
precision: 2
range: [90, 100]
Expand Down
7 changes: 4 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -41,9 +41,10 @@ v = SchemaValidator({
},
'is_developer': {
'schema': {
'type': 'bool',
},
'default': True,
'type': 'default',
'schema': {'type': 'bool'},
'default': True,
}
},
},
})
Expand Down
16 changes: 11 additions & 5 deletions pydantic_core/schema_types.py
Original file line number Diff line number Diff line change
Expand Up @@ -124,9 +124,6 @@ class NewClassSchema(TypedDict):
class TypedDictField(TypedDict, total=False):
schema: Required[Schema]
required: bool
default: Any
default_factory: Callable[[], Any]
on_error: Literal['raise', 'omit', 'fallback_on_default'] # default: 'raise'
alias: Union[str, List[Union[str, int]], List[List[Union[str, int]]]]
frozen: bool

Expand Down Expand Up @@ -287,8 +284,6 @@ class Parameter(TypedDict, total=False):
name: Required[str]
mode: Literal['positional_only', 'positional_or_keyword', 'keyword_only'] # default positional_or_keyword
schema: Required[Schema]
default: Any
default_factory: Callable[[], Any]
alias: Union[str, List[Union[str, int]], List[List[Union[str, int]]]]


Expand All @@ -309,6 +304,16 @@ class CallSchema(TypedDict):
ref: NotRequired[str]


class WithDefaultSchema(TypedDict, total=False):
type: Required[Literal['default']]
schema: Required[Schema]
default: Any
default_factory: Callable[[], Any]
on_error: Literal['raise', 'omit', 'default'] # default: 'raise'
strict: bool
ref: str


# pydantic allows types to be defined via a simple string instead of dict with just `type`, e.g.
# 'int' is equivalent to {'type': 'int'}, this only applies to schema types which do not have other required fields
BareType = Literal[
Expand Down Expand Up @@ -366,4 +371,5 @@ class CallSchema(TypedDict):
CallableSchema,
ArgumentsSchema,
CallSchema,
WithDefaultSchema,
]
1 change: 1 addition & 0 deletions src/build_tools.rs
Original file line number Diff line number Diff line change
Expand Up @@ -138,6 +138,7 @@ impl SchemaError {
SchemaError::new_err(format!("Invalid Schema:\n{}", details))
}
ValError::InternalErr(py_err) => py_err,
ValError::Omit => unreachable!(),
}
}
}
Expand Down
3 changes: 2 additions & 1 deletion src/errors/line_error.rs
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ pub type ValResult<'a, T> = Result<T, ValError<'a>>;
pub enum ValError<'a> {
LineErrors(Vec<ValLineError<'a>>),
InternalErr(PyErr),
Omit,
}

impl<'a> From<PyErr> for ValError<'a> {
Expand Down Expand Up @@ -56,7 +57,7 @@ impl<'a> ValError<'a> {
}
Self::LineErrors(line_errors)
}
Self::InternalErr(err) => Self::InternalErr(err),
other => other,
}
}
}
Expand Down
3 changes: 3 additions & 0 deletions src/errors/validation_exception.rs
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,9 @@ impl ValidationError {
PyErr::new::<ValidationError, _>((line_errors, title))
}
ValError::InternalErr(err) => err,
ValError::Omit => {
PyValueError::new_err("Uncaught Omit error, please check your usage of `default` validators.")
}
}
}

Expand Down
1 change: 1 addition & 0 deletions src/input/return_enums.rs
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,7 @@ fn validate_iter_to_vec<'a, 's>(
Err(ValError::LineErrors(line_errors)) => {
errors.extend(line_errors.into_iter().map(|err| err.with_outer_location(index.into())));
}
Err(ValError::Omit) => (),
Err(err) => return Err(err),
}
}
Expand Down
48 changes: 22 additions & 26 deletions src/validators/arguments.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,8 @@ use crate::errors::{ErrorKind, ValError, ValLineError, ValResult};
use crate::input::{GenericArguments, Input};
use crate::lookup_key::LookupKey;
use crate::recursion_guard::RecursionGuard;
use crate::SchemaError;

use super::with_default::get_default;
use super::{build_validator, BuildContext, BuildValidator, CombinedValidator, Extra, Validator};

#[derive(Debug, Clone)]
Expand All @@ -18,8 +18,6 @@ struct Parameter {
name: String,
kw_lookup_key: Option<LookupKey>,
kwarg_key: Option<Py<PyString>>,
default: Option<PyObject>,
default_factory: Option<PyObject>,
validator: CombinedValidator,
}

Expand Down Expand Up @@ -74,28 +72,33 @@ impl BuildValidator for ArgumentsValidator {
kwarg_key = Some(PyString::intern(py, &name).into());
}

let schema: &PyAny = arg
.get_as_req(intern!(py, "schema"))
.map_err(|err| SchemaError::new_err(format!("Parameter \"{}\":\n {}", name, err)))?;
let schema: &PyAny = arg.get_as_req(intern!(py, "schema"))?;

let validator = build_validator(schema, config, build_context)?;
let validator = match build_validator(schema, config, build_context) {
Ok(v) => v,
Err(err) => return py_error!("Parameter '{}':\n {}", name, err),
};

let default = arg.get_as(intern!(py, "default"))?;
let default_factory = arg.get_as(intern!(py, "default_factory"))?;
if default.is_some() && default_factory.is_some() {
return py_error!("'default' and 'default_factory' cannot be used together");
} else if had_default_arg && (default.is_none() && default_factory.is_none()) {
return py_error!("Non-default argument follows default argument");
} else if default.is_some() || default_factory.is_some() {
let has_default = match validator {
CombinedValidator::WithDefault(ref v) => {
if v.omit_on_error() {
return py_error!("Parameter '{}': omit_on_error cannot be used with arguments", name);
}
v.has_default()
}
_ => false,
};

if had_default_arg && !has_default {
return py_error!("Non-default argument '{}' follows default argument", name);
} else if has_default {
had_default_arg = true;
}
parameters.push(Parameter {
positional,
kw_lookup_key,
name,
kwarg_key,
default,
default_factory,
validator,
});
}
Expand Down Expand Up @@ -213,18 +216,11 @@ impl Validator for ArgumentsValidator {
}
}
(None, None) => {
if let Some(ref default) = parameter.default {
if let Some(ref kwarg_key) = parameter.kwarg_key {
output_kwargs.set_item(kwarg_key, default)?;
} else {
output_args.push(default.clone_ref(py));
}
} else if let Some(ref default_factory) = parameter.default_factory {
let default = default_factory.call0(py)?;
if let Some(value) = get_default(py, &parameter.validator)? {
if let Some(ref kwarg_key) = parameter.kwarg_key {
output_kwargs.set_item(kwarg_key, default)?;
output_kwargs.set_item(kwarg_key, value.as_ref())?;
} else {
output_args.push(default);
output_args.push(value.as_ref().clone_ref(py));
}
} else if parameter.kwarg_key.is_some() {
errors.push(ValLineError::new_with_loc(
Expand Down
2 changes: 1 addition & 1 deletion src/validators/date.rs
Original file line number Diff line number Diff line change
Expand Up @@ -132,7 +132,7 @@ fn date_from_datetime<'data>(
}
Err(ValError::LineErrors(line_errors))
}
ValError::InternalErr(internal_err) => Err(ValError::InternalErr(internal_err)),
other => Err(other),
};
}
};
Expand Down
2 changes: 2 additions & 0 deletions src/validators/dict.rs
Original file line number Diff line number Diff line change
Expand Up @@ -140,6 +140,7 @@ macro_rules! build_validate {
}
None
}
Err(ValError::Omit) => continue,
Err(err) => return Err(err),
};
let output_value = match value_validator.validate(py, value, extra, slots, recursion_guard) {
Expand All @@ -150,6 +151,7 @@ macro_rules! build_validate {
}
None
}
Err(ValError::Omit) => continue,
Err(err) => return Err(err),
};
if let (Some(key), Some(value)) = (output_key, output_value) {
Expand Down
5 changes: 5 additions & 0 deletions src/validators/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ mod timedelta;
mod tuple;
mod typed_dict;
mod union;
mod with_default;

#[pyclass(module = "pydantic_core._pydantic_core")]
#[derive(Debug, Clone)]
Expand Down Expand Up @@ -369,6 +370,8 @@ pub fn build_validator<'a>(
callable::CallableValidator,
// arguments
arguments::ArgumentsValidator,
// default value
with_default::WithDefaultValidator,
)
}

Expand Down Expand Up @@ -478,6 +481,8 @@ pub enum CombinedValidator {
Callable(callable::CallableValidator),
// arguments
Arguments(arguments::ArgumentsValidator),
// default value
WithDefault(with_default::WithDefaultValidator),
}

/// This trait must be implemented by all validators, it allows various validators to be accessed consistently,
Expand Down
84 changes: 50 additions & 34 deletions src/validators/tuple.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ use crate::input::{GenericCollection, Input};
use crate::recursion_guard::RecursionGuard;

use super::list::generic_collection_build;
use super::with_default::get_default;
use super::{build_validator, BuildContext, BuildValidator, CombinedValidator, Extra, Validator};

#[derive(Debug)]
Expand Down Expand Up @@ -122,48 +123,63 @@ impl Validator for TuplePositionalValidator {
let collection = input.validate_tuple(extra.strict.unwrap_or(self.strict))?;
let expected_length = self.items_validators.len();

let col_length = collection.generic_len();
if col_length < expected_length {
return Err(ValError::LineErrors(
(col_length..expected_length)
.map(|index| ValLineError::new_with_loc(ErrorKind::Missing, input, index))
.collect(),
));
}
let mut output: Vec<PyObject> = Vec::with_capacity(expected_length);
let mut errors: Vec<ValLineError> = Vec::new();
macro_rules! iter {
($collection:expr) => {
for (index, item) in $collection.iter().enumerate() {
let validator = match self.items_validators.get(index) {
Some(ref v) => v,
None => match self.extra_validator {
Some(ref v) => v.as_ref(),
None => {
return Err(ValError::new(
ErrorKind::TooLong {
max_length: expected_length,
input_length: col_length,
},
input,
));
($collection:expr) => {{
let mut iter = $collection.iter();
for (index, validator) in self.items_validators.iter().enumerate() {
match iter.next() {
Some(item) => match validator.validate(py, item, extra, slots, recursion_guard) {
Ok(item) => output.push(item),
Err(ValError::LineErrors(line_errors)) => {
errors.extend(
line_errors
.into_iter()
.map(|err| err.with_outer_location(index.into())),
);
}
Err(err) => return Err(err),
},
};

match validator.validate(py, item, extra, slots, recursion_guard) {
Ok(item) => output.push(item),
Err(ValError::LineErrors(line_errors)) => {
errors.extend(
line_errors
.into_iter()
.map(|err| err.with_outer_location(index.into())),
);
None => {
if let Some(value) = get_default(py, &validator)? {
output.push(value.as_ref().clone_ref(py));
} else {
errors.push(ValLineError::new_with_loc(ErrorKind::Missing, input, index));
}
}
}
}
for (index, item) in iter.enumerate() {
match self.extra_validator {
Some(ref extra_validator) => {
match extra_validator.validate(py, item, extra, slots, recursion_guard) {
Ok(item) => output.push(item),
Err(ValError::LineErrors(line_errors)) => {
errors.extend(
line_errors
.into_iter()
.map(|err| err.with_outer_location((index + expected_length).into())),
);
}
Err(ValError::Omit) => (),
Err(err) => return Err(err),
}
}
None => {
errors.push(ValLineError::new(
ErrorKind::TooLong {
max_length: expected_length,
input_length: collection.generic_len(),
},
input,
));
// no need to continue through further items
break;
}
Err(err) => return Err(err),
}
}
};
}};
}
match collection {
GenericCollection::List(collection) => iter!(collection),
Expand Down
Loading