From e2008d99bdb9ca78f0c639d32933ec8b19c42ab8 Mon Sep 17 00:00:00 2001 From: Bas Schoenmaeckers Date: Wed, 4 Jun 2025 11:50:15 +0200 Subject: [PATCH] Allow naive datetime into chrono `DateTime` conversion --- newsfragments/5178.changed.md | 1 + src/conversions/chrono.rs | 109 +++++++++++++++++++++++----------- 2 files changed, 75 insertions(+), 35 deletions(-) create mode 100644 newsfragments/5178.changed.md diff --git a/newsfragments/5178.changed.md b/newsfragments/5178.changed.md new file mode 100644 index 00000000000..7b050ccda81 --- /dev/null +++ b/newsfragments/5178.changed.md @@ -0,0 +1 @@ +Allow converting naive datetime into chrono `DateTime`. diff --git a/src/conversions/chrono.rs b/src/conversions/chrono.rs index fab734422f2..14a82e2e5be 100644 --- a/src/conversions/chrono.rs +++ b/src/conversions/chrono.rs @@ -336,26 +336,28 @@ impl FromPyObject<'py>> FromPyObject<'_> for DateTime Ok(value), - LocalResult::Ambiguous(earliest, latest) => { - #[cfg(not(Py_LIMITED_API))] - let fold = dt.get_fold(); - - #[cfg(Py_LIMITED_API)] - let fold = dt.getattr(intern!(dt.py(), "fold"))?.extract::()? > 0; - - if fold { - Ok(latest) - } else { - Ok(earliest) - } + + py_datetime_to_datetime_with_timezone(dt, tz) + } +} + +#[cfg(feature = "chrono-local")] +impl<'py> FromPyObject<'py> for DateTime { + fn extract_bound(dt: &Bound<'_, PyAny>) -> PyResult> { + let dt = dt.downcast::()?; + + if let Some(tzinfo) = dt.get_tzinfo() { + let local_tz = Local.into_pyobject(dt.py())?; + if !(tzinfo.eq(local_tz)?) { + let name = local_tz.getattr("key")?.downcast_into::()?; + return Err(PyValueError::new_err(format!( + "expected a datetime without timezone or with local timezone {}", + name.to_cow()? + ))); } - LocalResult::None => Err(PyValueError::new_err(format!( - "The datetime {dt:?} contains an incompatible timezone" - ))), - } + }; + + py_datetime_to_datetime_with_timezone(dt, Local) } } @@ -474,22 +476,6 @@ impl<'py> IntoPyObject<'py> for &Local { } } -#[cfg(feature = "chrono-local")] -impl FromPyObject<'_> for Local { - fn extract_bound(ob: &Bound<'_, PyAny>) -> PyResult { - let local_tz = Local.into_pyobject(ob.py())?; - if ob.eq(local_tz)? { - Ok(Local) - } else { - let name = local_tz.getattr("key")?.downcast_into::()?; - Err(PyValueError::new_err(format!( - "expected local timezone {}", - name.to_cow()? - ))) - } - } -} - struct DateArgs { year: i32, month: u8, @@ -590,6 +576,32 @@ fn py_time_to_naive_time(py_time: &Bound<'_, PyAny>) -> PyResult { .ok_or_else(|| PyValueError::new_err("invalid or out-of-range time")) } +fn py_datetime_to_datetime_with_timezone( + dt: &Bound<'_, PyDateTime>, + tz: Tz, +) -> PyResult> { + let naive_dt = NaiveDateTime::new(py_date_to_naive_date(dt)?, py_time_to_naive_time(dt)?); + match naive_dt.and_local_timezone(tz) { + LocalResult::Single(value) => Ok(value), + LocalResult::Ambiguous(earliest, latest) => { + #[cfg(not(Py_LIMITED_API))] + let fold = dt.get_fold(); + + #[cfg(Py_LIMITED_API)] + let fold = dt.getattr(intern!(dt.py(), "fold"))?.extract::()? > 0; + + if fold { + Ok(latest) + } else { + Ok(earliest) + } + } + LocalResult::None => Err(PyValueError::new_err(format!( + "The datetime {dt:?} contains an incompatible timezone" + ))), + } +} + #[cfg(test)] mod tests { use super::*; @@ -982,6 +994,33 @@ mod tests { }) } + #[test] + #[cfg(feature = "chrono-local")] + fn test_pyo3_naive_datetime_frompyobject_local() { + Python::with_gil(|py| { + let year = 2014; + let month = 5; + let day = 6; + let hour = 7; + let minute = 8; + let second = 9; + let micro = 999_999; + let py_datetime = new_py_datetime_ob( + py, + "datetime", + (year, month, day, hour, minute, second, micro), + ); + let py_datetime: DateTime = py_datetime.extract().unwrap(); + let expected_datetime = NaiveDate::from_ymd_opt(year, month, day) + .unwrap() + .and_hms_micro_opt(hour, minute, second, micro) + .unwrap() + .and_local_timezone(Local) + .unwrap(); + assert_eq!(py_datetime, expected_datetime); + }) + } + #[test] fn test_pyo3_datetime_frompyobject_fixed_offset() { Python::with_gil(|py| {