Skip to content

Commit 104d398

Browse files
authored
Coerce 'timedelta' schema constraints (#1676)
1 parent 6c936de commit 104d398

File tree

11 files changed

+82
-35
lines changed

11 files changed

+82
-35
lines changed

.github/workflows/ci.yml

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -362,7 +362,8 @@ jobs:
362362
# https://github.com/marketplace/actions/alls-green#why used for branch protection checks
363363
check:
364364
if: always()
365-
needs: [coverage, test-python, test-os, test-debug, lint, bench, build-wasm-emscripten]
365+
# TODO: add build-wasm-emscripten back when CI installation failures are solved:
366+
needs: [coverage, test-python, test-os, test-debug, lint, bench]
366367
runs-on: ubuntu-latest
367368
steps:
368369
- name: Decide whether the needed jobs succeeded or failed

src/validators/date.rs

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,6 @@ use crate::build_tools::{is_strict, py_schema_error_type};
99
use crate::errors::{ErrorType, ErrorTypeDefaults, ValError, ValResult};
1010
use crate::input::{EitherDate, Input};
1111

12-
use crate::tools::SchemaDict;
1312
use crate::validators::datetime::{NowConstraint, NowOp};
1413

1514
use super::Exactness;
@@ -177,7 +176,7 @@ impl DateConstraints {
177176
}
178177

179178
fn convert_pydate(schema: &Bound<'_, PyDict>, key: &Bound<'_, PyString>) -> PyResult<Option<Date>> {
180-
match schema.get_as::<Bound<'_, PyAny>>(key)? {
179+
match schema.get_item(key)? {
181180
Some(value) => match value.validate_date(false) {
182181
Ok(v) => Ok(Some(v.into_inner().as_raw()?)),
183182
Err(_) => Err(PyValueError::new_err(format!(

src/validators/datetime.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -210,7 +210,7 @@ impl DateTimeConstraints {
210210
}
211211

212212
fn py_datetime_as_datetime(schema: &Bound<'_, PyDict>, key: &Bound<'_, PyString>) -> PyResult<Option<DateTime>> {
213-
match schema.get_as::<Bound<'_, PyAny>>(key)? {
213+
match schema.get_item(key)? {
214214
Some(value) => match value.validate_datetime(false, MicrosecondsPrecisionOverflowBehavior::Truncate) {
215215
Ok(v) => Ok(Some(v.into_inner().as_raw()?)),
216216
Err(_) => Err(PyValueError::new_err(format!(

src/validators/decimal.rs

Lines changed: 11 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -28,8 +28,12 @@ pub fn get_decimal_type(py: Python) -> &Bound<'_, PyType> {
2828
.bind(py)
2929
}
3030

31-
fn validate_as_decimal(py: Python, schema: &Bound<'_, PyDict>, key: &str) -> PyResult<Option<Py<PyAny>>> {
32-
match schema.get_as::<Bound<'_, PyAny>>(&PyString::new(py, key))? {
31+
fn validate_as_decimal(
32+
py: Python,
33+
schema: &Bound<'_, PyDict>,
34+
key: &Bound<'_, PyString>,
35+
) -> PyResult<Option<Py<PyAny>>> {
36+
match schema.get_item(key)? {
3337
Some(value) => match value.validate_decimal(false, py) {
3438
Ok(v) => Ok(Some(v.into_inner().unbind())),
3539
Err(_) => Err(PyValueError::new_err(format!(
@@ -77,11 +81,11 @@ impl BuildValidator for DecimalValidator {
7781
allow_inf_nan,
7882
check_digits: decimal_places.is_some() || max_digits.is_some(),
7983
decimal_places,
80-
multiple_of: validate_as_decimal(py, schema, "multiple_of")?,
81-
le: validate_as_decimal(py, schema, "le")?,
82-
lt: validate_as_decimal(py, schema, "lt")?,
83-
ge: validate_as_decimal(py, schema, "ge")?,
84-
gt: validate_as_decimal(py, schema, "gt")?,
84+
multiple_of: validate_as_decimal(py, schema, intern!(py, "multiple_of"))?,
85+
le: validate_as_decimal(py, schema, intern!(py, "le"))?,
86+
lt: validate_as_decimal(py, schema, intern!(py, "lt"))?,
87+
ge: validate_as_decimal(py, schema, intern!(py, "ge"))?,
88+
gt: validate_as_decimal(py, schema, intern!(py, "gt"))?,
8589
max_digits,
8690
}
8791
.into())

src/validators/int.rs

Lines changed: 7 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -8,12 +8,11 @@ use pyo3::IntoPyObjectExt;
88
use crate::build_tools::is_strict;
99
use crate::errors::{ErrorType, ValError, ValResult};
1010
use crate::input::{Input, Int};
11-
use crate::tools::SchemaDict;
1211

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

15-
fn validate_as_int(py: Python, schema: &Bound<'_, PyDict>, key: &str) -> PyResult<Option<Int>> {
16-
match schema.get_as::<Bound<'_, PyAny>>(&PyString::new(py, key))? {
14+
fn validate_as_int(schema: &Bound<'_, PyDict>, key: &Bound<'_, PyString>) -> PyResult<Option<Int>> {
15+
match schema.get_item(key)? {
1716
Some(value) => match value.validate_int(false) {
1817
Ok(v) => match v.into_inner().as_int() {
1918
Ok(v) => Ok(Some(v)),
@@ -93,11 +92,11 @@ impl ConstrainedIntValidator {
9392
let py = schema.py();
9493
Ok(Self {
9594
strict: is_strict(schema, config)?,
96-
multiple_of: validate_as_int(py, schema, "multiple_of")?,
97-
le: validate_as_int(py, schema, "le")?,
98-
lt: validate_as_int(py, schema, "lt")?,
99-
ge: validate_as_int(py, schema, "ge")?,
100-
gt: validate_as_int(py, schema, "gt")?,
95+
multiple_of: validate_as_int(schema, intern!(py, "multiple_of"))?,
96+
le: validate_as_int(schema, intern!(py, "le"))?,
97+
lt: validate_as_int(schema, intern!(py, "lt"))?,
98+
ge: validate_as_int(schema, intern!(py, "ge"))?,
99+
gt: validate_as_int(schema, intern!(py, "gt"))?,
101100
}
102101
.into())
103102
}

src/validators/timedelta.rs

Lines changed: 17 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,12 @@
1+
use pyo3::exceptions::PyValueError;
2+
use pyo3::intern;
13
use pyo3::prelude::*;
2-
use pyo3::types::{PyDelta, PyDeltaAccess, PyDict};
3-
use speedate::Duration;
4+
use pyo3::types::{PyDelta, PyDeltaAccess, PyDict, PyString};
5+
use speedate::{Duration, MicrosecondsPrecisionOverflowBehavior};
46

57
use crate::build_tools::is_strict;
68
use crate::errors::{ErrorType, ValError, ValResult};
7-
use crate::input::{duration_as_pytimedelta, EitherTimedelta, Input};
9+
use crate::input::{duration_as_pytimedelta, Input};
810

911
use super::datetime::extract_microseconds_precision;
1012
use super::{BuildValidator, CombinedValidator, DefinitionsBuilder, ValidationState, Validator};
@@ -24,12 +26,14 @@ struct TimedeltaConstraints {
2426
gt: Option<Duration>,
2527
}
2628

27-
fn get_constraint(schema: &Bound<'_, PyDict>, key: &str) -> PyResult<Option<Duration>> {
29+
fn get_constraint(schema: &Bound<'_, PyDict>, key: &Bound<'_, PyString>) -> PyResult<Option<Duration>> {
2830
match schema.get_item(key)? {
29-
Some(value) => {
30-
let either_timedelta = EitherTimedelta::try_from(&value)?;
31-
Ok(Some(either_timedelta.to_duration()?))
32-
}
31+
Some(value) => match value.validate_timedelta(false, MicrosecondsPrecisionOverflowBehavior::default()) {
32+
Ok(v) => Ok(Some(v.into_inner().to_duration()?)),
33+
Err(_) => Err(PyValueError::new_err(format!(
34+
"'{key}' must be coercible to a timedelta instance"
35+
))),
36+
},
3337
None => Ok(None),
3438
}
3539
}
@@ -42,11 +46,12 @@ impl BuildValidator for TimeDeltaValidator {
4246
config: Option<&Bound<'_, PyDict>>,
4347
_definitions: &mut DefinitionsBuilder<CombinedValidator>,
4448
) -> PyResult<CombinedValidator> {
49+
let py = schema.py();
4550
let constraints = TimedeltaConstraints {
46-
le: get_constraint(schema, "le")?,
47-
lt: get_constraint(schema, "lt")?,
48-
ge: get_constraint(schema, "ge")?,
49-
gt: get_constraint(schema, "gt")?,
51+
le: get_constraint(schema, intern!(py, "le"))?,
52+
lt: get_constraint(schema, intern!(py, "lt"))?,
53+
ge: get_constraint(schema, intern!(py, "ge"))?,
54+
gt: get_constraint(schema, intern!(py, "gt"))?,
5055
};
5156

5257
Ok(Self {

tests/validators/test_date.py

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,11 +17,17 @@
1717
'constraint',
1818
['le', 'lt', 'ge', 'gt'],
1919
)
20-
def test_constraints_schema_validation(constraint: str) -> None:
20+
def test_constraints_schema_validation_error(constraint: str) -> None:
2121
with pytest.raises(SchemaError, match=f"'{constraint}' must be coercible to a date instance"):
2222
SchemaValidator(cs.date_schema(**{constraint: 'bad_value'}))
2323

2424

25+
def test_constraints_schema_validation() -> None:
26+
val = SchemaValidator(cs.date_schema(gt='2020-01-01'))
27+
with pytest.raises(ValidationError):
28+
val.validate_python('2019-01-01')
29+
30+
2531
@pytest.mark.parametrize(
2632
'input_value,expected',
2733
[

tests/validators/test_datetime.py

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,11 +18,17 @@
1818
'constraint',
1919
['le', 'lt', 'ge', 'gt'],
2020
)
21-
def test_constraints_schema_validation(constraint: str) -> None:
21+
def test_constraints_schema_validation_error(constraint: str) -> None:
2222
with pytest.raises(SchemaError, match=f"'{constraint}' must be coercible to a datetime instance"):
2323
SchemaValidator(cs.datetime_schema(**{constraint: 'bad_value'}))
2424

2525

26+
def test_constraints_schema_validation() -> None:
27+
val = SchemaValidator(cs.datetime_schema(gt='2020-01-01T00:00:00'))
28+
with pytest.raises(ValidationError):
29+
val.validate_python('2019-01-01T00:00:00')
30+
31+
2632
@pytest.mark.parametrize(
2733
'input_value,expected',
2834
[

tests/validators/test_decimal.py

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,11 +25,17 @@ class DecimalSubclass(Decimal):
2525
'constraint',
2626
['multiple_of', 'le', 'lt', 'ge', 'gt'],
2727
)
28-
def test_constraints_schema_validation(constraint: str) -> None:
28+
def test_constraints_schema_validation_error(constraint: str) -> None:
2929
with pytest.raises(SchemaError, match=f"'{constraint}' must be coercible to a Decimal instance"):
3030
SchemaValidator(cs.decimal_schema(**{constraint: 'bad_value'}))
3131

3232

33+
def test_constraints_schema_validation() -> None:
34+
val = SchemaValidator(cs.decimal_schema(gt='1'))
35+
with pytest.raises(ValidationError):
36+
val.validate_python('0')
37+
38+
3339
@pytest.mark.parametrize(
3440
'input_value,expected',
3541
[

tests/validators/test_int.py

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,11 +18,17 @@
1818
'constraint',
1919
['multiple_of', 'le', 'lt', 'ge', 'gt'],
2020
)
21-
def test_constraints_schema_validation(constraint: str) -> None:
21+
def test_constraints_schema_validation_error(constraint: str) -> None:
2222
with pytest.raises(SchemaError, match=f"'{constraint}' must be coercible to an integer"):
2323
SchemaValidator(cs.int_schema(**{constraint: 'bad_value'}))
2424

2525

26+
def test_constraints_schema_validation() -> None:
27+
val = SchemaValidator(cs.int_schema(gt='1'))
28+
with pytest.raises(ValidationError):
29+
val.validate_python('0')
30+
31+
2632
@pytest.mark.parametrize(
2733
'input_value,expected',
2834
[

0 commit comments

Comments
 (0)