From 15adfc78716b085c9de54b944bc86e76c4ae34ec Mon Sep 17 00:00:00 2001 From: David Hewitt Date: Wed, 18 Dec 2024 09:22:43 +0000 Subject: [PATCH] fix `ValueError` on year zero (#1583) --- src/input/datetime.rs | 72 ++++++++++++++++++------------- src/validators/date.rs | 3 +- src/validators/datetime.rs | 3 +- tests/validators/test_date.py | 6 +++ tests/validators/test_datetime.py | 8 ++++ 5 files changed, 58 insertions(+), 34 deletions(-) diff --git a/src/input/datetime.rs b/src/input/datetime.rs index 6e4093207..f00c00aff 100644 --- a/src/input/datetime.rs +++ b/src/input/datetime.rs @@ -47,20 +47,26 @@ pub fn pydate_as_date(py_date: &Bound<'_, PyAny>) -> PyResult { }) } -impl<'py> IntoPyObject<'py> for EitherDate<'py> { - type Target = PyDate; - type Output = Bound<'py, PyDate>; - type Error = PyErr; - - fn into_pyobject(self, py: Python<'py>) -> PyResult { +impl<'py> EitherDate<'py> { + pub fn try_into_py(&self, py: Python<'py>, input: &(impl Input<'py> + ?Sized)) -> ValResult { match self { - Self::Raw(date) => PyDate::new(py, date.year.into(), date.month, date.day), - Self::Py(date) => Ok(date), + Self::Raw(date) => { + if date.year == 0 { + return Err(ValError::new( + ErrorType::DateParsing { + error: Cow::Borrowed("year 0 is out of range"), + context: None, + }, + input, + )); + }; + let py_date = PyDate::new(py, date.year.into(), date.month, date.day)?; + Ok(py_date.into()) + } + Self::Py(py_date) => Ok(py_date.clone().into()), } } -} -impl EitherDate<'_> { pub fn as_raw(&self) -> PyResult { match self { Self::Raw(date) => Ok(date.clone()), @@ -278,30 +284,36 @@ pub fn pydatetime_as_datetime(py_dt: &Bound<'_, PyAny>) -> PyResult { }) } -impl<'py> IntoPyObject<'py> for EitherDateTime<'py> { - type Target = PyDateTime; - type Output = Bound<'py, PyDateTime>; - type Error = PyErr; - - fn into_pyobject(self, py: Python<'py>) -> PyResult { +impl<'py> EitherDateTime<'py> { + pub fn try_into_py(&self, py: Python<'py>, input: &(impl Input<'py> + ?Sized)) -> ValResult { match self { - Self::Raw(dt) => PyDateTime::new( - py, - dt.date.year.into(), - dt.date.month, - dt.date.day, - dt.time.hour, - dt.time.minute, - dt.time.second, - dt.time.microsecond, - time_as_tzinfo(py, &dt.time)?.as_ref(), - ), - Self::Py(dt) => Ok(dt), + Self::Raw(dt) => { + if dt.date.year == 0 { + return Err(ValError::new( + ErrorType::DatetimeParsing { + error: Cow::Borrowed("year 0 is out of range"), + context: None, + }, + input, + )); + }; + let py_dt = PyDateTime::new( + py, + dt.date.year.into(), + dt.date.month, + dt.date.day, + dt.time.hour, + dt.time.minute, + dt.time.second, + dt.time.microsecond, + time_as_tzinfo(py, &dt.time)?.as_ref(), + )?; + Ok(py_dt.into()) + } + Self::Py(py_dt) => Ok(py_dt.clone().into()), } } -} -impl EitherDateTime<'_> { pub fn as_raw(&self) -> PyResult { match self { Self::Raw(dt) => Ok(dt.clone()), diff --git a/src/validators/date.rs b/src/validators/date.rs index b814dd1f6..2b64b71c6 100644 --- a/src/validators/date.rs +++ b/src/validators/date.rs @@ -1,7 +1,6 @@ use pyo3::intern; use pyo3::prelude::*; use pyo3::types::{PyDate, PyDict, PyString}; -use pyo3::IntoPyObjectExt; use speedate::{Date, Time}; use strum::EnumMessage; @@ -98,7 +97,7 @@ impl Validator for DateValidator { } } } - Ok(date.into_py_any(py)?) + date.try_into_py(py, input) } fn get_name(&self) -> &str { diff --git a/src/validators/datetime.rs b/src/validators/datetime.rs index 2b3c7661a..0d685216f 100644 --- a/src/validators/datetime.rs +++ b/src/validators/datetime.rs @@ -2,7 +2,6 @@ use pyo3::intern; use pyo3::prelude::*; use pyo3::sync::GILOnceCell; use pyo3::types::{PyDict, PyString}; -use pyo3::IntoPyObjectExt; use speedate::{DateTime, Time}; use std::cmp::Ordering; use strum::EnumMessage; @@ -131,7 +130,7 @@ impl Validator for DateTimeValidator { tz_constraint.tz_check(speedate_dt.time.tz_offset, input)?; } } - Ok(datetime.into_py_any(py)?) + datetime.try_into_py(py, input) } fn get_name(&self) -> &str { diff --git a/tests/validators/test_date.py b/tests/validators/test_date.py index 3a1f77134..63e87556c 100644 --- a/tests/validators/test_date.py +++ b/tests/validators/test_date.py @@ -64,6 +64,12 @@ ), pytest.param('-', Err('Input should be a valid date or datetime, input is too short'), id='minus'), pytest.param('+', Err('Input should be a valid date or datetime, input is too short'), id='pus'), + pytest.param('0001-01-01', date(1, 1, 1), id='min-date'), + pytest.param( + '0000-12-31', + Err('Input should be a valid date in the format YYYY-MM-DD, year 0 is out of range [type=date_parsing,'), + id='year-0', + ), ], ) def test_date(input_value, expected): diff --git a/tests/validators/test_datetime.py b/tests/validators/test_datetime.py index d98daa3c5..96e12cf9c 100644 --- a/tests/validators/test_datetime.py +++ b/tests/validators/test_datetime.py @@ -53,6 +53,14 @@ 'Input should be a valid datetime or date, day value is outside expected range [type=datetime_from_date_parsing,' ), ), + ( + '0001-01-01T00:00:00.000000Z', + datetime(1, 1, 1, tzinfo=timezone.utc), + ), + ( + '0000-12-31T23:59:59.999999Z', + Err('Input should be a valid datetime, year 0 is out of range [type=datetime_parsing,'), + ), ], ) def test_datetime(input_value, expected):