Skip to content

Commit

Permalink
Adds conversion between SystemTime and datetime
Browse files Browse the repository at this point in the history
  • Loading branch information
Tpt committed Jan 24, 2024
1 parent f449fc0 commit 790f2cf
Show file tree
Hide file tree
Showing 4 changed files with 192 additions and 1 deletion.
2 changes: 1 addition & 1 deletion guide/src/conversions/tables.md
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ The table below contains the Python type and the corresponding function argument
| `type` | - | `&PyType` |
| `module` | - | `&PyModule` |
| `collections.abc.Buffer` | - | `PyBuffer<T>` |
| `datetime.datetime` | - | `&PyDateTime` |
| `datetime.datetime` | `SystemTime` | `&PyDateTime` |
| `datetime.date` | - | `&PyDate` |
| `datetime.time` | - | `&PyTime` |
| `datetime.tzinfo` | - | `&PyTzInfo` |
Expand Down
1 change: 1 addition & 0 deletions newsfragments/3736.added.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Conversion between `std::time::SystemTime` and `datetime.datetime`
1 change: 1 addition & 0 deletions src/conversions/std/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,4 +8,5 @@ mod path;
mod set;
mod slice;
mod string;
mod time;
mod vec;
189 changes: 189 additions & 0 deletions src/conversions/std/time.rs
Original file line number Diff line number Diff line change
@@ -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<Self> {
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<PyObject> for SystemTime {
fn into_py(self, py: Python<'_>) -> PyObject {
self.to_object(py)
}
}

fn unix_epoch_py(py: Python<'_>) -> &PyObject {
static UNIX_EPOCH: GILOnceCell<PyObject> = 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::<SystemTime>()
.unwrap(),
UNIX_EPOCH
);
assert_eq!(
new_datetime(py, 2020, 2, 3, 4, 5, 6, 7)
.extract::<SystemTime>()
.unwrap(),
UNIX_EPOCH
.checked_add(Duration::new(1580702706, 7000))
.unwrap()
);
assert_eq!(
max_datetime(py).extract::<SystemTime>().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::<SystemTime>()
.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()
}
}

0 comments on commit 790f2cf

Please sign in to comment.