diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 7ba7247a2..01176881b 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -362,7 +362,8 @@ jobs: # https://github.com/marketplace/actions/alls-green#why used for branch protection checks check: if: always() - needs: [coverage, test-python, test-os, test-debug, lint, bench, build-wasm-emscripten] + # TODO: add build-wasm-emscripten back when CI installation failures are solved: + needs: [coverage, test-python, test-os, test-debug, lint, bench] runs-on: ubuntu-latest steps: - name: Decide whether the needed jobs succeeded or failed diff --git a/src/validators/date.rs b/src/validators/date.rs index d85af4a13..6fec1f89e 100644 --- a/src/validators/date.rs +++ b/src/validators/date.rs @@ -9,7 +9,6 @@ use crate::build_tools::{is_strict, py_schema_error_type}; use crate::errors::{ErrorType, ErrorTypeDefaults, ValError, ValResult}; use crate::input::{EitherDate, Input}; -use crate::tools::SchemaDict; use crate::validators::datetime::{NowConstraint, NowOp}; use super::Exactness; @@ -177,7 +176,7 @@ impl DateConstraints { } fn convert_pydate(schema: &Bound<'_, PyDict>, key: &Bound<'_, PyString>) -> PyResult> { - match schema.get_as::>(key)? { + match schema.get_item(key)? { Some(value) => match value.validate_date(false) { Ok(v) => Ok(Some(v.into_inner().as_raw()?)), Err(_) => Err(PyValueError::new_err(format!( diff --git a/src/validators/datetime.rs b/src/validators/datetime.rs index fad79deff..d82a3dd6c 100644 --- a/src/validators/datetime.rs +++ b/src/validators/datetime.rs @@ -210,7 +210,7 @@ impl DateTimeConstraints { } fn py_datetime_as_datetime(schema: &Bound<'_, PyDict>, key: &Bound<'_, PyString>) -> PyResult> { - match schema.get_as::>(key)? { + match schema.get_item(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!( diff --git a/src/validators/decimal.rs b/src/validators/decimal.rs index 4f6db8ae8..3d8c90c6e 100644 --- a/src/validators/decimal.rs +++ b/src/validators/decimal.rs @@ -28,8 +28,12 @@ pub fn get_decimal_type(py: Python) -> &Bound<'_, PyType> { .bind(py) } -fn validate_as_decimal(py: Python, schema: &Bound<'_, PyDict>, key: &str) -> PyResult>> { - match schema.get_as::>(&PyString::new(py, key))? { +fn validate_as_decimal( + py: Python, + schema: &Bound<'_, PyDict>, + key: &Bound<'_, PyString>, +) -> PyResult>> { + match schema.get_item(key)? { Some(value) => match value.validate_decimal(false, py) { Ok(v) => Ok(Some(v.into_inner().unbind())), Err(_) => Err(PyValueError::new_err(format!( @@ -77,11 +81,11 @@ impl BuildValidator for DecimalValidator { allow_inf_nan, check_digits: decimal_places.is_some() || max_digits.is_some(), decimal_places, - 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")?, + multiple_of: validate_as_decimal(py, schema, intern!(py, "multiple_of"))?, + le: validate_as_decimal(py, schema, intern!(py, "le"))?, + lt: validate_as_decimal(py, schema, intern!(py, "lt"))?, + ge: validate_as_decimal(py, schema, intern!(py, "ge"))?, + gt: validate_as_decimal(py, schema, intern!(py, "gt"))?, max_digits, } .into()) diff --git a/src/validators/int.rs b/src/validators/int.rs index 8d30f215c..28d068c21 100644 --- a/src/validators/int.rs +++ b/src/validators/int.rs @@ -8,12 +8,11 @@ use pyo3::IntoPyObjectExt; use crate::build_tools::is_strict; use crate::errors::{ErrorType, ValError, ValResult}; use crate::input::{Input, Int}; -use crate::tools::SchemaDict; use super::{BuildValidator, CombinedValidator, DefinitionsBuilder, ValidationState, Validator}; -fn validate_as_int(py: Python, schema: &Bound<'_, PyDict>, key: &str) -> PyResult> { - match schema.get_as::>(&PyString::new(py, key))? { +fn validate_as_int(schema: &Bound<'_, PyDict>, key: &Bound<'_, PyString>) -> PyResult> { + match schema.get_item(key)? { Some(value) => match value.validate_int(false) { Ok(v) => match v.into_inner().as_int() { Ok(v) => Ok(Some(v)), @@ -93,11 +92,11 @@ impl ConstrainedIntValidator { 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")?, + multiple_of: validate_as_int(schema, intern!(py, "multiple_of"))?, + le: validate_as_int(schema, intern!(py, "le"))?, + lt: validate_as_int(schema, intern!(py, "lt"))?, + ge: validate_as_int(schema, intern!(py, "ge"))?, + gt: validate_as_int(schema, intern!(py, "gt"))?, } .into()) } diff --git a/src/validators/timedelta.rs b/src/validators/timedelta.rs index a4c755f8a..d1d6ff3b8 100644 --- a/src/validators/timedelta.rs +++ b/src/validators/timedelta.rs @@ -1,10 +1,12 @@ +use pyo3::exceptions::PyValueError; +use pyo3::intern; use pyo3::prelude::*; -use pyo3::types::{PyDelta, PyDeltaAccess, PyDict}; -use speedate::Duration; +use pyo3::types::{PyDelta, PyDeltaAccess, PyDict, PyString}; +use speedate::{Duration, MicrosecondsPrecisionOverflowBehavior}; use crate::build_tools::is_strict; use crate::errors::{ErrorType, ValError, ValResult}; -use crate::input::{duration_as_pytimedelta, EitherTimedelta, Input}; +use crate::input::{duration_as_pytimedelta, Input}; use super::datetime::extract_microseconds_precision; use super::{BuildValidator, CombinedValidator, DefinitionsBuilder, ValidationState, Validator}; @@ -24,12 +26,14 @@ struct TimedeltaConstraints { gt: Option, } -fn get_constraint(schema: &Bound<'_, PyDict>, key: &str) -> PyResult> { +fn get_constraint(schema: &Bound<'_, PyDict>, key: &Bound<'_, PyString>) -> PyResult> { match schema.get_item(key)? { - Some(value) => { - let either_timedelta = EitherTimedelta::try_from(&value)?; - Ok(Some(either_timedelta.to_duration()?)) - } + Some(value) => match value.validate_timedelta(false, MicrosecondsPrecisionOverflowBehavior::default()) { + Ok(v) => Ok(Some(v.into_inner().to_duration()?)), + Err(_) => Err(PyValueError::new_err(format!( + "'{key}' must be coercible to a timedelta instance" + ))), + }, None => Ok(None), } } @@ -42,11 +46,12 @@ impl BuildValidator for TimeDeltaValidator { config: Option<&Bound<'_, PyDict>>, _definitions: &mut DefinitionsBuilder, ) -> PyResult { + let py = schema.py(); let constraints = TimedeltaConstraints { - le: get_constraint(schema, "le")?, - lt: get_constraint(schema, "lt")?, - ge: get_constraint(schema, "ge")?, - gt: get_constraint(schema, "gt")?, + le: get_constraint(schema, intern!(py, "le"))?, + lt: get_constraint(schema, intern!(py, "lt"))?, + ge: get_constraint(schema, intern!(py, "ge"))?, + gt: get_constraint(schema, intern!(py, "gt"))?, }; Ok(Self { diff --git a/tests/validators/test_date.py b/tests/validators/test_date.py index 202a8f205..161f115d9 100644 --- a/tests/validators/test_date.py +++ b/tests/validators/test_date.py @@ -17,11 +17,17 @@ 'constraint', ['le', 'lt', 'ge', 'gt'], ) -def test_constraints_schema_validation(constraint: str) -> None: +def test_constraints_schema_validation_error(constraint: str) -> None: with pytest.raises(SchemaError, match=f"'{constraint}' must be coercible to a date instance"): SchemaValidator(cs.date_schema(**{constraint: 'bad_value'})) +def test_constraints_schema_validation() -> None: + val = SchemaValidator(cs.date_schema(gt='2020-01-01')) + with pytest.raises(ValidationError): + val.validate_python('2019-01-01') + + @pytest.mark.parametrize( 'input_value,expected', [ diff --git a/tests/validators/test_datetime.py b/tests/validators/test_datetime.py index 16ea0b4e0..21b0db61e 100644 --- a/tests/validators/test_datetime.py +++ b/tests/validators/test_datetime.py @@ -18,11 +18,17 @@ 'constraint', ['le', 'lt', 'ge', 'gt'], ) -def test_constraints_schema_validation(constraint: str) -> None: +def test_constraints_schema_validation_error(constraint: str) -> None: with pytest.raises(SchemaError, match=f"'{constraint}' must be coercible to a datetime instance"): SchemaValidator(cs.datetime_schema(**{constraint: 'bad_value'})) +def test_constraints_schema_validation() -> None: + val = SchemaValidator(cs.datetime_schema(gt='2020-01-01T00:00:00')) + with pytest.raises(ValidationError): + val.validate_python('2019-01-01T00:00:00') + + @pytest.mark.parametrize( 'input_value,expected', [ diff --git a/tests/validators/test_decimal.py b/tests/validators/test_decimal.py index 1a8d15871..64d34ba18 100644 --- a/tests/validators/test_decimal.py +++ b/tests/validators/test_decimal.py @@ -25,11 +25,17 @@ class DecimalSubclass(Decimal): 'constraint', ['multiple_of', 'le', 'lt', 'ge', 'gt'], ) -def test_constraints_schema_validation(constraint: str) -> None: +def test_constraints_schema_validation_error(constraint: str) -> None: with pytest.raises(SchemaError, match=f"'{constraint}' must be coercible to a Decimal instance"): SchemaValidator(cs.decimal_schema(**{constraint: 'bad_value'})) +def test_constraints_schema_validation() -> None: + val = SchemaValidator(cs.decimal_schema(gt='1')) + with pytest.raises(ValidationError): + val.validate_python('0') + + @pytest.mark.parametrize( 'input_value,expected', [ diff --git a/tests/validators/test_int.py b/tests/validators/test_int.py index f16bf1377..4db426032 100644 --- a/tests/validators/test_int.py +++ b/tests/validators/test_int.py @@ -18,11 +18,17 @@ 'constraint', ['multiple_of', 'le', 'lt', 'ge', 'gt'], ) -def test_constraints_schema_validation(constraint: str) -> None: +def test_constraints_schema_validation_error(constraint: str) -> None: with pytest.raises(SchemaError, match=f"'{constraint}' must be coercible to an integer"): SchemaValidator(cs.int_schema(**{constraint: 'bad_value'})) +def test_constraints_schema_validation() -> None: + val = SchemaValidator(cs.int_schema(gt='1')) + with pytest.raises(ValidationError): + val.validate_python('0') + + @pytest.mark.parametrize( 'input_value,expected', [ diff --git a/tests/validators/test_timedelta.py b/tests/validators/test_timedelta.py index e9b885481..c10a2d592 100644 --- a/tests/validators/test_timedelta.py +++ b/tests/validators/test_timedelta.py @@ -15,6 +15,21 @@ pandas = None +@pytest.mark.parametrize( + 'constraint', + ['le', 'lt', 'ge', 'gt'], +) +def test_constraints_schema_validation_error(constraint: str) -> None: + with pytest.raises(SchemaError, match=f"'{constraint}' must be coercible to a timedelta instance"): + SchemaValidator(core_schema.timedelta_schema(**{constraint: 'bad_value'})) + + +def test_constraints_schema_validation() -> None: + val = SchemaValidator(core_schema.timedelta_schema(gt=3)) + with pytest.raises(ValidationError): + val.validate_python(1) + + @pytest.mark.parametrize( 'input_value,expected', [