39 changes: 8 additions & 31 deletions src/validators/arguments.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,16 +6,16 @@ use ahash::AHashSet;

use crate::build_tools::py_schema_err;
use crate::build_tools::schema_or_config_same;
use crate::errors::{ErrorTypeDefaults, ValError, ValLineError, ValResult};
use crate::input::{GenericArguments, Input};
use crate::errors::{AsLocItem, ErrorTypeDefaults, ValError, ValLineError, ValResult};
use crate::input::{GenericArguments, Input, ValidationMatch};
use crate::lookup_key::LookupKey;

use crate::tools::SchemaDict;

use super::validation_state::ValidationState;
use super::{build_validator, BuildValidator, CombinedValidator, DefinitionsBuilder, Validator};

#[derive(Debug, Clone)]
#[derive(Debug)]
struct Parameter {
positional: bool,
name: String,
Expand All @@ -24,7 +24,7 @@ struct Parameter {
validator: CombinedValidator,
}

#[derive(Debug, Clone)]
#[derive(Debug)]
pub struct ArgumentsValidator {
parameters: Vec<Parameter>,
positional_params_count: usize,
Expand Down Expand Up @@ -66,7 +66,7 @@ impl BuildValidator for ArgumentsValidator {
let mut kw_lookup_key = None;
let mut kwarg_key = None;
if mode == "keyword_only" || mode == "positional_or_keyword" {
kw_lookup_key = match arg.get_item(intern!(py, "alias")) {
kw_lookup_key = match arg.get_item(intern!(py, "alias"))? {
Some(alias) => {
let alt_alias = if populate_by_name { Some(name.as_str()) } else { None };
Some(LookupKey::from_py(py, alias, alt_alias)?)
Expand Down Expand Up @@ -110,11 +110,11 @@ impl BuildValidator for ArgumentsValidator {
Ok(Self {
parameters,
positional_params_count,
var_args_validator: match schema.get_item(intern!(py, "var_args_schema")) {
var_args_validator: match schema.get_item(intern!(py, "var_args_schema"))? {
Some(v) => Some(Box::new(build_validator(v, config, definitions)?)),
None => None,
},
var_kwargs_validator: match schema.get_item(intern!(py, "var_kwargs_schema")) {
var_kwargs_validator: match schema.get_item(intern!(py, "var_kwargs_schema"))? {
Some(v) => Some(Box::new(build_validator(v, config, definitions)?)),
None => None,
},
Expand Down Expand Up @@ -282,7 +282,7 @@ impl Validator for ArgumentsValidator {
if let Some(kwargs) = $args.kwargs {
if kwargs.len() > used_kwargs.len() {
for (raw_key, value) in kwargs.iter() {
let either_str = match raw_key.strict_str() {
let either_str = match raw_key.validate_str(true, false).map(ValidationMatch::into_inner) {
Ok(k) => k,
Err(ValError::LineErrors(line_errors)) => {
for err in line_errors {
Expand Down Expand Up @@ -332,30 +332,7 @@ impl Validator for ArgumentsValidator {
}
}

fn different_strict_behavior(
&self,
definitions: Option<&DefinitionsBuilder<CombinedValidator>>,
ultra_strict: bool,
) -> bool {
self.parameters
.iter()
.any(|p| p.validator.different_strict_behavior(definitions, ultra_strict))
}

fn get_name(&self) -> &str {
Self::EXPECTED_TYPE
}

fn complete(&mut self, definitions: &DefinitionsBuilder<CombinedValidator>) -> PyResult<()> {
self.parameters
.iter_mut()
.try_for_each(|parameter| parameter.validator.complete(definitions))?;
if let Some(v) = &mut self.var_args_validator {
v.complete(definitions)?;
}
if let Some(v) = &mut self.var_kwargs_validator {
v.complete(definitions)?;
};
Ok(())
}
}
17 changes: 3 additions & 14 deletions src/validators/bool.rs
Original file line number Diff line number Diff line change
Expand Up @@ -38,23 +38,12 @@ impl Validator for BoolValidator {
) -> ValResult<'data, PyObject> {
// TODO in theory this could be quicker if we used PyBool rather than going to a bool
// and back again, might be worth profiling?
let strict = state.strict_or(self.strict);
Ok(input.validate_bool(strict)?.into_py(py))
}

fn different_strict_behavior(
&self,
_definitions: Option<&DefinitionsBuilder<CombinedValidator>>,
ultra_strict: bool,
) -> bool {
!ultra_strict
input
.validate_bool(state.strict_or(self.strict))
.map(|val_match| val_match.unpack(state).into_py(py))
}

fn get_name(&self) -> &str {
Self::EXPECTED_TYPE
}

fn complete(&mut self, _definitions: &DefinitionsBuilder<CombinedValidator>) -> PyResult<()> {
Ok(())
}
}
36 changes: 6 additions & 30 deletions src/validators/bytes.rs
Original file line number Diff line number Diff line change
Expand Up @@ -24,8 +24,8 @@ impl BuildValidator for BytesValidator {
_definitions: &mut DefinitionsBuilder<CombinedValidator>,
) -> PyResult<CombinedValidator> {
let py = schema.py();
let use_constrained = schema.get_item(intern!(py, "max_length")).is_some()
|| schema.get_item(intern!(py, "min_length")).is_some();
let use_constrained = schema.get_item(intern!(py, "max_length"))?.is_some()
|| schema.get_item(intern!(py, "min_length"))?.is_some();
if use_constrained {
BytesConstrainedValidator::build(schema, config)
} else {
Expand All @@ -46,25 +46,14 @@ impl Validator for BytesValidator {
input: &'data impl Input<'data>,
state: &mut ValidationState,
) -> ValResult<'data, PyObject> {
let either_bytes = input.validate_bytes(state.strict_or(self.strict))?;
Ok(either_bytes.into_py(py))
}

fn different_strict_behavior(
&self,
_definitions: Option<&DefinitionsBuilder<CombinedValidator>>,
ultra_strict: bool,
) -> bool {
!ultra_strict
input
.validate_bytes(state.strict_or(self.strict))
.map(|m| m.unpack(state).into_py(py))
}

fn get_name(&self) -> &str {
Self::EXPECTED_TYPE
}

fn complete(&mut self, _definitions: &DefinitionsBuilder<CombinedValidator>) -> PyResult<()> {
Ok(())
}
}

#[derive(Debug, Clone)]
Expand All @@ -83,7 +72,7 @@ impl Validator for BytesConstrainedValidator {
input: &'data impl Input<'data>,
state: &mut ValidationState,
) -> ValResult<'data, PyObject> {
let either_bytes = input.validate_bytes(state.strict_or(self.strict))?;
let either_bytes = input.validate_bytes(state.strict_or(self.strict))?.unpack(state);
let len = either_bytes.len()?;

if let Some(min_length) = self.min_length {
Expand All @@ -108,25 +97,12 @@ impl Validator for BytesConstrainedValidator {
));
}
}

Ok(either_bytes.into_py(py))
}

fn different_strict_behavior(
&self,
_definitions: Option<&DefinitionsBuilder<CombinedValidator>>,
ultra_strict: bool,
) -> bool {
!ultra_strict
}

fn get_name(&self) -> &str {
"constrained-bytes"
}

fn complete(&mut self, _definitions: &DefinitionsBuilder<CombinedValidator>) -> PyResult<()> {
Ok(())
}
}

impl BytesConstrainedValidator {
Expand Down
26 changes: 2 additions & 24 deletions src/validators/call.rs
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ use crate::tools::SchemaDict;
use super::validation_state::ValidationState;
use super::{build_validator, BuildValidator, CombinedValidator, DefinitionsBuilder, Validator};

#[derive(Debug, Clone)]
#[derive(Debug)]
pub struct CallValidator {
function: PyObject,
arguments_validator: Box<CombinedValidator>,
Expand All @@ -32,7 +32,7 @@ impl BuildValidator for CallValidator {
let arguments_schema: &PyAny = schema.get_as_req(intern!(py, "arguments_schema"))?;
let arguments_validator = Box::new(build_validator(arguments_schema, config, definitions)?);

let return_schema = schema.get_item(intern!(py, "return_schema"));
let return_schema = schema.get_item(intern!(py, "return_schema"))?;
let return_validator = match return_schema {
Some(return_schema) => Some(Box::new(build_validator(return_schema, config, definitions)?)),
None => None,
Expand Down Expand Up @@ -98,29 +98,7 @@ impl Validator for CallValidator {
}
}

fn different_strict_behavior(
&self,
definitions: Option<&DefinitionsBuilder<CombinedValidator>>,
ultra_strict: bool,
) -> bool {
if let Some(return_validator) = &self.return_validator {
if return_validator.different_strict_behavior(definitions, ultra_strict) {
return true;
}
}
self.arguments_validator
.different_strict_behavior(definitions, ultra_strict)
}

fn get_name(&self) -> &str {
&self.name
}

fn complete(&mut self, definitions: &DefinitionsBuilder<CombinedValidator>) -> PyResult<()> {
self.arguments_validator.complete(definitions)?;
match &mut self.return_validator {
Some(v) => v.complete(definitions),
None => Ok(()),
}
}
}
16 changes: 3 additions & 13 deletions src/validators/callable.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ use pyo3::types::PyDict;
use crate::errors::{ErrorTypeDefaults, ValError, ValResult};
use crate::input::Input;

use super::validation_state::Exactness;
use super::{BuildValidator, CombinedValidator, DefinitionsBuilder, ValidationState, Validator};

#[derive(Debug, Clone)]
Expand All @@ -28,27 +29,16 @@ impl Validator for CallableValidator {
&self,
py: Python<'data>,
input: &'data impl Input<'data>,
_state: &mut ValidationState,
state: &mut ValidationState,
) -> ValResult<'data, PyObject> {
state.floor_exactness(Exactness::Lax);
match input.callable() {
true => Ok(input.to_object(py)),
false => Err(ValError::new(ErrorTypeDefaults::CallableType, input)),
}
}

fn different_strict_behavior(
&self,
_definitions: Option<&DefinitionsBuilder<CombinedValidator>>,
_ultra_strict: bool,
) -> bool {
false
}

fn get_name(&self) -> &str {
Self::EXPECTED_TYPE
}

fn complete(&mut self, _definitions: &DefinitionsBuilder<CombinedValidator>) -> PyResult<()> {
Ok(())
}
}
16 changes: 1 addition & 15 deletions src/validators/chain.rs
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ use crate::tools::SchemaDict;
use super::validation_state::ValidationState;
use super::{build_validator, BuildValidator, CombinedValidator, DefinitionsBuilder, Validator};

#[derive(Debug, Clone)]
#[derive(Debug)]
pub struct ChainValidator {
steps: Vec<CombinedValidator>,
name: String,
Expand Down Expand Up @@ -83,21 +83,7 @@ impl Validator for ChainValidator {
steps_iter.try_fold(value, |v, step| step.validate(py, v.into_ref(py), state))
}

fn different_strict_behavior(
&self,
definitions: Option<&DefinitionsBuilder<CombinedValidator>>,
ultra_strict: bool,
) -> bool {
self.steps
.iter()
.any(|v| v.different_strict_behavior(definitions, ultra_strict))
}

fn get_name(&self) -> &str {
&self.name
}

fn complete(&mut self, definitions: &DefinitionsBuilder<CombinedValidator>) -> PyResult<()> {
self.steps.iter_mut().try_for_each(|v| v.complete(definitions))
}
}
14 changes: 1 addition & 13 deletions src/validators/custom_error.rs
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,7 @@ impl CustomError {
}
}

#[derive(Debug, Clone)]
#[derive(Debug)]
pub struct CustomErrorValidator {
validator: Box<CombinedValidator>,
custom_error: CustomError,
Expand Down Expand Up @@ -99,19 +99,7 @@ impl Validator for CustomErrorValidator {
.map_err(|_| self.custom_error.as_val_error(input))
}

fn different_strict_behavior(
&self,
definitions: Option<&DefinitionsBuilder<CombinedValidator>>,
ultra_strict: bool,
) -> bool {
self.validator.different_strict_behavior(definitions, ultra_strict)
}

fn get_name(&self) -> &str {
&self.name
}

fn complete(&mut self, definitions: &DefinitionsBuilder<CombinedValidator>) -> PyResult<()> {
self.validator.complete(definitions)
}
}
90 changes: 36 additions & 54 deletions src/validators/dataclass.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,19 +7,20 @@ 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, ErrorTypeDefaults, ValError, ValLineError, ValResult};
use crate::input::{BorrowInput, GenericArguments, Input};
use crate::errors::{AsLocItem, ErrorType, ErrorTypeDefaults, ValError, ValLineError, ValResult};
use crate::input::{BorrowInput, GenericArguments, Input, ValidationMatch};
use crate::lookup_key::LookupKey;
use crate::tools::SchemaDict;
use crate::validators::function::convert_err;

use super::arguments::{json_get, json_slice, py_get, py_slice};
use super::model::{create_class, force_setattr, Revalidate};
use super::validation_state::Exactness;
use super::{
build_validator, BuildValidator, CombinedValidator, DefinitionsBuilder, Extra, ValidationState, Validator,
};

#[derive(Debug, Clone)]
#[derive(Debug)]
struct Field {
kw_only: bool,
name: String,
Expand All @@ -30,7 +31,7 @@ struct Field {
frozen: bool,
}

#[derive(Debug, Clone)]
#[derive(Debug)]
pub struct DataclassArgsValidator {
fields: Vec<Field>,
positional_count: usize,
Expand All @@ -56,7 +57,7 @@ impl BuildValidator for DataclassArgsValidator {

let extra_behavior = ExtraBehavior::from_schema_or_config(py, schema, config, ExtraBehavior::Ignore)?;

let extras_validator = match (schema.get_item(intern!(py, "extras_schema")), &extra_behavior) {
let extras_validator = match (schema.get_item(intern!(py, "extras_schema"))?, &extra_behavior) {
(Some(v), ExtraBehavior::Allow) => Some(Box::new(build_validator(v, config, definitions)?)),
(Some(_), _) => return py_schema_err!("extras_schema can only be used if extra_behavior=allow"),
(_, _) => None,
Expand All @@ -73,7 +74,7 @@ impl BuildValidator for DataclassArgsValidator {
let py_name: &PyString = field.get_as_req(intern!(py, "name"))?;
let name: String = py_name.extract()?;

let lookup_key = match field.get_item(intern!(py, "validation_alias")) {
let lookup_key = match field.get_item(intern!(py, "validation_alias"))? {
Some(alias) => {
let alt_alias = if populate_by_name { Some(name.as_str()) } else { None };
LookupKey::from_py(py, alias, alt_alias)?
Expand Down Expand Up @@ -232,19 +233,31 @@ impl Validator for DataclassArgsValidator {
}
// found neither, check if there is a default value, otherwise error
(None, None) => {
if let Some(value) =
field
.validator
.default_value(py, Some(field.name.as_str()), state)?
{
set_item!(field, value);
} else {
errors.push(field.lookup_key.error(
ErrorTypeDefaults::Missing,
input,
self.loc_by_alias,
&field.name,
));
match field.validator.default_value(py, Some(field.name.as_str()), state) {
Ok(Some(value)) => {
// Default value exists, and passed validation if required
set_item!(field, value);
},
Ok(None) => {
// This means there was no default value
errors.push(field.lookup_key.error(
ErrorTypeDefaults::Missing,
input,
self.loc_by_alias,
&field.name
));
},
Err(ValError::Omit) => continue,
Err(ValError::LineErrors(line_errors)) => {
for err in line_errors {
// Note: this will always use the field name even if there is an alias
// However, we don't mind so much because this error can only happen if the
// default value fails validation, which is arguably a developer error.
// We could try to "fix" this in the future if desired.
errors.push(err);
}
}
Err(err) => return Err(err),
}
}
}
Expand All @@ -269,7 +282,7 @@ impl Validator for DataclassArgsValidator {
if let Some(kwargs) = $args.kwargs {
if kwargs.len() != used_keys.len() {
for (raw_key, value) in kwargs.iter() {
match raw_key.strict_str() {
match raw_key.validate_str(true, false).map(ValidationMatch::into_inner) {
Ok(either_str) => {
if !used_keys.contains(either_str.as_cow()?.as_ref()) {
// Unknown / extra field
Expand Down Expand Up @@ -426,28 +439,12 @@ impl Validator for DataclassArgsValidator {
}
}

fn different_strict_behavior(
&self,
definitions: Option<&DefinitionsBuilder<CombinedValidator>>,
ultra_strict: bool,
) -> bool {
self.fields
.iter()
.any(|f| f.validator.different_strict_behavior(definitions, ultra_strict))
}

fn get_name(&self) -> &str {
&self.validator_name
}

fn complete(&mut self, definitions: &DefinitionsBuilder<CombinedValidator>) -> PyResult<()> {
self.fields
.iter_mut()
.try_for_each(|field| field.validator.complete(definitions))
}
}

#[derive(Debug, Clone)]
#[derive(Debug)]
pub struct DataclassValidator {
strict: bool,
validator: Box<CombinedValidator>,
Expand Down Expand Up @@ -548,6 +545,7 @@ impl Validator for DataclassValidator {
))
} else {
let val_output = self.validator.validate(py, input, state)?;
state.floor_exactness(Exactness::Strict);
let dc = create_class(self.class.as_ref(py))?;
self.set_dict_call(py, dc.as_ref(py), val_output, input)?;
Ok(dc)
Expand Down Expand Up @@ -578,7 +576,7 @@ impl Validator for DataclassValidator {

if self.slots {
let value = dc_dict
.get_item(field_name)
.get_item(field_name)?
.ok_or_else(|| PyKeyError::new_err(field_name.to_string()))?;
force_setattr(py, obj, field_name, value)?;
} else {
Expand All @@ -588,25 +586,9 @@ impl Validator for DataclassValidator {
Ok(obj.to_object(py))
}

fn different_strict_behavior(
&self,
definitions: Option<&DefinitionsBuilder<CombinedValidator>>,
ultra_strict: bool,
) -> bool {
if ultra_strict {
self.validator.different_strict_behavior(definitions, ultra_strict)
} else {
true
}
}

fn get_name(&self) -> &str {
&self.name
}

fn complete(&mut self, definitions: &DefinitionsBuilder<CombinedValidator>) -> PyResult<()> {
self.validator.complete(definitions)
}
}

impl DataclassValidator {
Expand Down
22 changes: 7 additions & 15 deletions src/validators/date.rs
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ use crate::input::{EitherDate, Input};
use crate::tools::SchemaDict;
use crate::validators::datetime::{NowConstraint, NowOp};

use super::Exactness;
use super::{BuildValidator, CombinedValidator, DefinitionsBuilder, ValidationState, Validator};

#[derive(Debug, Clone)]
Expand Down Expand Up @@ -46,9 +47,12 @@ impl Validator for DateValidator {
) -> ValResult<'data, PyObject> {
let strict = state.strict_or(self.strict);
let date = match input.validate_date(strict) {
Ok(date) => date,
Ok(val_match) => val_match.unpack(state),
// if the error was a parsing error, in lax mode we allow datetimes at midnight
Err(line_errors @ ValError::LineErrors(..)) if !strict => date_from_datetime(input)?.ok_or(line_errors)?,
Err(line_errors @ ValError::LineErrors(..)) if !strict => {
state.floor_exactness(Exactness::Lax);
date_from_datetime(input)?.ok_or(line_errors)?
}
Err(otherwise) => return Err(otherwise),
};
if let Some(constraints) = &self.constraints {
Expand Down Expand Up @@ -96,21 +100,9 @@ impl Validator for DateValidator {
Ok(date.try_into_py(py)?)
}

fn different_strict_behavior(
&self,
_definitions: Option<&DefinitionsBuilder<CombinedValidator>>,
ultra_strict: bool,
) -> bool {
!ultra_strict
}

fn get_name(&self) -> &str {
Self::EXPECTED_TYPE
}

fn complete(&mut self, _definitions: &DefinitionsBuilder<CombinedValidator>) -> PyResult<()> {
Ok(())
}
}

/// In lax mode, if the input is not a date, we try parsing the input as a datetime, then check it is an
Expand All @@ -119,7 +111,7 @@ impl Validator for DateValidator {
/// Ok(None) means that this is not relevant to dates (the input was not a datetime nor a string)
fn date_from_datetime<'data>(input: &'data impl Input<'data>) -> Result<Option<EitherDate<'data>>, ValError<'data>> {
let either_dt = match input.validate_datetime(false, speedate::MicrosecondsPrecisionOverflowBehavior::Truncate) {
Ok(dt) => dt,
Ok(val_match) => val_match.into_inner(),
// if the error was a parsing error, update the error type from DatetimeParsing to DateFromDatetimeParsing
// and return it
Err(ValError::LineErrors(mut line_errors)) => {
Expand Down
18 changes: 4 additions & 14 deletions src/validators/datetime.rs
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,9 @@ impl Validator for DateTimeValidator {
state: &mut ValidationState,
) -> ValResult<'data, PyObject> {
let strict = state.strict_or(self.strict);
let datetime = input.validate_datetime(strict, self.microseconds_precision)?;
let datetime = input
.validate_datetime(strict, self.microseconds_precision)?
.unpack(state);
if let Some(constraints) = &self.constraints {
// if we get an error from as_speedate, it's probably because the input datetime was invalid
// specifically had an invalid tzinfo, hence here we return a validation error
Expand Down Expand Up @@ -125,21 +127,9 @@ impl Validator for DateTimeValidator {
Ok(datetime.try_into_py(py)?)
}

fn different_strict_behavior(
&self,
_definitions: Option<&DefinitionsBuilder<CombinedValidator>>,
ultra_strict: bool,
) -> bool {
!ultra_strict
}

fn get_name(&self) -> &str {
Self::EXPECTED_TYPE
}

fn complete(&mut self, _definitions: &DefinitionsBuilder<CombinedValidator>) -> PyResult<()> {
Ok(())
}
}

#[derive(Debug, Clone)]
Expand Down Expand Up @@ -260,7 +250,7 @@ impl TZConstraint {

pub(super) fn from_py(schema: &PyDict) -> PyResult<Option<Self>> {
let py = schema.py();
let tz_constraint = match schema.get_item(intern!(py, "tz_constraint")) {
let tz_constraint = match schema.get_item(intern!(py, "tz_constraint"))? {
Some(c) => c,
None => return Ok(None),
};
Expand Down
162 changes: 92 additions & 70 deletions src/validators/decimal.rs
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
use pyo3::exceptions::{PyTypeError, PyValueError};
use pyo3::intern;
use pyo3::sync::GILOnceCell;
use pyo3::types::{IntoPyDict, PyDict, PyTuple, PyType};
use pyo3::{intern, AsPyPointer};
use pyo3::{prelude::*, PyTypeInfo};

use crate::build_tools::{is_strict, schema_or_config_same};
Expand Down Expand Up @@ -83,6 +83,41 @@ impl_py_gc_traverse!(DecimalValidator {
gt
});

fn extract_decimal_digits_info<'data>(
decimal: &PyAny,
normalized: bool,
py: Python<'data>,
) -> ValResult<'data, (u64, u64)> {
let mut normalized_decimal: Option<&PyAny> = None;
if normalized {
normalized_decimal = Some(decimal.call_method0(intern!(py, "normalize")).unwrap_or(decimal));
}
let (_, digit_tuple, exponent): (&PyAny, &PyTuple, &PyAny) = normalized_decimal
.unwrap_or(decimal)
.call_method0(intern!(py, "as_tuple"))?
.extract()?;

// finite values have numeric exponent, we checked is_finite above
let exponent: i64 = exponent.extract()?;
let mut digits: u64 = u64::try_from(digit_tuple.len()).map_err(|e| ValError::InternalErr(e.into()))?;
let decimals;
if exponent >= 0 {
// A positive exponent adds that many trailing zeros.
digits += exponent as u64;
decimals = 0;
} else {
// If the absolute value of the negative exponent is larger than the
// number of digits, then it's the same as the number of digits,
// because it'll consume all the digits in digit_tuple and then
// add abs(exponent) - len(digit_tuple) leading zeros after the
// decimal point.
decimals = exponent.unsigned_abs();
digits = digits.max(decimals);
}

Ok((decimals, digits))
}

impl Validator for DecimalValidator {
fn validate<'data>(
&self,
Expand All @@ -98,65 +133,53 @@ impl Validator for DecimalValidator {
}

if self.check_digits {
let normalized_value = decimal.call_method0(intern!(py, "normalize")).unwrap_or(decimal);
let (_, digit_tuple, exponent): (&PyAny, &PyTuple, &PyAny) =
normalized_value.call_method0(intern!(py, "as_tuple"))?.extract()?;
if let Ok((normalized_decimals, normalized_digits)) = extract_decimal_digits_info(decimal, true, py) {
if let Ok((decimals, digits)) = extract_decimal_digits_info(decimal, false, py) {
if let Some(max_digits) = self.max_digits {
if (digits > max_digits) & (normalized_digits > max_digits) {
return Err(ValError::new(
ErrorType::DecimalMaxDigits {
max_digits,
context: None,
},
input,
));
}
}

// finite values have numeric exponent, we checked is_finite above
let exponent: i64 = exponent.extract()?;
let mut digits: u64 = u64::try_from(digit_tuple.len()).map_err(|e| ValError::InternalErr(e.into()))?;
let decimals;
if exponent >= 0 {
// A positive exponent adds that many trailing zeros.
digits += exponent as u64;
decimals = 0;
} else {
// If the absolute value of the negative exponent is larger than the
// number of digits, then it's the same as the number of digits,
// because it'll consume all the digits in digit_tuple and then
// add abs(exponent) - len(digit_tuple) leading zeros after the
// decimal point.
decimals = exponent.unsigned_abs();
digits = digits.max(decimals);
}
if let Some(decimal_places) = self.decimal_places {
if (decimals > decimal_places) & (normalized_decimals > decimal_places) {
return Err(ValError::new(
ErrorType::DecimalMaxPlaces {
decimal_places,
context: None,
},
input,
));
}

if let Some(max_digits) = self.max_digits {
if digits > max_digits {
return Err(ValError::new(
ErrorType::DecimalMaxDigits {
max_digits,
context: None,
},
input,
));
}
}
if let Some(max_digits) = self.max_digits {
let whole_digits = digits.saturating_sub(decimals);
let max_whole_digits = max_digits.saturating_sub(decimal_places);

if let Some(decimal_places) = self.decimal_places {
if decimals > decimal_places {
return Err(ValError::new(
ErrorType::DecimalMaxPlaces {
decimal_places,
context: None,
},
input,
));
}
let normalized_whole_digits = normalized_digits.saturating_sub(normalized_decimals);
let normalized_max_whole_digits = max_digits.saturating_sub(decimal_places);

if let Some(max_digits) = self.max_digits {
let whole_digits = digits.saturating_sub(decimals);
let max_whole_digits = max_digits.saturating_sub(decimal_places);
if whole_digits > max_whole_digits {
return Err(ValError::new(
ErrorType::DecimalWholeDigits {
whole_digits: max_whole_digits,
context: None,
},
input,
));
if (whole_digits > max_whole_digits)
& (normalized_whole_digits > normalized_max_whole_digits)
{
return Err(ValError::new(
ErrorType::DecimalWholeDigits {
whole_digits: max_whole_digits,
context: None,
},
input,
));
}
}
}
}
}
};
}
}

Expand All @@ -182,8 +205,19 @@ impl Validator for DecimalValidator {
}
}

// Decimal raises DecimalOperation when comparing NaN, so if it's necessary to compare
// the value to a number, we need to check for NaN first. We cache the result on the first
// time we check it.
let mut is_nan: Option<bool> = None;
let mut is_nan = || -> PyResult<bool> {
match is_nan {
Some(is_nan) => Ok(is_nan),
None => Ok(*is_nan.insert(decimal.call_method0(intern!(py, "is_nan"))?.extract()?)),
}
};

if let Some(le) = &self.le {
if !decimal.le(le)? {
if is_nan()? || !decimal.le(le)? {
return Err(ValError::new(
ErrorType::LessThanEqual {
le: Number::String(le.to_string()),
Expand All @@ -194,7 +228,7 @@ impl Validator for DecimalValidator {
}
}
if let Some(lt) = &self.lt {
if !decimal.lt(lt)? {
if is_nan()? || !decimal.lt(lt)? {
return Err(ValError::new(
ErrorType::LessThan {
lt: Number::String(lt.to_string()),
Expand All @@ -205,7 +239,7 @@ impl Validator for DecimalValidator {
}
}
if let Some(ge) = &self.ge {
if !decimal.ge(ge)? {
if is_nan()? || !decimal.ge(ge)? {
return Err(ValError::new(
ErrorType::GreaterThanEqual {
ge: Number::String(ge.to_string()),
Expand All @@ -216,7 +250,7 @@ impl Validator for DecimalValidator {
}
}
if let Some(gt) = &self.gt {
if !decimal.gt(gt)? {
if is_nan()? || !decimal.gt(gt)? {
return Err(ValError::new(
ErrorType::GreaterThan {
gt: Number::String(gt.to_string()),
Expand All @@ -230,21 +264,9 @@ impl Validator for DecimalValidator {
Ok(decimal.into())
}

fn different_strict_behavior(
&self,
_definitions: Option<&DefinitionsBuilder<CombinedValidator>>,
_ultra_strict: bool,
) -> bool {
true
}

fn get_name(&self) -> &str {
Self::EXPECTED_TYPE
}

fn complete(&mut self, _definitions: &DefinitionsBuilder<CombinedValidator>) -> PyResult<()> {
Ok(())
}
}

pub(crate) fn create_decimal<'a>(
Expand Down
87 changes: 18 additions & 69 deletions src/validators/definitions.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ use pyo3::intern;
use pyo3::prelude::*;
use pyo3::types::{PyDict, PyList};

use crate::definitions::DefinitionRef;
use crate::errors::{ErrorTypeDefaults, ValError, ValResult};
use crate::input::Input;

Expand Down Expand Up @@ -39,17 +40,12 @@ impl BuildValidator for DefinitionsValidatorBuilder {

#[derive(Debug, Clone)]
pub struct DefinitionRefValidator {
validator_id: usize,
inner_name: String,
// we have to record the answers to `Question`s as we can't access the validator when `ask()` is called
definition: DefinitionRef<CombinedValidator>,
}

impl DefinitionRefValidator {
pub fn new(validator_id: usize) -> Self {
Self {
validator_id,
inner_name: "...".to_string(),
}
pub fn new(definition: DefinitionRef<CombinedValidator>) -> Self {
Self { definition }
}
}

Expand All @@ -61,15 +57,10 @@ impl BuildValidator for DefinitionRefValidator {
_config: Option<&PyDict>,
definitions: &mut DefinitionsBuilder<CombinedValidator>,
) -> PyResult<CombinedValidator> {
let schema_ref: String = schema.get_as_req(intern!(schema.py(), "schema_ref"))?;

let validator_id = definitions.get_reference_id(&schema_ref);
let schema_ref = schema.get_as_req(intern!(schema.py(), "schema_ref"))?;

Ok(Self {
validator_id,
inner_name: "...".to_string(),
}
.into())
let definition = definitions.get_definition(schema_ref);
Ok(Self::new(definition).into())
}
}

Expand All @@ -82,21 +73,22 @@ impl Validator for DefinitionRefValidator {
input: &'data impl Input<'data>,
state: &mut ValidationState,
) -> ValResult<'data, PyObject> {
let validator = self.definition.get().unwrap();
if let Some(id) = input.identity() {
if state.recursion_guard.contains_or_insert(id, self.validator_id) {
if state.recursion_guard.contains_or_insert(id, self.definition.id()) {
// we don't remove id here, we leave that to the validator which originally added id to `recursion_guard`
Err(ValError::new(ErrorTypeDefaults::RecursionLoop, input))
} else {
if state.recursion_guard.incr_depth() {
return Err(ValError::new(ErrorTypeDefaults::RecursionLoop, input));
}
let output = validate(self.validator_id, py, input, state);
state.recursion_guard.remove(id, self.validator_id);
let output = validator.validate(py, input, state);
state.recursion_guard.remove(id, self.definition.id());
state.recursion_guard.decr_depth();
output
}
} else {
validate(self.validator_id, py, input, state)
validator.validate(py, input, state)
}
}

Expand All @@ -108,69 +100,26 @@ impl Validator for DefinitionRefValidator {
field_value: &'data PyAny,
state: &mut ValidationState,
) -> ValResult<'data, PyObject> {
let validator = self.definition.get().unwrap();
if let Some(id) = obj.identity() {
if state.recursion_guard.contains_or_insert(id, self.validator_id) {
if state.recursion_guard.contains_or_insert(id, self.definition.id()) {
// we don't remove id here, we leave that to the validator which originally added id to `recursion_guard`
Err(ValError::new(ErrorTypeDefaults::RecursionLoop, obj))
} else {
if state.recursion_guard.incr_depth() {
return Err(ValError::new(ErrorTypeDefaults::RecursionLoop, obj));
}
let output = validate_assignment(self.validator_id, py, obj, field_name, field_value, state);
state.recursion_guard.remove(id, self.validator_id);
let output = validator.validate_assignment(py, obj, field_name, field_value, state);
state.recursion_guard.remove(id, self.definition.id());
state.recursion_guard.decr_depth();
output
}
} else {
validate_assignment(self.validator_id, py, obj, field_name, field_value, state)
}
}

fn different_strict_behavior(
&self,
definitions: Option<&DefinitionsBuilder<CombinedValidator>>,
ultra_strict: bool,
) -> bool {
if let Some(definitions) = definitions {
// have to unwrap here, because we can't return an error from this function, should be okay
let validator = definitions.get_definition(self.validator_id).unwrap();
validator.different_strict_behavior(None, ultra_strict)
} else {
false
validator.validate_assignment(py, obj, field_name, field_value, state)
}
}

fn get_name(&self) -> &str {
&self.inner_name
}

/// don't need to call complete on the inner validator here, complete_validators takes care of that.
fn complete(&mut self, definitions: &DefinitionsBuilder<CombinedValidator>) -> PyResult<()> {
let validator = definitions.get_definition(self.validator_id)?;
self.inner_name = validator.get_name().to_string();
Ok(())
self.definition.get_or_init_name(|v| v.get_name().into())
}
}

fn validate<'data>(
validator_id: usize,
py: Python<'data>,
input: &'data impl Input<'data>,
state: &mut ValidationState,
) -> ValResult<'data, PyObject> {
let validator = state.definitions.get(validator_id).unwrap();
validator.validate(py, input, state)
}

#[allow(clippy::too_many_arguments)]
fn validate_assignment<'data>(
validator_id: usize,
py: Python<'data>,
obj: &'data PyAny,
field_name: &'data str,
field_value: &'data PyAny,
state: &mut ValidationState,
) -> ValResult<'data, PyObject> {
let validator = state.definitions.get(validator_id).unwrap();
validator.validate_assignment(py, obj, field_name, field_value, state)
}
27 changes: 5 additions & 22 deletions src/validators/dict.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ use pyo3::prelude::*;
use pyo3::types::PyDict;

use crate::build_tools::is_strict;
use crate::errors::{ValError, ValLineError, ValResult};
use crate::errors::{AsLocItem, ValError, ValLineError, ValResult};
use crate::input::BorrowInput;
use crate::input::{
DictGenericIterator, GenericMapping, Input, JsonObjectGenericIterator, MappingGenericIterator,
Expand All @@ -16,7 +16,7 @@ use super::any::AnyValidator;
use super::list::length_check;
use super::{build_validator, BuildValidator, CombinedValidator, DefinitionsBuilder, ValidationState, Validator};

#[derive(Debug, Clone)]
#[derive(Debug)]
pub struct DictValidator {
strict: bool,
key_validator: Box<CombinedValidator>,
Expand All @@ -35,11 +35,11 @@ impl BuildValidator for DictValidator {
definitions: &mut DefinitionsBuilder<CombinedValidator>,
) -> PyResult<CombinedValidator> {
let py = schema.py();
let key_validator = match schema.get_item(intern!(py, "keys_schema")) {
let key_validator = match schema.get_item(intern!(py, "keys_schema"))? {
Some(schema) => Box::new(build_validator(schema, config, definitions)?),
None => Box::new(AnyValidator::build(schema, config, definitions)?),
};
let value_validator = match schema.get_item(intern!(py, "values_schema")) {
let value_validator = match schema.get_item(intern!(py, "values_schema"))? {
Some(d) => Box::new(build_validator(d, config, definitions)?),
None => Box::new(AnyValidator::build(schema, config, definitions)?),
};
Expand Down Expand Up @@ -80,6 +80,7 @@ impl Validator for DictValidator {
self.validate_generic_mapping(py, input, DictGenericIterator::new(py_dict)?, state)
}
GenericMapping::PyMapping(mapping) => {
state.floor_exactness(super::Exactness::Lax);
self.validate_generic_mapping(py, input, MappingGenericIterator::new(mapping)?, state)
}
GenericMapping::StringMapping(dict) => {
Expand All @@ -92,27 +93,9 @@ impl Validator for DictValidator {
}
}

fn different_strict_behavior(
&self,
definitions: Option<&DefinitionsBuilder<CombinedValidator>>,
ultra_strict: bool,
) -> bool {
if ultra_strict {
self.key_validator.different_strict_behavior(definitions, true)
|| self.value_validator.different_strict_behavior(definitions, true)
} else {
true
}
}

fn get_name(&self) -> &str {
&self.name
}

fn complete(&mut self, definitions: &DefinitionsBuilder<CombinedValidator>) -> PyResult<()> {
self.key_validator.complete(definitions)?;
self.value_validator.complete(definitions)
}
}

impl DictValidator {
Expand Down
50 changes: 13 additions & 37 deletions src/validators/float.rs
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
use std::cmp::Ordering;

use pyo3::intern;
use pyo3::prelude::*;
use pyo3::types::PyDict;
Expand All @@ -19,11 +21,11 @@ impl BuildValidator for FloatBuilder {
definitions: &mut DefinitionsBuilder<CombinedValidator>,
) -> PyResult<CombinedValidator> {
let py = schema.py();
let use_constrained = schema.get_item(intern!(py, "multiple_of")).is_some()
|| schema.get_item(intern!(py, "le")).is_some()
|| schema.get_item(intern!(py, "lt")).is_some()
|| schema.get_item(intern!(py, "ge")).is_some()
|| schema.get_item(intern!(py, "gt")).is_some();
let use_constrained = schema.get_item(intern!(py, "multiple_of"))?.is_some()
|| schema.get_item(intern!(py, "le"))?.is_some()
|| schema.get_item(intern!(py, "lt"))?.is_some()
|| schema.get_item(intern!(py, "ge"))?.is_some()
|| schema.get_item(intern!(py, "gt"))?.is_some();
if use_constrained {
ConstrainedFloatValidator::build(schema, config, definitions)
} else {
Expand Down Expand Up @@ -68,29 +70,16 @@ impl Validator for FloatValidator {
input: &'data impl Input<'data>,
state: &mut ValidationState,
) -> ValResult<'data, PyObject> {
let strict = state.strict_or(self.strict);
let either_float = input.validate_float(strict, state.extra().ultra_strict)?;
let either_float = input.validate_float(state.strict_or(self.strict))?.unpack(state);
if !self.allow_inf_nan && !either_float.as_f64().is_finite() {
return Err(ValError::new(ErrorTypeDefaults::FiniteNumber, input));
}
Ok(either_float.into_py(py))
}

fn different_strict_behavior(
&self,
_definitions: Option<&DefinitionsBuilder<CombinedValidator>>,
_ultra_strict: bool,
) -> bool {
true
}

fn get_name(&self) -> &str {
Self::EXPECTED_TYPE
}

fn complete(&mut self, _definitions: &DefinitionsBuilder<CombinedValidator>) -> PyResult<()> {
Ok(())
}
}

#[derive(Debug, Clone)]
Expand All @@ -113,8 +102,7 @@ impl Validator for ConstrainedFloatValidator {
input: &'data impl Input<'data>,
state: &mut ValidationState,
) -> ValResult<'data, PyObject> {
let strict = state.strict_or(self.strict);
let either_float = input.validate_float(strict, state.extra().ultra_strict)?;
let either_float = input.validate_float(state.strict_or(self.strict))?.unpack(state);
let float: f64 = either_float.as_f64();
if !self.allow_inf_nan && !float.is_finite() {
return Err(ValError::new(ErrorTypeDefaults::FiniteNumber, input));
Expand All @@ -133,7 +121,7 @@ impl Validator for ConstrainedFloatValidator {
}
}
if let Some(le) = self.le {
if float > le {
if !matches!(float.partial_cmp(&le), Some(Ordering::Less | Ordering::Equal)) {
return Err(ValError::new(
ErrorType::LessThanEqual {
le: le.into(),
Expand All @@ -144,7 +132,7 @@ impl Validator for ConstrainedFloatValidator {
}
}
if let Some(lt) = self.lt {
if float >= lt {
if !matches!(float.partial_cmp(&lt), Some(Ordering::Less)) {
return Err(ValError::new(
ErrorType::LessThan {
lt: lt.into(),
Expand All @@ -155,7 +143,7 @@ impl Validator for ConstrainedFloatValidator {
}
}
if let Some(ge) = self.ge {
if float < ge {
if !matches!(float.partial_cmp(&ge), Some(Ordering::Greater | Ordering::Equal)) {
return Err(ValError::new(
ErrorType::GreaterThanEqual {
ge: ge.into(),
Expand All @@ -166,7 +154,7 @@ impl Validator for ConstrainedFloatValidator {
}
}
if let Some(gt) = self.gt {
if float <= gt {
if !matches!(float.partial_cmp(&gt), Some(Ordering::Greater)) {
return Err(ValError::new(
ErrorType::GreaterThan {
gt: gt.into(),
Expand All @@ -179,21 +167,9 @@ impl Validator for ConstrainedFloatValidator {
Ok(either_float.into_py(py))
}

fn different_strict_behavior(
&self,
_definitions: Option<&DefinitionsBuilder<CombinedValidator>>,
_ultra_strict: bool,
) -> bool {
true
}

fn get_name(&self) -> &str {
"constrained-float"
}

fn complete(&mut self, _definitions: &DefinitionsBuilder<CombinedValidator>) -> PyResult<()> {
Ok(())
}
}

impl BuildValidator for ConstrainedFloatValidator {
Expand Down
27 changes: 9 additions & 18 deletions src/validators/frozenset.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,15 +2,16 @@ use pyo3::prelude::*;
use pyo3::types::{PyDict, PyFrozenSet};

use crate::errors::ValResult;
use crate::input::Input;
use crate::input::{GenericIterable, Input};
use crate::tools::SchemaDict;
use crate::validators::Exactness;

use super::list::min_length_check;
use super::set::set_build;
use super::validation_state::ValidationState;
use super::{BuildValidator, CombinedValidator, DefinitionsBuilder, Validator};

#[derive(Debug, Clone)]
#[derive(Debug)]
pub struct FrozenSetValidator {
strict: bool,
item_validator: Box<CombinedValidator>,
Expand All @@ -34,6 +35,12 @@ impl Validator for FrozenSetValidator {
state: &mut ValidationState,
) -> ValResult<'data, PyObject> {
let collection = input.validate_frozenset(state.strict_or(self.strict))?;
let exactness = match &collection {
GenericIterable::FrozenSet(_) => Exactness::Exact,
GenericIterable::Set(_) | GenericIterable::JsonArray(_) => Exactness::Strict,
_ => Exactness::Lax,
};
state.floor_exactness(exactness);
let f_set = PyFrozenSet::empty(py)?;
collection.validate_to_set(
py,
Expand All @@ -48,23 +55,7 @@ impl Validator for FrozenSetValidator {
Ok(f_set.into_py(py))
}

fn different_strict_behavior(
&self,
definitions: Option<&DefinitionsBuilder<CombinedValidator>>,
ultra_strict: bool,
) -> bool {
if ultra_strict {
self.item_validator.different_strict_behavior(definitions, true)
} else {
true
}
}

fn get_name(&self) -> &str {
&self.name
}

fn complete(&mut self, definitions: &DefinitionsBuilder<CombinedValidator>) -> PyResult<()> {
self.item_validator.complete(definitions)
}
}
98 changes: 20 additions & 78 deletions src/validators/function.rs
Original file line number Diff line number Diff line change
@@ -1,14 +1,16 @@
use pyo3::exceptions::{PyAssertionError, PyTypeError, PyValueError};
use std::sync::Arc;

use pyo3::exceptions::{PyAssertionError, PyValueError};
use pyo3::prelude::*;
use pyo3::types::{PyAny, PyDict, PyString};
use pyo3::{intern, PyTraverseError, PyVisit};

use crate::errors::{
ErrorType, LocItem, PydanticCustomError, PydanticKnownError, PydanticOmit, ValError, ValResult, ValidationError,
AsLocItem, ErrorType, PydanticCustomError, PydanticKnownError, PydanticOmit, ValError, ValResult, ValidationError,
};
use crate::input::Input;
use crate::py_gc::PyGcTraverse;
use crate::tools::{function_name, py_err, safe_repr, SchemaDict};
use crate::tools::{function_name, safe_repr, SchemaDict};
use crate::PydanticUseDefault;

use super::generator::InternalValidator;
Expand Down Expand Up @@ -111,31 +113,14 @@ macro_rules! impl_validator {
self._validate(validate, py, obj, state)
}

fn different_strict_behavior(
&self,
definitions: Option<&DefinitionsBuilder<CombinedValidator>>,
ultra_strict: bool,
) -> bool {
if ultra_strict {
self.validator
.different_strict_behavior(definitions, ultra_strict)
} else {
true
}
}

fn get_name(&self) -> &str {
&self.name
}

fn complete(&mut self, definitions: &DefinitionsBuilder<CombinedValidator>) -> PyResult<()> {
self.validator.complete(definitions)
}
}
};
}

#[derive(Debug, Clone)]
#[derive(Debug)]
pub struct FunctionBeforeValidator {
validator: Box<CombinedValidator>,
func: PyObject,
Expand Down Expand Up @@ -168,7 +153,7 @@ impl FunctionBeforeValidator {

impl_validator!(FunctionBeforeValidator);

#[derive(Debug, Clone)]
#[derive(Debug)]
pub struct FunctionAfterValidator {
validator: Box<CombinedValidator>,
func: PyObject,
Expand Down Expand Up @@ -255,27 +240,14 @@ impl Validator for FunctionPlainValidator {
r.map_err(|e| convert_err(py, e, input))
}

fn different_strict_behavior(
&self,
_definitions: Option<&DefinitionsBuilder<CombinedValidator>>,
ultra_strict: bool,
) -> bool {
// best guess, should we change this?
!ultra_strict
}

fn get_name(&self) -> &str {
&self.name
}

fn complete(&mut self, _definitions: &DefinitionsBuilder<CombinedValidator>) -> PyResult<()> {
Ok(())
}
}

#[derive(Debug, Clone)]
#[derive(Debug)]
pub struct FunctionWrapValidator {
validator: Box<CombinedValidator>,
validator: Arc<CombinedValidator>,
func: PyObject,
config: PyObject,
name: String,
Expand All @@ -299,7 +271,7 @@ impl BuildValidator for FunctionWrapValidator {
let hide_input_in_errors: bool = config.get_as(intern!(py, "hide_input_in_errors"))?.unwrap_or(false);
let validation_error_cause: bool = config.get_as(intern!(py, "validation_error_cause"))?.unwrap_or(false);
Ok(Self {
validator: Box::new(validator),
validator: Arc::new(validator),
func: function_info.function.clone(),
config: match config {
Some(c) => c.into(),
Expand Down Expand Up @@ -350,18 +322,16 @@ impl Validator for FunctionWrapValidator {
validator: InternalValidator::new(
py,
"ValidatorCallable",
&self.validator,
self.validator.clone(),
state,
self.hide_input_in_errors,
self.validation_error_cause,
),
};
self._validate(
Py::new(py, handler)?.into_ref(py),
py,
input.to_object(py).into_ref(py),
state,
)
let handler = Py::new(py, handler)?.into_ref(py);
let result = self._validate(handler, py, input.to_object(py).into_ref(py), state);
state.exactness = handler.borrow_mut().validator.exactness;
result
}

fn validate_assignment<'data>(
Expand All @@ -376,7 +346,7 @@ impl Validator for FunctionWrapValidator {
validator: InternalValidator::new(
py,
"AssignmentValidatorCallable",
&self.validator,
self.validator.clone(),
state,
self.hide_input_in_errors,
self.validation_error_cause,
Expand All @@ -387,43 +357,21 @@ impl Validator for FunctionWrapValidator {
self._validate(Py::new(py, handler)?.into_ref(py), py, obj, state)
}

fn different_strict_behavior(
&self,
definitions: Option<&DefinitionsBuilder<CombinedValidator>>,
ultra_strict: bool,
) -> bool {
if ultra_strict {
self.validator.different_strict_behavior(definitions, ultra_strict)
} else {
true
}
}

fn get_name(&self) -> &str {
&self.name
}

fn complete(&mut self, definitions: &DefinitionsBuilder<CombinedValidator>) -> PyResult<()> {
self.validator.complete(definitions)
}
}

#[pyclass(module = "pydantic_core._pydantic_core")]
#[derive(Debug, Clone)]
#[derive(Debug)]
struct ValidatorCallable {
validator: InternalValidator,
}

#[pymethods]
impl ValidatorCallable {
fn __call__(&mut self, py: Python, input_value: &PyAny, outer_location: Option<&PyAny>) -> PyResult<PyObject> {
let outer_location = match outer_location {
Some(ol) => match LocItem::try_from(ol) {
Ok(ol) => Some(ol),
Err(_) => return py_err!(PyTypeError; "outer_location must be a str or int"),
},
None => None,
};
let outer_location = outer_location.map(AsLocItem::as_loc_item);
self.validator.validate(py, input_value, outer_location)
}

Expand All @@ -441,7 +389,7 @@ impl ValidatorCallable {
}

#[pyclass(module = "pydantic_core._pydantic_core")]
#[derive(Debug, Clone)]
#[derive(Debug)]
struct AssignmentValidatorCallable {
updated_field_name: String,
updated_field_value: Py<PyAny>,
Expand All @@ -451,13 +399,7 @@ struct AssignmentValidatorCallable {
#[pymethods]
impl AssignmentValidatorCallable {
fn __call__(&mut self, py: Python, input_value: &PyAny, outer_location: Option<&PyAny>) -> PyResult<PyObject> {
let outer_location = match outer_location {
Some(ol) => match LocItem::try_from(ol) {
Ok(ol) => Some(ol),
Err(_) => return py_err!(PyTypeError; "outer_location must be a str or int"),
},
None => None,
};
let outer_location = outer_location.map(AsLocItem::as_loc_item);
self.validator.validate_assignment(
py,
input_value,
Expand Down
67 changes: 27 additions & 40 deletions src/validators/generator.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
use std::fmt;
use std::sync::Arc;

use pyo3::prelude::*;
use pyo3::types::PyDict;
Expand All @@ -10,11 +11,13 @@ use crate::tools::SchemaDict;
use crate::ValidationError;

use super::list::get_items_schema;
use super::{BuildValidator, CombinedValidator, DefinitionsBuilder, Extra, InputType, ValidationState, Validator};
use super::{
BuildValidator, CombinedValidator, DefinitionsBuilder, Exactness, Extra, InputType, ValidationState, Validator,
};

#[derive(Debug, Clone)]
pub struct GeneratorValidator {
item_validator: Option<Box<CombinedValidator>>,
item_validator: Option<Arc<CombinedValidator>>,
min_length: Option<usize>,
max_length: Option<usize>,
name: String,
Expand All @@ -30,7 +33,7 @@ impl BuildValidator for GeneratorValidator {
config: Option<&PyDict>,
definitions: &mut DefinitionsBuilder<CombinedValidator>,
) -> PyResult<CombinedValidator> {
let item_validator = get_items_schema(schema, config, definitions)?;
let item_validator = get_items_schema(schema, config, definitions)?.map(Arc::new);
let name = match item_validator {
Some(ref v) => format!("{}[{}]", Self::EXPECTED_TYPE, v.get_name()),
None => format!("{}[any]", Self::EXPECTED_TYPE),
Expand Down Expand Up @@ -67,7 +70,7 @@ impl Validator for GeneratorValidator {
InternalValidator::new(
py,
"ValidatorIterator",
v,
v.clone(),
state,
self.hide_input_in_errors,
self.validation_error_cause,
Expand All @@ -85,32 +88,13 @@ impl Validator for GeneratorValidator {
Ok(v_iterator.into_py(py))
}

fn different_strict_behavior(
&self,
definitions: Option<&DefinitionsBuilder<CombinedValidator>>,
ultra_strict: bool,
) -> bool {
if let Some(ref v) = self.item_validator {
v.different_strict_behavior(definitions, ultra_strict)
} else {
false
}
}

fn get_name(&self) -> &str {
&self.name
}

fn complete(&mut self, definitions: &DefinitionsBuilder<CombinedValidator>) -> PyResult<()> {
match self.item_validator {
Some(ref mut v) => v.complete(definitions),
None => Ok(()),
}
}
}

#[pyclass(module = "pydantic_core._pydantic_core")]
#[derive(Debug, Clone)]
#[derive(Debug)]
struct ValidatorIterator {
iterator: GenericIterator,
validator: Option<InternalValidator>,
Expand Down Expand Up @@ -217,20 +201,19 @@ impl ValidatorIterator {
}
}

/// Cloneable validator wrapper for use in generators in functions, this can be passed back to python
/// Owned validator wrapper for use in generators in functions, this can be passed back to python
/// mid-validation
#[derive(Clone)]
pub struct InternalValidator {
name: String,
validator: CombinedValidator,
definitions: Vec<CombinedValidator>,
validator: Arc<CombinedValidator>,
// TODO, do we need data?
data: Option<Py<PyDict>>,
strict: Option<bool>,
from_attributes: Option<bool>,
context: Option<PyObject>,
self_instance: Option<PyObject>,
recursion_guard: RecursionGuard,
pub(crate) exactness: Option<Exactness>,
validation_mode: InputType,
hide_input_in_errors: bool,
validation_error_cause: bool,
Expand All @@ -246,22 +229,22 @@ impl InternalValidator {
pub fn new(
py: Python,
name: &str,
validator: &CombinedValidator,
validator: Arc<CombinedValidator>,
state: &ValidationState,
hide_input_in_errors: bool,
validation_error_cause: bool,
) -> Self {
let extra = state.extra();
Self {
name: name.to_string(),
validator: validator.clone(),
definitions: state.definitions.to_vec(),
validator,
data: extra.data.map(|d| d.into_py(py)),
strict: extra.strict,
from_attributes: extra.from_attributes,
context: extra.context.map(|d| d.into_py(py)),
self_instance: extra.self_instance.map(|d| d.into_py(py)),
recursion_guard: state.recursion_guard.clone(),
exactness: state.exactness,
validation_mode: extra.input_type,
hide_input_in_errors,
validation_error_cause,
Expand All @@ -280,13 +263,14 @@ impl InternalValidator {
input_type: self.validation_mode,
data: self.data.as_ref().map(|data| data.as_ref(py)),
strict: self.strict,
ultra_strict: false,
from_attributes: self.from_attributes,
context: self.context.as_ref().map(|data| data.as_ref(py)),
self_instance: self.self_instance.as_ref().map(|data| data.as_ref(py)),
};
let mut state = ValidationState::new(extra, &self.definitions, &mut self.recursion_guard);
self.validator
let mut state = ValidationState::new(extra, &mut self.recursion_guard);
state.exactness = self.exactness;
let result = self
.validator
.validate_assignment(py, model, field_name, field_value, &mut state)
.map_err(|e| {
ValidationError::from_val_error(
Expand All @@ -298,7 +282,9 @@ impl InternalValidator {
self.hide_input_in_errors,
self.validation_error_cause,
)
})
});
self.exactness = state.exactness;
result
}

pub fn validate<'data>(
Expand All @@ -311,13 +297,13 @@ impl InternalValidator {
input_type: self.validation_mode,
data: self.data.as_ref().map(|data| data.as_ref(py)),
strict: self.strict,
ultra_strict: false,
from_attributes: self.from_attributes,
context: self.context.as_ref().map(|data| data.as_ref(py)),
self_instance: self.self_instance.as_ref().map(|data| data.as_ref(py)),
};
let mut state = ValidationState::new(extra, &self.definitions, &mut self.recursion_guard);
self.validator.validate(py, input, &mut state).map_err(|e| {
let mut state = ValidationState::new(extra, &mut self.recursion_guard);
state.exactness = self.exactness;
let result = self.validator.validate(py, input, &mut state).map_err(|e| {
ValidationError::from_val_error(
py,
self.name.to_object(py),
Expand All @@ -327,13 +313,14 @@ impl InternalValidator {
self.hide_input_in_errors,
self.validation_error_cause,
)
})
});
self.exactness = state.exactness;
result
}
}

impl_py_gc_traverse!(InternalValidator {
validator,
definitions,
data,
context,
self_instance
Expand Down
44 changes: 10 additions & 34 deletions src/validators/int.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,7 @@ use crate::errors::{ErrorType, ValError, ValResult};
use crate::input::{Input, Int};
use crate::tools::SchemaDict;

use super::ValidationState;
use super::{BuildValidator, CombinedValidator, DefinitionsBuilder, Validator};
use super::{BuildValidator, CombinedValidator, DefinitionsBuilder, ValidationState, Validator};

#[derive(Debug, Clone)]
pub struct IntValidator {
Expand All @@ -25,11 +24,11 @@ impl BuildValidator for IntValidator {
_definitions: &mut DefinitionsBuilder<CombinedValidator>,
) -> PyResult<CombinedValidator> {
let py = schema.py();
let use_constrained = schema.get_item(intern!(py, "multiple_of")).is_some()
|| schema.get_item(intern!(py, "le")).is_some()
|| schema.get_item(intern!(py, "lt")).is_some()
|| schema.get_item(intern!(py, "ge")).is_some()
|| schema.get_item(intern!(py, "gt")).is_some();
let use_constrained = schema.get_item(intern!(py, "multiple_of"))?.is_some()
|| schema.get_item(intern!(py, "le"))?.is_some()
|| schema.get_item(intern!(py, "lt"))?.is_some()
|| schema.get_item(intern!(py, "ge"))?.is_some()
|| schema.get_item(intern!(py, "gt"))?.is_some();
if use_constrained {
ConstrainedIntValidator::build(schema, config)
} else {
Expand All @@ -50,25 +49,14 @@ impl Validator for IntValidator {
input: &'data impl Input<'data>,
state: &mut ValidationState,
) -> ValResult<'data, PyObject> {
let either_int = input.validate_int(state.strict_or(self.strict))?;
Ok(either_int.into_py(py))
}

fn different_strict_behavior(
&self,
_definitions: Option<&DefinitionsBuilder<CombinedValidator>>,
ultra_strict: bool,
) -> bool {
!ultra_strict
input
.validate_int(state.strict_or(self.strict))
.map(|val_match| val_match.unpack(state).into_py(py))
}

fn get_name(&self) -> &str {
Self::EXPECTED_TYPE
}

fn complete(&mut self, _definitions: &DefinitionsBuilder<CombinedValidator>) -> PyResult<()> {
Ok(())
}
}

#[derive(Debug, Clone)]
Expand All @@ -90,7 +78,7 @@ impl Validator for ConstrainedIntValidator {
input: &'data impl Input<'data>,
state: &mut ValidationState,
) -> ValResult<'data, PyObject> {
let either_int = input.validate_int(state.strict_or(self.strict))?;
let either_int = input.validate_int(state.strict_or(self.strict))?.unpack(state);
let int_value = either_int.as_int()?;

if let Some(ref multiple_of) = self.multiple_of {
Expand Down Expand Up @@ -151,21 +139,9 @@ impl Validator for ConstrainedIntValidator {
Ok(either_int.into_py(py))
}

fn different_strict_behavior(
&self,
_definitions: Option<&DefinitionsBuilder<CombinedValidator>>,
ultra_strict: bool,
) -> bool {
!ultra_strict
}

fn get_name(&self) -> &str {
"constrained-int"
}

fn complete(&mut self, _definitions: &DefinitionsBuilder<CombinedValidator>) -> PyResult<()> {
Ok(())
}
}

impl ConstrainedIntValidator {
Expand Down
15 changes: 1 addition & 14 deletions src/validators/is_instance.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,7 @@ use crate::errors::{ErrorType, ValError, ValResult};
use crate::input::Input;
use crate::tools::SchemaDict;

use super::ValidationState;
use super::{BuildValidator, CombinedValidator, DefinitionsBuilder, Validator};
use super::{BuildValidator, CombinedValidator, DefinitionsBuilder, ValidationState, Validator};

#[derive(Debug, Clone)]
pub struct IsInstanceValidator {
Expand Down Expand Up @@ -83,19 +82,7 @@ impl Validator for IsInstanceValidator {
}
}

fn different_strict_behavior(
&self,
_definitions: Option<&DefinitionsBuilder<CombinedValidator>>,
_ultra_strict: bool,
) -> bool {
false
}

fn get_name(&self) -> &str {
&self.name
}

fn complete(&mut self, _definitions: &DefinitionsBuilder<CombinedValidator>) -> PyResult<()> {
Ok(())
}
}
15 changes: 1 addition & 14 deletions src/validators/is_subclass.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,7 @@ use crate::errors::{ErrorType, ValError, ValResult};
use crate::input::Input;
use crate::tools::SchemaDict;

use super::ValidationState;
use super::{BuildValidator, CombinedValidator, DefinitionsBuilder, Validator};
use super::{BuildValidator, CombinedValidator, DefinitionsBuilder, ValidationState, Validator};

#[derive(Debug, Clone)]
pub struct IsSubclassValidator {
Expand Down Expand Up @@ -62,19 +61,7 @@ impl Validator for IsSubclassValidator {
}
}

fn different_strict_behavior(
&self,
_definitions: Option<&DefinitionsBuilder<CombinedValidator>>,
_ultra_strict: bool,
) -> bool {
false
}

fn get_name(&self) -> &str {
&self.name
}

fn complete(&mut self, _definitions: &DefinitionsBuilder<CombinedValidator>) -> PyResult<()> {
Ok(())
}
}
24 changes: 2 additions & 22 deletions src/validators/json.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,9 @@ use crate::errors::ValResult;
use crate::input::Input;
use crate::tools::SchemaDict;

use super::ValidationState;
use super::{build_validator, BuildValidator, CombinedValidator, DefinitionsBuilder, Validator};
use super::{build_validator, BuildValidator, CombinedValidator, DefinitionsBuilder, ValidationState, Validator};

#[derive(Debug, Clone)]
#[derive(Debug)]
pub struct JsonValidator {
validator: Option<Box<CombinedValidator>>,
name: String,
Expand Down Expand Up @@ -61,26 +60,7 @@ impl Validator for JsonValidator {
}
}

fn different_strict_behavior(
&self,
definitions: Option<&DefinitionsBuilder<CombinedValidator>>,
ultra_strict: bool,
) -> bool {
if let Some(ref v) = self.validator {
v.different_strict_behavior(definitions, ultra_strict)
} else {
false
}
}

fn get_name(&self) -> &str {
&self.name
}

fn complete(&mut self, definitions: &DefinitionsBuilder<CombinedValidator>) -> PyResult<()> {
match self.validator {
Some(ref mut v) => v.complete(definitions),
None => Ok(()),
}
}
}
20 changes: 2 additions & 18 deletions src/validators/json_or_python.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,11 +7,9 @@ use crate::errors::ValResult;
use crate::input::Input;
use crate::tools::SchemaDict;

use super::InputType;
use super::ValidationState;
use super::{build_validator, BuildValidator, CombinedValidator, Validator};
use super::{build_validator, BuildValidator, CombinedValidator, InputType, ValidationState, Validator};

#[derive(Debug, Clone)]
#[derive(Debug)]
pub struct JsonOrPython {
json: Box<CombinedValidator>,
python: Box<CombinedValidator>,
Expand Down Expand Up @@ -63,21 +61,7 @@ impl Validator for JsonOrPython {
}
}

fn different_strict_behavior(
&self,
definitions: Option<&DefinitionsBuilder<CombinedValidator>>,
ultra_strict: bool,
) -> bool {
self.json.different_strict_behavior(definitions, ultra_strict)
|| self.python.different_strict_behavior(definitions, ultra_strict)
}

fn get_name(&self) -> &str {
&self.name
}

fn complete(&mut self, definitions: &DefinitionsBuilder<CombinedValidator>) -> PyResult<()> {
self.json.complete(definitions)?;
self.python.complete(definitions)
}
}
29 changes: 11 additions & 18 deletions src/validators/lax_or_strict.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,10 +7,11 @@ use crate::errors::ValResult;
use crate::input::Input;
use crate::tools::SchemaDict;

use super::Exactness;
use super::ValidationState;
use super::{build_validator, BuildValidator, CombinedValidator, DefinitionsBuilder, Validator};

#[derive(Debug, Clone)]
#[derive(Debug)]
pub struct LaxOrStrictValidator {
strict: bool,
lax_validator: Box<CombinedValidator>,
Expand Down Expand Up @@ -64,28 +65,20 @@ impl Validator for LaxOrStrictValidator {
if state.strict_or(self.strict) {
self.strict_validator.validate(py, input, state)
} else {
// horrible edge case: if doing smart union validation, we need to try the strict validator
// anyway and prefer that if it succeeds
if state.exactness.is_some() {
if let Ok(strict_result) = self.strict_validator.validate(py, input, state) {
return Ok(strict_result);
}
// this is now known to be not strict
state.floor_exactness(Exactness::Lax);
}
self.lax_validator.validate(py, input, state)
}
}

fn different_strict_behavior(
&self,
definitions: Option<&DefinitionsBuilder<CombinedValidator>>,
ultra_strict: bool,
) -> bool {
if ultra_strict {
self.strict_validator.different_strict_behavior(definitions, true)
} else {
true
}
}

fn get_name(&self) -> &str {
&self.name
}

fn complete(&mut self, definitions: &DefinitionsBuilder<CombinedValidator>) -> PyResult<()> {
self.lax_validator.complete(definitions)?;
self.strict_validator.complete(definitions)
}
}
64 changes: 31 additions & 33 deletions src/validators/list.rs
Original file line number Diff line number Diff line change
@@ -1,32 +1,35 @@
use std::sync::OnceLock;

use pyo3::prelude::*;
use pyo3::types::PyDict;

use crate::errors::ValResult;
use crate::input::{GenericIterable, Input};
use crate::tools::SchemaDict;
use crate::validators::Exactness;

use super::{build_validator, BuildValidator, CombinedValidator, DefinitionsBuilder, ValidationState, Validator};

#[derive(Debug, Clone)]
#[derive(Debug)]
pub struct ListValidator {
strict: bool,
item_validator: Option<Box<CombinedValidator>>,
min_length: Option<usize>,
max_length: Option<usize>,
name: String,
name: OnceLock<String>,
}

pub fn get_items_schema(
schema: &PyDict,
config: Option<&PyDict>,
definitions: &mut DefinitionsBuilder<CombinedValidator>,
) -> PyResult<Option<Box<CombinedValidator>>> {
match schema.get_item(pyo3::intern!(schema.py(), "items_schema")) {
) -> PyResult<Option<CombinedValidator>> {
match schema.get_item(pyo3::intern!(schema.py(), "items_schema"))? {
Some(d) => {
let validator = build_validator(d, config, definitions)?;
match validator {
CombinedValidator::Any(_) => Ok(None),
_ => Ok(Some(Box::new(validator))),
_ => Ok(Some(validator)),
}
}
None => Ok(None),
Expand Down Expand Up @@ -98,15 +101,13 @@ impl BuildValidator for ListValidator {
definitions: &mut DefinitionsBuilder<CombinedValidator>,
) -> PyResult<CombinedValidator> {
let py = schema.py();
let item_validator = get_items_schema(schema, config, definitions)?;
let inner_name = item_validator.as_ref().map_or("any", |v| v.get_name());
let name = format!("{}[{inner_name}]", Self::EXPECTED_TYPE);
let item_validator = get_items_schema(schema, config, definitions)?.map(Box::new);
Ok(Self {
strict: crate::build_tools::is_strict(schema, config)?,
item_validator,
min_length: schema.get_as(pyo3::intern!(py, "min_length"))?,
max_length: schema.get_as(pyo3::intern!(py, "max_length"))?,
name,
name: OnceLock::new(),
}
.into())
}
Expand All @@ -122,6 +123,12 @@ impl Validator for ListValidator {
state: &mut ValidationState,
) -> ValResult<'data, PyObject> {
let seq = input.validate_list(state.strict_or(self.strict))?;
let exactness = match &seq {
GenericIterable::List(_) | GenericIterable::JsonArray(_) => Exactness::Exact,
GenericIterable::Tuple(_) => Exactness::Strict,
_ => Exactness::Lax,
};
state.floor_exactness(exactness);

let output = match self.item_validator {
Some(ref v) => seq.validate_to_vec(py, input, self.max_length, "List", v, state)?,
Expand All @@ -138,31 +145,22 @@ impl Validator for ListValidator {
Ok(output.into_py(py))
}

fn different_strict_behavior(
&self,
definitions: Option<&DefinitionsBuilder<CombinedValidator>>,
ultra_strict: bool,
) -> bool {
if ultra_strict {
match self.item_validator {
Some(ref v) => v.different_strict_behavior(definitions, true),
None => false,
}
} else {
true
}
}

fn get_name(&self) -> &str {
&self.name
}

fn complete(&mut self, definitions: &DefinitionsBuilder<CombinedValidator>) -> PyResult<()> {
if let Some(ref mut v) = self.item_validator {
v.complete(definitions)?;
let inner_name = v.get_name();
self.name = format!("{}[{inner_name}]", Self::EXPECTED_TYPE);
// The logic here is a little janky, it's done to try to cache the formatted name
// while also trying to render definitions correctly when possible.
//
// Probably an opportunity for a future refactor
match self.name.get() {
Some(s) => s.as_str(),
None => {
let name = self.item_validator.as_ref().map_or("any", |v| v.get_name());
if name == "..." {
// when inner name is not initialized yet, don't cache it here
"list[...]"
} else {
self.name.get_or_init(|| format!("list[{name}]")).as_str()
}
}
}
Ok(())
}
}
28 changes: 8 additions & 20 deletions src/validators/literal.rs
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ struct BoolLiteral {
}

#[derive(Debug, Clone)]
pub struct LiteralLookup<T: Clone + Debug> {
pub struct LiteralLookup<T: Debug> {
// Specialized lookups for ints, bools and strings because they
// (1) are easy to convert between Rust and Python
// (2) hashing them in Rust is very fast
Expand All @@ -35,7 +35,7 @@ pub struct LiteralLookup<T: Clone + Debug> {
pub values: Vec<T>,
}

impl<T: Clone + Debug> LiteralLookup<T> {
impl<T: Debug> LiteralLookup<T> {
pub fn new<'py>(py: Python<'py>, expected: impl Iterator<Item = (&'py PyAny, T)>) -> PyResult<Self> {
let mut expected_int = AHashMap::new();
let mut expected_str: AHashMap<String, usize> = AHashMap::new();
Expand All @@ -48,8 +48,8 @@ impl<T: Clone + Debug> LiteralLookup<T> {
for (k, v) in expected {
let id = values.len();
values.push(v);
if let Ok(bool) = k.strict_bool() {
if bool {
if let Ok(bool) = k.validate_bool(true) {
if bool.into_inner() {
expected_bool.true_id = Some(id);
} else {
expected_bool.false_id = Some(id);
Expand Down Expand Up @@ -97,8 +97,8 @@ impl<T: Clone + Debug> LiteralLookup<T> {
input: &'data I,
) -> ValResult<'data, Option<(&'data I, &T)>> {
if let Some(expected_bool) = &self.expected_bool {
if let Ok(bool_value) = input.strict_bool() {
if bool_value {
if let Ok(bool_value) = input.validate_bool(true) {
if bool_value.into_inner() {
if let Some(true_value) = &expected_bool.true_id {
return Ok(Some((input, &self.values[*true_value])));
}
Expand Down Expand Up @@ -126,7 +126,7 @@ impl<T: Clone + Debug> LiteralLookup<T> {
}
// must be an enum or bytes
if let Some(expected_py) = &self.expected_py {
if let Some(v) = expected_py.as_ref(py).get_item(input) {
if let Some(v) = expected_py.as_ref(py).get_item(input)? {
let id: usize = v.extract().unwrap();
return Ok(Some((input, &self.values[id])));
}
Expand All @@ -135,7 +135,7 @@ impl<T: Clone + Debug> LiteralLookup<T> {
}
}

impl<T: PyGcTraverse + Clone + Debug> PyGcTraverse for LiteralLookup<T> {
impl<T: PyGcTraverse + Debug> PyGcTraverse for LiteralLookup<T> {
fn py_gc_traverse(&self, visit: &PyVisit<'_>) -> Result<(), PyTraverseError> {
self.expected_py.py_gc_traverse(visit)?;
self.values.py_gc_traverse(visit)?;
Expand Down Expand Up @@ -198,21 +198,9 @@ impl Validator for LiteralValidator {
}
}

fn different_strict_behavior(
&self,
_definitions: Option<&DefinitionsBuilder<CombinedValidator>>,
ultra_strict: bool,
) -> bool {
!ultra_strict
}

fn get_name(&self) -> &str {
&self.name
}

fn complete(&mut self, _definitions: &DefinitionsBuilder<CombinedValidator>) -> PyResult<()> {
Ok(())
}
}

pub fn expected_repr_name(mut repr_args: Vec<String>, base_name: &'static str) -> (String, String) {
Expand Down
91 changes: 37 additions & 54 deletions src/validators/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ use pyo3::types::{PyAny, PyDict, PyTuple, PyType};
use pyo3::{intern, PyTraverseError, PyVisit};

use crate::build_tools::{py_schema_err, py_schema_error_type, SchemaError};
use crate::definitions::DefinitionsBuilder;
use crate::definitions::{Definitions, DefinitionsBuilder};
use crate::errors::{LocItem, ValError, ValResult, ValidationError};
use crate::input::{Input, InputType, StringMapping};
use crate::py_gc::PyGcTraverse;
Expand Down Expand Up @@ -58,10 +58,9 @@ mod uuid;
mod validation_state;
mod with_default;

pub use self::validation_state::{Exactness, ValidationState};
pub use with_default::DefaultType;

pub use self::validation_state::ValidationState;

#[pyclass(module = "pydantic_core._pydantic_core", name = "Some")]
pub struct PySome {
#[pyo3(get)]
Expand Down Expand Up @@ -97,12 +96,15 @@ impl PySome {
}
}

#[pyclass(module = "pydantic_core._pydantic_core")]
#[derive(Debug, Clone)]
#[pyclass(module = "pydantic_core._pydantic_core", frozen)]
#[derive(Debug)]
pub struct SchemaValidator {
validator: CombinedValidator,
definitions: Vec<CombinedValidator>,
schema: PyObject,
definitions: Definitions<CombinedValidator>,
// References to the Python schema and config objects are saved to enable
// reconstructing the object for cloudpickle support (see `__reduce__`).
py_schema: Py<PyAny>,
py_config: Option<Py<PyDict>>,
#[pyo3(get)]
title: PyObject,
hide_input_in_errors: bool,
Expand All @@ -115,14 +117,15 @@ impl SchemaValidator {
pub fn py_new(py: Python, schema: &PyAny, config: Option<&PyDict>) -> PyResult<Self> {
let mut definitions_builder = DefinitionsBuilder::new();

let mut validator = build_validator(schema, config, &mut definitions_builder)?;
validator.complete(&definitions_builder)?;
let mut definitions = definitions_builder.clone().finish()?;
for val in &mut definitions {
val.complete(&definitions_builder)?;
}
let validator = build_validator(schema, config, &mut definitions_builder)?;
let definitions = definitions_builder.finish()?;
let py_schema = schema.into_py(py);
let py_config = match config {
Some(c) if !c.is_empty() => Some(c.into_py(py)),
_ => None,
};
let config_title = match config {
Some(c) => c.get_item("title"),
Some(c) => c.get_item("title")?,
None => None,
};
let title = match config_title {
Expand All @@ -134,17 +137,20 @@ impl SchemaValidator {
Ok(Self {
validator,
definitions,
schema: schema.into_py(py),
py_schema,
py_config,
title,
hide_input_in_errors,
validation_error_cause,
})
}

pub fn __reduce__(&self, py: Python) -> PyResult<PyObject> {
let args = (self.schema.as_ref(py),);
let cls = Py::new(py, self.clone())?.getattr(py, "__class__")?;
Ok((cls, args).into_py(py))
pub fn __reduce__(slf: &PyCell<Self>) -> PyResult<(PyObject, (PyObject, PyObject))> {
// Enables support for `pickle` serialization.
let py = slf.py();
let cls = slf.get_type().into();
let init_args = (slf.get().py_schema.to_object(py), slf.get().py_config.to_object(py));
Ok((cls, init_args))
}

#[pyo3(signature = (input, *, strict=None, from_attributes=None, context=None, self_instance=None))]
Expand Down Expand Up @@ -260,13 +266,12 @@ impl SchemaValidator {
data: None,
strict,
from_attributes,
ultra_strict: false,
context,
self_instance: None,
};

let guard = &mut RecursionGuard::default();
let mut state = ValidationState::new(extra, &self.definitions, guard);
let mut state = ValidationState::new(extra, guard);
self.validator
.validate_assignment(py, obj, field_name, field_value, &mut state)
.map_err(|e| self.prepare_validation_err(py, e, InputType::Python))
Expand All @@ -279,12 +284,11 @@ impl SchemaValidator {
data: None,
strict,
from_attributes: None,
ultra_strict: false,
context,
self_instance: None,
};
let recursion_guard = &mut RecursionGuard::default();
let mut state = ValidationState::new(extra, &self.definitions, recursion_guard);
let mut state = ValidationState::new(extra, recursion_guard);
let r = self.validator.default_value(py, None::<i64>, &mut state);
match r {
Ok(maybe_default) => match maybe_default {
Expand All @@ -306,9 +310,9 @@ impl SchemaValidator {

fn __traverse__(&self, visit: PyVisit<'_>) -> Result<(), PyTraverseError> {
self.validator.py_gc_traverse(&visit)?;
visit.call(&self.schema)?;
for slot in &self.definitions {
slot.py_gc_traverse(&visit)?;
visit.call(&self.py_schema)?;
if let Some(ref py_config) = self.py_config {
visit.call(py_config)?;
}
Ok(())
}
Expand All @@ -332,7 +336,6 @@ impl SchemaValidator {
{
let mut state = ValidationState::new(
Extra::new(strict, from_attributes, context, self_instance, input_type),
&self.definitions,
recursion_guard,
);
self.validator.validate(py, input, &mut state)
Expand Down Expand Up @@ -371,7 +374,6 @@ impl<'py> SelfValidator<'py> {
let mut recursion_guard = RecursionGuard::default();
let mut state = ValidationState::new(
Extra::new(strict, None, None, None, InputType::Python),
&self.validator.definitions,
&mut recursion_guard,
);
match self.validator.validator.validate(py, schema, &mut state) {
Expand All @@ -388,19 +390,16 @@ impl<'py> SelfValidator<'py> {

let mut definitions_builder = DefinitionsBuilder::new();

let mut validator = match build_validator(self_schema, None, &mut definitions_builder) {
let validator = match build_validator(self_schema, None, &mut definitions_builder) {
Ok(v) => v,
Err(err) => return py_schema_err!("Error building self-schema:\n {}", err),
};
validator.complete(&definitions_builder)?;
let mut definitions = definitions_builder.clone().finish()?;
for val in &mut definitions {
val.complete(&definitions_builder)?;
}
let definitions = definitions_builder.finish()?;
Ok(SchemaValidator {
validator,
definitions,
schema: py.None(),
py_schema: py.None(),
py_config: None,
title: "Self Schema".into_py(py),
hide_input_in_errors: false,
validation_error_cause: false,
Expand Down Expand Up @@ -559,8 +558,6 @@ pub struct Extra<'a> {
pub data: Option<&'a PyDict>,
/// whether we're in strict or lax mode
pub strict: Option<bool>,
/// whether we're in ultra-strict mode, only used occasionally in unions
pub ultra_strict: bool,
/// Validation time setting of `from_attributes`
pub from_attributes: Option<bool>,
/// context used in validator functions
Expand All @@ -581,7 +578,6 @@ impl<'a> Extra<'a> {
input_type,
data: None,
strict,
ultra_strict: false,
from_attributes,
context,
self_instance,
Expand All @@ -590,20 +586,19 @@ impl<'a> Extra<'a> {
}

impl<'a> Extra<'a> {
pub fn as_strict(&self, ultra_strict: bool) -> Self {
pub fn as_strict(&self) -> Self {
Self {
input_type: self.input_type,
data: self.data,
strict: Some(true),
ultra_strict,
from_attributes: self.from_attributes,
context: self.context,
self_instance: self.self_instance,
}
}
}

#[derive(Debug, Clone)]
#[derive(Debug)]
#[enum_dispatch(PyGcTraverse)]
pub enum CombinedValidator {
// typed dict e.g. heterogeneous dicts or simply a model
Expand Down Expand Up @@ -699,7 +694,7 @@ pub enum CombinedValidator {
/// This trait must be implemented by all validators, it allows various validators to be accessed consistently,
/// validators defined in `build_validator` also need `EXPECTED_TYPE` as a const, but that can't be part of the trait
#[enum_dispatch(CombinedValidator)]
pub trait Validator: Send + Sync + Clone + Debug {
pub trait Validator: Send + Sync + Debug {
/// Do the actual validation for this schema/type
fn validate<'data>(
&self,
Expand Down Expand Up @@ -732,19 +727,7 @@ pub trait Validator: Send + Sync + Clone + Debug {
Err(py_err.into())
}

/// whether the validator behaves differently in strict mode, and in ultra strict mode
/// implementations should return true if any of their sub-validators return true
fn different_strict_behavior(
&self,
definitions: Option<&DefinitionsBuilder<CombinedValidator>>,
ultra_strict: bool,
) -> bool;

/// `get_name` generally returns `Self::EXPECTED_TYPE` or some other clear identifier of the validator
/// this is used in the error location in unions, and in the top level message in `ValidationError`
fn get_name(&self) -> &str;

/// this method must be implemented for any validator which holds references to other validators,
/// it is used by `DefinitionRefValidator` to set its name
fn complete(&mut self, _definitions: &DefinitionsBuilder<CombinedValidator>) -> PyResult<()>;
}
21 changes: 4 additions & 17 deletions src/validators/model.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ use pyo3::types::{PyDict, PySet, PyString, PyTuple, PyType};
use pyo3::{ffi, intern};

use super::function::convert_err;
use super::validation_state::Exactness;
use super::{
build_validator, BuildValidator, CombinedValidator, DefinitionsBuilder, Extra, ValidationState, Validator,
};
Expand Down Expand Up @@ -50,7 +51,7 @@ impl Revalidate {
}
}

#[derive(Debug, Clone)]
#[derive(Debug)]
pub struct ModelValidator {
revalidate: Revalidate,
validator: Box<CombinedValidator>,
Expand Down Expand Up @@ -143,6 +144,8 @@ impl Validator for ModelValidator {
Ok(input.to_object(py))
}
} else {
// Having to construct a new model is not an exact match
state.floor_exactness(Exactness::Strict);
self.validate_construct(py, input, None, state)
}
}
Expand Down Expand Up @@ -206,25 +209,9 @@ impl Validator for ModelValidator {
Ok(model.into_py(py))
}

fn different_strict_behavior(
&self,
definitions: Option<&DefinitionsBuilder<CombinedValidator>>,
ultra_strict: bool,
) -> bool {
if ultra_strict {
self.validator.different_strict_behavior(definitions, ultra_strict)
} else {
true
}
}

fn get_name(&self) -> &str {
&self.name
}

fn complete(&mut self, definitions: &DefinitionsBuilder<CombinedValidator>) -> PyResult<()> {
self.validator.complete(definitions)
}
}

impl ModelValidator {
Expand Down
75 changes: 37 additions & 38 deletions src/validators/model_fields.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,20 +7,21 @@ 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, ErrorTypeDefaults, ValError, ValLineError, ValResult};
use crate::errors::{AsLocItem, ErrorType, ErrorTypeDefaults, ValError, ValLineError, ValResult};
use crate::input::{
AttributesGenericIterator, BorrowInput, DictGenericIterator, GenericMapping, Input, JsonObjectGenericIterator,
MappingGenericIterator, StringMappingGenericIterator,
MappingGenericIterator, StringMappingGenericIterator, ValidationMatch,
};
use crate::lookup_key::LookupKey;
use crate::tools::SchemaDict;

use super::ValidationState;
use super::{build_validator, BuildValidator, CombinedValidator, DefinitionsBuilder, Extra, Validator};
use super::{
build_validator, BuildValidator, CombinedValidator, DefinitionsBuilder, Extra, ValidationState, Validator,
};

use std::ops::ControlFlow;

#[derive(Debug, Clone)]
#[derive(Debug)]
struct Field {
name: String,
lookup_key: LookupKey,
Expand All @@ -31,7 +32,7 @@ struct Field {

impl_py_gc_traverse!(Field { validator });

#[derive(Debug, Clone)]
#[derive(Debug)]
pub struct ModelFieldsValidator {
fields: Vec<Field>,
model_name: String,
Expand All @@ -58,7 +59,7 @@ impl BuildValidator for ModelFieldsValidator {

let extra_behavior = ExtraBehavior::from_schema_or_config(py, schema, config, ExtraBehavior::Ignore)?;

let extras_validator = match (schema.get_item(intern!(py, "extras_schema")), &extra_behavior) {
let extras_validator = match (schema.get_item(intern!(py, "extras_schema"))?, &extra_behavior) {
(Some(v), ExtraBehavior::Allow) => Some(Box::new(build_validator(v, config, definitions)?)),
(Some(_), _) => return py_schema_err!("extras_schema can only be used if extra_behavior=allow"),
(_, _) => None,
Expand All @@ -81,7 +82,7 @@ impl BuildValidator for ModelFieldsValidator {
Err(err) => return py_schema_err!("Field \"{}\":\n {}", field_name, err),
};

let lookup_key = match field_info.get_item(intern!(py, "validation_alias")) {
let lookup_key = match field_info.get_item(intern!(py, "validation_alias"))? {
Some(alias) => {
let alt_alias = if populate_by_name { Some(field_name) } else { None };
LookupKey::from_py(py, alias, alt_alias)?
Expand Down Expand Up @@ -211,15 +212,33 @@ impl Validator for ModelFieldsValidator {
Err(err) => return ControlFlow::Break(err.into_owned(py)),
}
continue;
} else if let Some(value) = control_flow!(field.validator.default_value(py, Some(field.name.as_str()), state))? {
control_flow!(model_dict.set_item(&field.name_py, value))?;
} else {
errors.push(field.lookup_key.error(
ErrorTypeDefaults::Missing,
input,
self.loc_by_alias,
&field.name
));
}

match field.validator.default_value(py, Some(field.name.as_str()), state) {
Ok(Some(value)) => {
// Default value exists, and passed validation if required
control_flow!(model_dict.set_item(&field.name_py, value))?;
},
Ok(None) => {
// This means there was no default value
errors.push(field.lookup_key.error(
ErrorTypeDefaults::Missing,
input,
self.loc_by_alias,
&field.name
));
},
Err(ValError::Omit) => continue,
Err(ValError::LineErrors(line_errors)) => {
for err in line_errors {
// Note: this will always use the field name even if there is an alias
// However, we don't mind so much because this error can only happen if the
// default value fails validation, which is arguably a developer error.
// We could try to "fix" this in the future if desired.
errors.push(err);
}
}
Err(err) => return ControlFlow::Break(err),
}
}
ControlFlow::Continue(())
Expand All @@ -232,7 +251,7 @@ impl Validator for ModelFieldsValidator {
let model_extra_dict = PyDict::new(py);
for item_result in <$iter>::new($dict)? {
let (raw_key, value) = item_result?;
let either_str = match raw_key.strict_str() {
let either_str = match raw_key.validate_str(true, false).map(ValidationMatch::into_inner) {
Ok(k) => k,
Err(ValError::LineErrors(line_errors)) => {
for err in line_errors {
Expand Down Expand Up @@ -415,27 +434,7 @@ impl Validator for ModelFieldsValidator {
Ok((new_data.to_object(py), new_extra, fields_set.to_object(py)).to_object(py))
}

fn different_strict_behavior(
&self,
definitions: Option<&DefinitionsBuilder<CombinedValidator>>,
ultra_strict: bool,
) -> bool {
self.fields
.iter()
.any(|f| f.validator.different_strict_behavior(definitions, ultra_strict))
}

fn get_name(&self) -> &str {
Self::EXPECTED_TYPE
}

fn complete(&mut self, definitions: &DefinitionsBuilder<CombinedValidator>) -> PyResult<()> {
self.fields
.iter_mut()
.try_for_each(|f| f.validator.complete(definitions))?;
match &mut self.extras_validator {
Some(v) => v.complete(definitions),
None => Ok(()),
}
}
}
12 changes: 0 additions & 12 deletions src/validators/none.rs
Original file line number Diff line number Diff line change
Expand Up @@ -36,19 +36,7 @@ impl Validator for NoneValidator {
}
}

fn different_strict_behavior(
&self,
_definitions: Option<&DefinitionsBuilder<CombinedValidator>>,
_ultra_strict: bool,
) -> bool {
false
}

fn get_name(&self) -> &str {
Self::EXPECTED_TYPE
}

fn complete(&mut self, _definitions: &DefinitionsBuilder<CombinedValidator>) -> PyResult<()> {
Ok(())
}
}
14 changes: 1 addition & 13 deletions src/validators/nullable.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ use crate::tools::SchemaDict;
use super::ValidationState;
use super::{build_validator, BuildValidator, CombinedValidator, DefinitionsBuilder, Validator};

#[derive(Debug, Clone)]
#[derive(Debug)]
pub struct NullableValidator {
validator: Box<CombinedValidator>,
name: String,
Expand Down Expand Up @@ -45,19 +45,7 @@ impl Validator for NullableValidator {
}
}

fn different_strict_behavior(
&self,
definitions: Option<&DefinitionsBuilder<CombinedValidator>>,
ultra_strict: bool,
) -> bool {
self.validator.different_strict_behavior(definitions, ultra_strict)
}

fn get_name(&self) -> &str {
&self.name
}

fn complete(&mut self, definitions: &DefinitionsBuilder<CombinedValidator>) -> PyResult<()> {
self.validator.complete(definitions)
}
}
29 changes: 10 additions & 19 deletions src/validators/set.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,14 @@ use pyo3::prelude::*;
use pyo3::types::{PyDict, PySet};

use crate::errors::ValResult;
use crate::input::Input;
use crate::input::{GenericIterable, Input};
use crate::tools::SchemaDict;
use crate::validators::Exactness;

use super::list::min_length_check;
use super::{BuildValidator, CombinedValidator, DefinitionsBuilder, ValidationState, Validator};

#[derive(Debug, Clone)]
#[derive(Debug)]
pub struct SetValidator {
strict: bool,
item_validator: Box<CombinedValidator>,
Expand All @@ -25,7 +26,7 @@ macro_rules! set_build {
definitions: &mut DefinitionsBuilder<CombinedValidator>,
) -> PyResult<CombinedValidator> {
let py = schema.py();
let item_validator = match schema.get_item(pyo3::intern!(schema.py(), "items_schema")) {
let item_validator = match schema.get_item(pyo3::intern!(schema.py(), "items_schema"))? {
Some(d) => Box::new(crate::validators::build_validator(d, config, definitions)?),
None => Box::new(crate::validators::any::AnyValidator::build(
schema,
Expand Down Expand Up @@ -64,29 +65,19 @@ impl Validator for SetValidator {
state: &mut ValidationState,
) -> ValResult<'data, PyObject> {
let collection = input.validate_set(state.strict_or(self.strict))?;
let exactness = match &collection {
GenericIterable::Set(_) => Exactness::Exact,
GenericIterable::FrozenSet(_) | GenericIterable::JsonArray(_) => Exactness::Strict,
_ => Exactness::Lax,
};
state.floor_exactness(exactness);
let set = PySet::empty(py)?;
collection.validate_to_set(py, set, input, self.max_length, "Set", &self.item_validator, state)?;
min_length_check!(input, "Set", self.min_length, set);
Ok(set.into_py(py))
}

fn different_strict_behavior(
&self,
definitions: Option<&DefinitionsBuilder<CombinedValidator>>,
ultra_strict: bool,
) -> bool {
if ultra_strict {
self.item_validator.different_strict_behavior(definitions, true)
} else {
true
}
}

fn get_name(&self) -> &str {
&self.name
}

fn complete(&mut self, definitions: &DefinitionsBuilder<CombinedValidator>) -> PyResult<()> {
self.item_validator.complete(definitions)
}
}
Loading