Skip to content

Commit

Permalink
Convert datetime error message to human friendly message (pydantic#915)
Browse files Browse the repository at this point in the history
  • Loading branch information
yohanvalencia authored Aug 23, 2023
1 parent 7f7d03d commit 15f62b6
Show file tree
Hide file tree
Showing 2 changed files with 63 additions and 15 deletions.
46 changes: 42 additions & 4 deletions src/validators/timedelta.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
use pyo3::prelude::*;
use pyo3::types::PyDict;
use pyo3::types::{PyDelta, PyDeltaAccess, PyDict};
use speedate::Duration;

use crate::build_tools::is_strict;
Expand Down Expand Up @@ -83,9 +83,7 @@ impl Validator for TimeDeltaValidator {
return Err(ValError::new(
ErrorType::$error {
context: None,
$constraint: duration_as_pytimedelta(py, constraint)?
.repr()?
.to_string()
$constraint: pydelta_to_human_readable(duration_as_pytimedelta(py, constraint)?)
.into(),
},
py_timedelta.as_ref(),
Expand Down Expand Up @@ -119,3 +117,43 @@ impl Validator for TimeDeltaValidator {
Ok(())
}
}
fn pydelta_to_human_readable(py_delta: &PyDelta) -> String {
let total_seconds = py_delta.get_seconds();
let hours = total_seconds / 3600;
let minutes = (total_seconds % 3600) / 60;
let seconds = total_seconds % 60;
let microseconds = py_delta.get_microseconds();
let days = py_delta.get_days();

let mut formatted_duration = Vec::new();

if days != 0 {
formatted_duration.push(format!("{} day{}", days, if days != 1 { "s" } else { "" }));
}

if hours != 0 {
formatted_duration.push(format!("{} hour{}", hours, if hours != 1 { "s" } else { "" }));
}

if minutes != 0 {
formatted_duration.push(format!("{} minute{}", minutes, if minutes != 1 { "s" } else { "" }));
}

if seconds != 0 {
formatted_duration.push(format!("{} second{}", seconds, if seconds != 1 { "s" } else { "" }));
}

if microseconds != 0 {
formatted_duration.push(format!(
"{} microsecond{}",
microseconds,
if microseconds != 1 { "s" } else { "" }
));
}

if formatted_duration.is_empty() {
formatted_duration.push("0 seconds".to_string());
}

formatted_duration.join(" and ")
}
32 changes: 21 additions & 11 deletions tests/validators/test_timedelta.py
Original file line number Diff line number Diff line change
Expand Up @@ -136,25 +136,21 @@ def test_timedelta_strict_json(input_value, expected):
({}, 'P0Y0M3D2WT1H2M3S', timedelta(days=3, weeks=2, hours=1, minutes=2, seconds=3)),
({'le': timedelta(days=3)}, 'P2DT1H', timedelta(days=2, hours=1)),
({'le': timedelta(days=3)}, 'P3DT0H', timedelta(days=3)),
({'le': timedelta(days=3)}, 'P3DT1H', Err('Input should be less than or equal to datetime.timedelta(days=3)')),
({'le': timedelta(days=3)}, 'P3DT1H', Err('Input should be less than or equal to 3 days')),
({'lt': timedelta(days=3)}, 'P2DT1H', timedelta(days=2, hours=1)),
({'lt': timedelta(days=3)}, 'P3DT1H', Err('Input should be less than datetime.timedelta(days=3)')),
({'lt': timedelta(days=3)}, 'P3DT1H', Err('Input should be less than 3 days')),
({'ge': timedelta(days=3)}, 'P3DT1H', timedelta(days=3, hours=1)),
({'ge': timedelta(days=3)}, 'P3D', timedelta(days=3)),
(
{'ge': timedelta(days=3)},
'P2DT1H',
Err('Input should be greater than or equal to datetime.timedelta(days=3)'),
),
({'ge': timedelta(days=3)}, 'P2DT1H', Err('Input should be greater than or equal to 3 days')),
({'gt': timedelta(days=3)}, 'P3DT1H', timedelta(days=3, hours=1)),
({'gt': 'P3D'}, 'P2DT1H', Err('Input should be greater than datetime.timedelta(days=3)')),
({'gt': 'P3D'}, 'P2DT1H', Err('Input should be greater than 3 days')),
({'le': timedelta(seconds=-86400.123)}, '-PT86400.123S', timedelta(seconds=-86400.123)),
({'le': timedelta(seconds=-86400.123)}, '-PT86400.124S', timedelta(seconds=-86400.124)),
(
{'le': timedelta(seconds=-86400.123)},
'-PT86400.122S',
Err(
'Input should be less than or equal to datetime.timedelta(days=-2, seconds=86399, microseconds=877000) [type=less_than_equal'
'Input should be less than or equal to -2 days and 23 hours and 59 minutes and 59 seconds and 877000 microseconds [type=less_than_equal'
),
),
({'gt': timedelta(seconds=-86400.123)}, timedelta(seconds=-86400.122), timedelta(seconds=-86400.122)),
Expand All @@ -163,9 +159,23 @@ def test_timedelta_strict_json(input_value, expected):
{'gt': timedelta(seconds=-86400.123)},
'-PT86400.124S',
Err(
'Input should be greater than datetime.timedelta(days=-2, seconds=86399, microseconds=877000) [type=greater_than'
'Input should be greater than -2 days and 23 hours and 59 minutes and 59 seconds and 877000 microseconds [type=greater_than'
),
),
(
{'gt': timedelta(hours=1, minutes=30)},
'PT180S',
Err('Input should be greater than 1 hour and 30 minutes [type=greater_than'),
),
({'gt': timedelta()}, '-P0DT0.1S', Err('Input should be greater than 0 seconds [type=greater_than')),
({'gt': timedelta()}, 'P0DT0.0S', Err('Input should be greater than 0 seconds [type=greater_than')),
({'ge': timedelta()}, 'P0DT0.0S', timedelta()),
({'lt': timedelta()}, '-PT0S', timedelta()),
(
{'lt': timedelta(days=740, weeks=1, hours=48, minutes=60, seconds=61, microseconds=100000)},
'P2Y1W10DT48H60M61.100000S',
Err('Input should be less than 749 days and 1 hour and 1 minute and 1 second and 100000 microseconds'),
),
],
ids=repr,
)
Expand Down Expand Up @@ -275,7 +285,7 @@ def test_pandas():
assert v.validate_python(two_hours.to_pytimedelta()) == two_hours

one_55 = pandas.Timestamp('2023-01-01T01:55:00Z') - pandas.Timestamp('2023-01-01T00:00:00Z')
msg = r'Input should be greater than or equal to datetime.timedelta\(seconds=7200\)'
msg = r'Input should be greater than or equal to 2 hours'
with pytest.raises(ValidationError, match=msg):
v.validate_python(one_55)
with pytest.raises(ValidationError, match=msg):
Expand Down

0 comments on commit 15f62b6

Please sign in to comment.