Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 10 additions & 4 deletions src/validators/date.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
use pyo3::exceptions::PyValueError;
use pyo3::intern;
use pyo3::prelude::*;
use pyo3::types::{PyDate, PyDict, PyString};
use pyo3::types::{PyDict, PyString};
use speedate::{Date, Time};
use strum::EnumMessage;

Expand Down Expand Up @@ -175,9 +176,14 @@ impl DateConstraints {
}
}

fn convert_pydate(schema: &Bound<'_, PyDict>, field: &Bound<'_, PyString>) -> PyResult<Option<Date>> {
match schema.get_as::<Bound<'_, PyDate>>(field)? {
Some(date) => Ok(Some(EitherDate::Py(date).as_raw()?)),
fn convert_pydate(schema: &Bound<'_, PyDict>, key: &Bound<'_, PyString>) -> PyResult<Option<Date>> {
match schema.get_as::<Bound<'_, PyAny>>(key)? {
Some(value) => match value.validate_date(false) {
Ok(v) => Ok(Some(v.into_inner().as_raw()?)),
Err(_) => Err(PyValueError::new_err(format!(
"'{key}' must be coercible to a date instance",
))),
},
None => Ok(None),
}
}
14 changes: 10 additions & 4 deletions src/validators/datetime.rs
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
use pyo3::exceptions::PyValueError;
use pyo3::intern;
use pyo3::prelude::*;
use pyo3::sync::GILOnceCell;
use pyo3::types::{PyDict, PyString};
use speedate::{DateTime, Time};
use speedate::{DateTime, MicrosecondsPrecisionOverflowBehavior, Time};
use std::cmp::Ordering;
use strum::EnumMessage;

Expand Down Expand Up @@ -208,9 +209,14 @@ impl DateTimeConstraints {
}
}

fn py_datetime_as_datetime(schema: &Bound<'_, PyDict>, field: &Bound<'_, PyString>) -> PyResult<Option<DateTime>> {
match schema.get_as(field)? {
Some(dt) => Ok(Some(EitherDateTime::Py(dt).as_raw()?)),
fn py_datetime_as_datetime(schema: &Bound<'_, PyDict>, key: &Bound<'_, PyString>) -> PyResult<Option<DateTime>> {
match schema.get_as::<Bound<'_, PyAny>>(key)? {
Some(value) => match value.validate_datetime(false, MicrosecondsPrecisionOverflowBehavior::Truncate) {
Ok(v) => Ok(Some(v.into_inner().as_raw()?)),
Err(_) => Err(PyValueError::new_err(format!(
"'{key}' must be coercible to a datetime instance",
))),
},
None => Ok(None),
}
}
Expand Down
26 changes: 20 additions & 6 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::types::{IntoPyDict, PyDict, PyString, PyTuple, PyType};
use pyo3::{prelude::*, PyTypeInfo};

use crate::build_tools::{is_strict, schema_or_config_same};
Expand All @@ -28,6 +28,18 @@ pub fn get_decimal_type(py: Python) -> &Bound<'_, PyType> {
.bind(py)
}

fn validate_as_decimal(py: Python, schema: &Bound<'_, PyDict>, key: &str) -> PyResult<Option<Py<PyAny>>> {
match schema.get_as::<Bound<'_, PyAny>>(&PyString::new(py, key))? {
Some(value) => match value.validate_decimal(false, py) {
Ok(v) => Ok(Some(v.into_inner().unbind())),
Err(_) => Err(PyValueError::new_err(format!(
"'{key}' must be coercible to a Decimal instance",
))),
},
None => Ok(None),
}
}

#[derive(Debug, Clone)]
pub struct DecimalValidator {
strict: bool,
Expand All @@ -50,6 +62,7 @@ impl BuildValidator for DecimalValidator {
_definitions: &mut DefinitionsBuilder<CombinedValidator>,
) -> PyResult<CombinedValidator> {
let py = schema.py();

let allow_inf_nan = schema_or_config_same(schema, config, intern!(py, "allow_inf_nan"))?.unwrap_or(false);
let decimal_places = schema.get_as(intern!(py, "decimal_places"))?;
let max_digits = schema.get_as(intern!(py, "max_digits"))?;
Expand All @@ -58,16 +71,17 @@ impl BuildValidator for DecimalValidator {
"allow_inf_nan=True cannot be used with max_digits or decimal_places",
));
}

Ok(Self {
strict: is_strict(schema, config)?,
allow_inf_nan,
check_digits: decimal_places.is_some() || max_digits.is_some(),
decimal_places,
multiple_of: schema.get_as(intern!(py, "multiple_of"))?,
le: schema.get_as(intern!(py, "le"))?,
lt: schema.get_as(intern!(py, "lt"))?,
ge: schema.get_as(intern!(py, "ge"))?,
gt: schema.get_as(intern!(py, "gt"))?,
multiple_of: validate_as_decimal(py, schema, "multiple_of")?,
le: validate_as_decimal(py, schema, "le")?,
lt: validate_as_decimal(py, schema, "lt")?,
ge: validate_as_decimal(py, schema, "ge")?,
gt: validate_as_decimal(py, schema, "gt")?,
max_digits,
}
.into())
Expand Down
50 changes: 34 additions & 16 deletions src/validators/int.rs
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
use num_bigint::BigInt;
use pyo3::exceptions::PyValueError;
use pyo3::intern;
use pyo3::prelude::*;
use pyo3::types::PyDict;
use pyo3::types::{PyDict, PyString};
use pyo3::IntoPyObjectExt;

use crate::build_tools::is_strict;
Expand All @@ -11,6 +12,23 @@ use crate::tools::SchemaDict;

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

fn validate_as_int(py: Python, schema: &Bound<'_, PyDict>, key: &str) -> PyResult<Option<Int>> {
match schema.get_as::<Bound<'_, PyAny>>(&PyString::new(py, key))? {
Some(value) => match value.validate_int(false) {
Ok(v) => match v.into_inner().as_int() {
Ok(v) => Ok(Some(v)),
Err(_) => Err(PyValueError::new_err(format!(
"'{key}' must be coercible to an integer"
))),
},
Err(_) => Err(PyValueError::new_err(format!(
"'{key}' must be coercible to an integer"
))),
},
None => Ok(None),
}
}

#[derive(Debug, Clone)]
pub struct IntValidator {
strict: bool,
Expand Down Expand Up @@ -70,6 +88,21 @@ pub struct ConstrainedIntValidator {
gt: Option<Int>,
}

impl ConstrainedIntValidator {
fn build(schema: &Bound<'_, PyDict>, config: Option<&Bound<'_, PyDict>>) -> PyResult<CombinedValidator> {
let py = schema.py();
Ok(Self {
strict: is_strict(schema, config)?,
multiple_of: validate_as_int(py, schema, "multiple_of")?,
le: validate_as_int(py, schema, "le")?,
lt: validate_as_int(py, schema, "lt")?,
ge: validate_as_int(py, schema, "ge")?,
gt: validate_as_int(py, schema, "gt")?,
}
.into())
}
}

impl_py_gc_traverse!(ConstrainedIntValidator {});

impl Validator for ConstrainedIntValidator {
Expand Down Expand Up @@ -144,18 +177,3 @@ impl Validator for ConstrainedIntValidator {
"constrained-int"
}
}

impl ConstrainedIntValidator {
fn build(schema: &Bound<'_, PyDict>, config: Option<&Bound<'_, PyDict>>) -> PyResult<CombinedValidator> {
let py = schema.py();
Ok(Self {
strict: is_strict(schema, config)?,
multiple_of: schema.get_as(intern!(py, "multiple_of"))?,
le: schema.get_as(intern!(py, "le"))?,
lt: schema.get_as(intern!(py, "lt"))?,
ge: schema.get_as(intern!(py, "ge"))?,
gt: schema.get_as(intern!(py, "gt"))?,
}
.into())
}
}
9 changes: 9 additions & 0 deletions tests/validators/test_date.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,15 @@
from ..conftest import Err, PyAndJson


@pytest.mark.parametrize(
'constraint',
['le', 'lt', 'ge', 'gt'],
)
def test_constraints_schema_validation(constraint: str) -> None:
with pytest.raises(SchemaError, match=f"'{constraint}' must be coercible to a date instance"):
SchemaValidator(cs.date_schema(**{constraint: 'bad_value'}))


@pytest.mark.parametrize(
'input_value,expected',
[
Expand Down
9 changes: 9 additions & 0 deletions tests/validators/test_datetime.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,15 @@
from ..conftest import Err, PyAndJson


@pytest.mark.parametrize(
'constraint',
['le', 'lt', 'ge', 'gt'],
)
def test_constraints_schema_validation(constraint: str) -> None:
with pytest.raises(SchemaError, match=f"'{constraint}' must be coercible to a datetime instance"):
SchemaValidator(cs.datetime_schema(**{constraint: 'bad_value'}))


@pytest.mark.parametrize(
'input_value,expected',
[
Expand Down
19 changes: 15 additions & 4 deletions tests/validators/test_decimal.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
import pytest
from dirty_equals import FunctionCheck, IsStr

from pydantic_core import SchemaValidator, ValidationError, core_schema
from pydantic_core import SchemaError, SchemaValidator, ValidationError
from pydantic_core import core_schema as cs

from ..conftest import Err, PyAndJson, plain_repr
Expand All @@ -19,6 +19,17 @@ class DecimalSubclass(Decimal):
pass


# Note: there's another constraint validation (allow_inf_nan=True cannot be used with max_digits or decimal_places).
# but it is tested in Pydantic:
@pytest.mark.parametrize(
'constraint',
['multiple_of', 'le', 'lt', 'ge', 'gt'],
)
def test_constraints_schema_validation(constraint: str) -> None:
with pytest.raises(SchemaError, match=f"'{constraint}' must be coercible to a Decimal instance"):
SchemaValidator(cs.decimal_schema(**{constraint: 'bad_value'}))


@pytest.mark.parametrize(
'input_value,expected',
[
Expand Down Expand Up @@ -487,20 +498,20 @@ def test_validate_max_digits_and_decimal_places_edge_case() -> None:


def test_str_validation_w_strict() -> None:
s = SchemaValidator(core_schema.decimal_schema(strict=True))
s = SchemaValidator(cs.decimal_schema(strict=True))

with pytest.raises(ValidationError):
assert s.validate_python('1.23')


def test_str_validation_w_lax() -> None:
s = SchemaValidator(core_schema.decimal_schema(strict=False))
s = SchemaValidator(cs.decimal_schema(strict=False))

assert s.validate_python('1.23') == Decimal('1.23')


def test_union_with_str_prefers_str() -> None:
s = SchemaValidator(core_schema.union_schema([core_schema.decimal_schema(), core_schema.str_schema()]))
s = SchemaValidator(cs.union_schema([cs.decimal_schema(), cs.str_schema()]))

assert s.validate_python('1.23') == '1.23'
assert s.validate_python(1.23) == Decimal('1.23')
15 changes: 12 additions & 3 deletions tests/validators/test_int.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,14 +6,23 @@
import pytest
from dirty_equals import IsStr

from pydantic_core import SchemaValidator, ValidationError, core_schema
from pydantic_core import SchemaError, SchemaValidator, ValidationError
from pydantic_core import core_schema as cs

from ..conftest import Err, PyAndJson, plain_repr

i64_max = 9_223_372_036_854_775_807


@pytest.mark.parametrize(
'constraint',
['multiple_of', 'le', 'lt', 'ge', 'gt'],
)
def test_constraints_schema_validation(constraint: str) -> None:
with pytest.raises(SchemaError, match=f"'{constraint}' must be coercible to an integer"):
SchemaValidator(cs.int_schema(**{constraint: 'bad_value'}))


@pytest.mark.parametrize(
'input_value,expected',
[
Expand Down Expand Up @@ -532,7 +541,7 @@ class PlainEnum(Enum):


def test_allow_inf_nan_true_json() -> None:
v = SchemaValidator(core_schema.int_schema(), config=core_schema.CoreConfig(allow_inf_nan=True))
v = SchemaValidator(cs.int_schema(), config=cs.CoreConfig(allow_inf_nan=True))

assert v.validate_json('123') == 123
with pytest.raises(ValidationError, match=r'Input should be a finite number \[type=finite_number'):
Expand All @@ -544,7 +553,7 @@ def test_allow_inf_nan_true_json() -> None:


def test_allow_inf_nan_false_json() -> None:
v = SchemaValidator(core_schema.int_schema(), config=core_schema.CoreConfig(allow_inf_nan=False))
v = SchemaValidator(cs.int_schema(), config=cs.CoreConfig(allow_inf_nan=False))

assert v.validate_json('123') == 123
with pytest.raises(ValidationError, match=r'Input should be a finite number \[type=finite_number'):
Expand Down
Loading