diff --git a/guide/src/conversions/tables.md b/guide/src/conversions/tables.md index f9d716b324d..6b7c3bfbb80 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 00000000000..0d3a4a08c1a --- /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/mod.rs b/src/conversions/std/mod.rs index ebe1c955cc6..98914edc662 100644 --- a/src/conversions/std/mod.rs +++ b/src/conversions/std/mod.rs @@ -8,4 +8,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 00000000000..303c628fa43 --- /dev/null +++ b/src/conversions/std/time.rs @@ -0,0 +1,189 @@ +//! Conversions here do not rely on the floating point timestamp of the timestamp/fromtimestamp APIs +//! to avoid loosing precision but goes through the timedelta/std::time::Duration types by taking for +//! reference point the UNIX epoch. + +use crate::exceptions::PyOverflowError; +use crate::sync::GILOnceCell; +#[cfg(not(Py_LIMITED_API))] +use crate::types::{timezone_utc, PyDateTime}; +use crate::{intern, FromPyObject, IntoPy, PyAny, PyErr, PyObject, PyResult, Python, ToPyObject}; +use std::time::{Duration, SystemTime, UNIX_EPOCH}; + +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_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_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_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_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 datetime_class(py: Python<'_>) -> &PyAny { + py.import("datetime").unwrap().getattr("datetime").unwrap() + } +}