From f83544910f55bdf741c689241b3e00058f833660 Mon Sep 17 00:00:00 2001 From: Tpt Date: Thu, 11 Jan 2024 10:00:49 +0100 Subject: [PATCH] Adds conversion between SystemTime and datetime --- guide/src/conversions/tables.md | 2 +- newsfragments/3736.added.md | 1 + src/conversions/std/duration.rs | 196 ----------------- src/conversions/std/mod.rs | 2 +- src/conversions/std/time.rs | 374 ++++++++++++++++++++++++++++++++ 5 files changed, 377 insertions(+), 198 deletions(-) create mode 100644 newsfragments/3736.added.md delete mode 100755 src/conversions/std/duration.rs create mode 100755 src/conversions/std/time.rs diff --git a/guide/src/conversions/tables.md b/guide/src/conversions/tables.md index f9d716b3..6b7c3bfb 100644 --- a/guide/src/conversions/tables.md +++ b/guide/src/conversions/tables.md @@ -29,7 +29,7 @@ The table below contains the Python type and the corresponding function argument | `type` | - | `&PyType` | | `module` | - | `&PyModule` | | `collections.abc.Buffer` | - | `PyBuffer` | -| `datetime.datetime` | - | `&PyDateTime` | +| `datetime.datetime` | `SystemTime` | `&PyDateTime` | | `datetime.date` | - | `&PyDate` | | `datetime.time` | - | `&PyTime` | | `datetime.tzinfo` | - | `&PyTzInfo` | diff --git a/newsfragments/3736.added.md b/newsfragments/3736.added.md new file mode 100644 index 00000000..0d3a4a08 --- /dev/null +++ b/newsfragments/3736.added.md @@ -0,0 +1 @@ +Conversion between `std::time::SystemTime` and `datetime.datetime` \ No newline at end of file diff --git a/src/conversions/std/duration.rs b/src/conversions/std/duration.rs deleted file mode 100755 index e4540bd0..00000000 --- a/src/conversions/std/duration.rs +++ /dev/null @@ -1,196 +0,0 @@ -use crate::exceptions::PyValueError; -#[cfg(Py_LIMITED_API)] -use crate::sync::GILOnceCell; -#[cfg(Py_LIMITED_API)] -use crate::types::PyType; -#[cfg(not(Py_LIMITED_API))] -use crate::types::{PyDelta, PyDeltaAccess}; -#[cfg(Py_LIMITED_API)] -use crate::{intern, Py}; -use crate::{FromPyObject, IntoPy, PyAny, PyObject, PyResult, Python, ToPyObject}; -use std::time::Duration; - -const SECONDS_PER_DAY: u64 = 24 * 60 * 60; - -impl FromPyObject<'_> for Duration { - fn extract(obj: &PyAny) -> PyResult { - #[cfg(not(Py_LIMITED_API))] - let (days, seconds, microseconds) = { - let delta: &PyDelta = obj.downcast()?; - ( - delta.get_days(), - delta.get_seconds(), - delta.get_microseconds(), - ) - }; - #[cfg(Py_LIMITED_API)] - let (days, seconds, microseconds): (i32, i32, i32) = { - ( - obj.getattr(intern!(obj.py(), "days"))?.extract()?, - obj.getattr(intern!(obj.py(), "seconds"))?.extract()?, - obj.getattr(intern!(obj.py(), "microseconds"))?.extract()?, - ) - }; - - // We cast - let days = u64::try_from(days).map_err(|_| { - PyValueError::new_err( - "It is not possible to convert a negative timedelta to a Rust Duration", - ) - })?; - let seconds = u64::try_from(seconds).unwrap(); // 0 <= seconds < 3600*24 - let microseconds = u32::try_from(microseconds).unwrap(); // 0 <= microseconds < 1000000 - - // We convert - let total_seconds = days * SECONDS_PER_DAY + seconds; // We casted from i32, this can't overflow - let nanoseconds = microseconds.checked_mul(1_000).unwrap(); // 0 <= microseconds < 1000000 - - Ok(Duration::new(total_seconds, nanoseconds)) - } -} - -impl ToPyObject for Duration { - fn to_object(&self, py: Python<'_>) -> PyObject { - let days = self.as_secs() / SECONDS_PER_DAY; - let seconds = self.as_secs() % SECONDS_PER_DAY; - let microseconds = self.subsec_micros(); - - #[cfg(not(Py_LIMITED_API))] - { - PyDelta::new( - py, - days.try_into() - .expect("Too large Rust duration for timedelta"), - seconds.try_into().unwrap(), - microseconds.try_into().unwrap(), - false, - ) - .expect("failed to construct timedelta (overflow?)") - .into() - } - #[cfg(Py_LIMITED_API)] - { - static TIMEDELTA: GILOnceCell> = GILOnceCell::new(); - TIMEDELTA - .get_or_try_init_type_ref(py, "datetime", "timedelta") - .unwrap() - .call1((days, seconds, microseconds)) - .unwrap() - .into() - } - } -} - -impl IntoPy for Duration { - fn into_py(self, py: Python<'_>) -> PyObject { - self.to_object(py) - } -} - -#[cfg(test)] -mod tests { - use super::*; - use std::panic; - - #[test] - fn test_frompyobject() { - Python::with_gil(|py| { - assert_eq!( - new_timedelta(py, 0, 0, 0).extract::().unwrap(), - Duration::new(0, 0) - ); - assert_eq!( - new_timedelta(py, 1, 0, 0).extract::().unwrap(), - Duration::new(86400, 0) - ); - assert_eq!( - new_timedelta(py, 0, 1, 0).extract::().unwrap(), - Duration::new(1, 0) - ); - assert_eq!( - new_timedelta(py, 0, 0, 1).extract::().unwrap(), - Duration::new(0, 1_000) - ); - assert_eq!( - new_timedelta(py, 1, 1, 1).extract::().unwrap(), - Duration::new(86401, 1_000) - ); - assert_eq!( - timedelta_class(py) - .getattr("max") - .unwrap() - .extract::() - .unwrap(), - Duration::new(86399999999999, 999999000) - ); - }); - } - - #[test] - fn test_frompyobject_negative() { - Python::with_gil(|py| { - assert_eq!( - new_timedelta(py, 0, -1, 0) - .extract::() - .unwrap_err() - .to_string(), - "ValueError: It is not possible to convert a negative timedelta to a Rust Duration" - ); - }) - } - - #[test] - fn test_topyobject() { - Python::with_gil(|py| { - let assert_eq = |l: PyObject, r: &PyAny| { - assert!(l.as_ref(py).eq(r).unwrap()); - }; - - assert_eq( - Duration::new(0, 0).to_object(py), - new_timedelta(py, 0, 0, 0), - ); - assert_eq( - Duration::new(86400, 0).to_object(py), - new_timedelta(py, 1, 0, 0), - ); - assert_eq( - Duration::new(1, 0).to_object(py), - new_timedelta(py, 0, 1, 0), - ); - assert_eq( - Duration::new(0, 1_000).to_object(py), - new_timedelta(py, 0, 0, 1), - ); - assert_eq( - Duration::new(0, 1).to_object(py), - new_timedelta(py, 0, 0, 0), - ); - assert_eq( - Duration::new(86401, 1_000).to_object(py), - new_timedelta(py, 1, 1, 1), - ); - assert_eq( - Duration::new(86399999999999, 999999000).to_object(py), - timedelta_class(py).getattr("max").unwrap(), - ); - }); - } - - #[test] - fn test_topyobject_overflow() { - Python::with_gil(|py| { - assert!(panic::catch_unwind(|| Duration::MAX.to_object(py)).is_err()); - }) - } - - fn new_timedelta(py: Python<'_>, days: i32, seconds: i32, microseconds: i32) -> &PyAny { - timedelta_class(py) - .call1((days, seconds, microseconds)) - .unwrap() - } - - fn timedelta_class(py: Python<'_>) -> &PyAny { - py.import("datetime").unwrap().getattr("timedelta").unwrap() - } -} diff --git a/src/conversions/std/mod.rs b/src/conversions/std/mod.rs index ebe1c955..9b10b59f 100644 --- a/src/conversions/std/mod.rs +++ b/src/conversions/std/mod.rs @@ -1,5 +1,4 @@ mod array; -mod duration; mod ipaddr; mod map; mod num; @@ -8,4 +7,5 @@ mod path; mod set; mod slice; mod string; +mod time; mod vec; diff --git a/src/conversions/std/time.rs b/src/conversions/std/time.rs new file mode 100755 index 00000000..71cdfa23 --- /dev/null +++ b/src/conversions/std/time.rs @@ -0,0 +1,374 @@ +use crate::exceptions::{PyOverflowError, PyValueError}; +use crate::sync::GILOnceCell; +#[cfg(Py_LIMITED_API)] +use crate::types::PyType; +#[cfg(not(Py_LIMITED_API))] +use crate::types::{timezone_utc, PyDateTime, PyDelta, PyDeltaAccess}; +#[cfg(Py_LIMITED_API)] +use crate::Py; +use crate::{intern, FromPyObject, IntoPy, PyAny, PyErr, PyObject, PyResult, Python, ToPyObject}; +use std::time::{Duration, SystemTime, UNIX_EPOCH}; + +const SECONDS_PER_DAY: u64 = 24 * 60 * 60; + +impl FromPyObject<'_> for Duration { + fn extract(obj: &PyAny) -> PyResult { + #[cfg(not(Py_LIMITED_API))] + let (days, seconds, microseconds) = { + let delta: &PyDelta = obj.downcast()?; + ( + delta.get_days(), + delta.get_seconds(), + delta.get_microseconds(), + ) + }; + #[cfg(Py_LIMITED_API)] + let (days, seconds, microseconds): (i32, i32, i32) = { + ( + obj.getattr(intern!(obj.py(), "days"))?.extract()?, + obj.getattr(intern!(obj.py(), "seconds"))?.extract()?, + obj.getattr(intern!(obj.py(), "microseconds"))?.extract()?, + ) + }; + + // We cast + let days = u64::try_from(days).map_err(|_| { + PyValueError::new_err( + "It is not possible to convert a negative timedelta to a Rust Duration", + ) + })?; + let seconds = u64::try_from(seconds).unwrap(); // 0 <= seconds < 3600*24 + let microseconds = u32::try_from(microseconds).unwrap(); // 0 <= microseconds < 1000000 + + // We convert + let total_seconds = days * SECONDS_PER_DAY + seconds; // We casted from i32, this can't overflow + let nanoseconds = microseconds.checked_mul(1_000).unwrap(); // 0 <= microseconds < 1000000 + + Ok(Duration::new(total_seconds, nanoseconds)) + } +} + +impl ToPyObject for Duration { + fn to_object(&self, py: Python<'_>) -> PyObject { + let days = self.as_secs() / SECONDS_PER_DAY; + let seconds = self.as_secs() % SECONDS_PER_DAY; + let microseconds = self.subsec_micros(); + + #[cfg(not(Py_LIMITED_API))] + { + PyDelta::new( + py, + days.try_into() + .expect("Too large Rust duration for timedelta"), + seconds.try_into().unwrap(), + microseconds.try_into().unwrap(), + false, + ) + .expect("failed to construct timedelta (overflow?)") + .into() + } + #[cfg(Py_LIMITED_API)] + { + static TIMEDELTA: GILOnceCell> = GILOnceCell::new(); + TIMEDELTA + .get_or_try_init_type_ref(py, "datetime", "timedelta") + .unwrap() + .call1((days, seconds, microseconds)) + .unwrap() + .into() + } + } +} + +impl IntoPy for Duration { + fn into_py(self, py: Python<'_>) -> PyObject { + self.to_object(py) + } +} + +// Conversions between SystemTime and datetime do not rely on the floating point timestamp of the +// timestamp/fromtimestamp APIs to avoid possible precision loss but goes through the +// timedelta/std::time::Duration types by taking for reference point the UNIX epoch. +// +// TODO: it might be nice to investigate using timestamps anyway, at least when the datetime is a safe range. + +impl FromPyObject<'_> for SystemTime { + fn extract(obj: &PyAny) -> PyResult { + let duration_since_unix_epoch: Duration = obj + .call_method1(intern!(obj.py(), "__sub__"), (unix_epoch_py(obj.py()),))? + .extract()?; + UNIX_EPOCH + .checked_add(duration_since_unix_epoch) + .ok_or_else(|| { + PyOverflowError::new_err("Overflow error when converting the time to Rust") + }) + } +} + +impl ToPyObject for SystemTime { + fn to_object(&self, py: Python<'_>) -> PyObject { + let duration_since_unix_epoch = self.duration_since(UNIX_EPOCH).unwrap().into_py(py); + unix_epoch_py(py) + .call_method1(py, intern!(py, "__add__"), (duration_since_unix_epoch,)) + .unwrap() + } +} + +impl IntoPy for SystemTime { + fn into_py(self, py: Python<'_>) -> PyObject { + self.to_object(py) + } +} + +fn unix_epoch_py(py: Python<'_>) -> &PyObject { + static UNIX_EPOCH: GILOnceCell = GILOnceCell::new(); + UNIX_EPOCH + .get_or_try_init(py, || { + #[cfg(not(Py_LIMITED_API))] + { + Ok::<_, PyErr>( + PyDateTime::new(py, 1970, 1, 1, 0, 0, 0, 0, Some(timezone_utc(py)))?.into(), + ) + } + #[cfg(Py_LIMITED_API)] + { + let datetime = py.import("datetime")?; + let utc = datetime.getattr("timezone")?.getattr("utc")?; + Ok::<_, PyErr>( + datetime + .getattr("datetime")? + .call1((1970, 1, 1, 0, 0, 0, 0, utc)) + .unwrap() + .into(), + ) + } + }) + .unwrap() +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::types::PyDict; + use std::panic; + + #[test] + fn test_duration_frompyobject() { + Python::with_gil(|py| { + assert_eq!( + new_timedelta(py, 0, 0, 0).extract::().unwrap(), + Duration::new(0, 0) + ); + assert_eq!( + new_timedelta(py, 1, 0, 0).extract::().unwrap(), + Duration::new(86400, 0) + ); + assert_eq!( + new_timedelta(py, 0, 1, 0).extract::().unwrap(), + Duration::new(1, 0) + ); + assert_eq!( + new_timedelta(py, 0, 0, 1).extract::().unwrap(), + Duration::new(0, 1_000) + ); + assert_eq!( + new_timedelta(py, 1, 1, 1).extract::().unwrap(), + Duration::new(86401, 1_000) + ); + assert_eq!( + timedelta_class(py) + .getattr("max") + .unwrap() + .extract::() + .unwrap(), + Duration::new(86399999999999, 999999000) + ); + }); + } + + #[test] + fn test_duration_frompyobject_negative() { + Python::with_gil(|py| { + assert_eq!( + new_timedelta(py, 0, -1, 0) + .extract::() + .unwrap_err() + .to_string(), + "ValueError: It is not possible to convert a negative timedelta to a Rust Duration" + ); + }) + } + + #[test] + fn test_duration_topyobject() { + Python::with_gil(|py| { + let assert_eq = |l: PyObject, r: &PyAny| { + assert!(l.as_ref(py).eq(r).unwrap()); + }; + + assert_eq( + Duration::new(0, 0).to_object(py), + new_timedelta(py, 0, 0, 0), + ); + assert_eq( + Duration::new(86400, 0).to_object(py), + new_timedelta(py, 1, 0, 0), + ); + assert_eq( + Duration::new(1, 0).to_object(py), + new_timedelta(py, 0, 1, 0), + ); + assert_eq( + Duration::new(0, 1_000).to_object(py), + new_timedelta(py, 0, 0, 1), + ); + assert_eq( + Duration::new(0, 1).to_object(py), + new_timedelta(py, 0, 0, 0), + ); + assert_eq( + Duration::new(86401, 1_000).to_object(py), + new_timedelta(py, 1, 1, 1), + ); + assert_eq( + Duration::new(86399999999999, 999999000).to_object(py), + timedelta_class(py).getattr("max").unwrap(), + ); + }); + } + + #[test] + fn test_duration_topyobject_overflow() { + Python::with_gil(|py| { + assert!(panic::catch_unwind(|| Duration::MAX.to_object(py)).is_err()); + }) + } + + #[test] + fn test_time_frompyobject() { + Python::with_gil(|py| { + assert_eq!( + new_datetime(py, 1970, 1, 1, 0, 0, 0, 0) + .extract::() + .unwrap(), + UNIX_EPOCH + ); + assert_eq!( + new_datetime(py, 2020, 2, 3, 4, 5, 6, 7) + .extract::() + .unwrap(), + UNIX_EPOCH + .checked_add(Duration::new(1580702706, 7000)) + .unwrap() + ); + assert_eq!( + max_datetime(py).extract::().unwrap(), + UNIX_EPOCH + .checked_add(Duration::new(253402300799, 999999000)) + .unwrap() + ); + }); + } + + #[test] + fn test_time_frompyobject_before_epoch() { + Python::with_gil(|py| { + assert_eq!( + new_datetime(py, 1950, 1, 1, 0, 0, 0, 0) + .extract::() + .unwrap_err() + .to_string(), + "ValueError: It is not possible to convert a negative timedelta to a Rust Duration" + ); + }) + } + + #[test] + fn test_time_topyobject() { + Python::with_gil(|py| { + let assert_eq = |l: PyObject, r: &PyAny| { + assert!(l.as_ref(py).eq(r).unwrap()); + }; + + assert_eq( + UNIX_EPOCH + .checked_add(Duration::new(1580702706, 7123)) + .unwrap() + .into_py(py), + new_datetime(py, 2020, 2, 3, 4, 5, 6, 7), + ); + assert_eq( + UNIX_EPOCH + .checked_add(Duration::new(253402300799, 999999000)) + .unwrap() + .into_py(py), + max_datetime(py), + ); + }); + } + + #[allow(clippy::too_many_arguments)] + fn new_datetime( + py: Python<'_>, + year: i32, + month: u8, + day: u8, + hour: u8, + minute: u8, + second: u8, + microsecond: u32, + ) -> &PyAny { + datetime_class(py) + .call1(( + year, + month, + day, + hour, + minute, + second, + microsecond, + tz_utc(py), + )) + .unwrap() + } + + fn max_datetime(py: Python<'_>) -> &PyAny { + let naive_max = datetime_class(py).getattr("max").unwrap(); + let kargs = PyDict::new(py); + kargs.set_item("tzinfo", tz_utc(py)).unwrap(); + naive_max.call_method("replace", (), Some(kargs)).unwrap() + } + + #[test] + fn test_time_topyobject_overflow() { + let big_system_time = UNIX_EPOCH + .checked_add(Duration::new(300000000000, 0)) + .unwrap(); + Python::with_gil(|py| { + assert!(panic::catch_unwind(|| big_system_time.into_py(py)).is_err()); + }) + } + + fn tz_utc(py: Python<'_>) -> &PyAny { + py.import("datetime") + .unwrap() + .getattr("timezone") + .unwrap() + .getattr("utc") + .unwrap() + } + + fn new_timedelta(py: Python<'_>, days: i32, seconds: i32, microseconds: i32) -> &PyAny { + timedelta_class(py) + .call1((days, seconds, microseconds)) + .unwrap() + } + + fn datetime_class(py: Python<'_>) -> &PyAny { + py.import("datetime").unwrap().getattr("datetime").unwrap() + } + + fn timedelta_class(py: Python<'_>) -> &PyAny { + py.import("datetime").unwrap().getattr("timedelta").unwrap() + } +}