pyo3/conversions/std/
time.rs

1use crate::conversion::IntoPyObject;
2use crate::exceptions::{PyOverflowError, PyValueError};
3use crate::sync::GILOnceCell;
4use crate::types::any::PyAnyMethods;
5#[cfg(Py_LIMITED_API)]
6use crate::types::PyType;
7#[cfg(not(Py_LIMITED_API))]
8use crate::types::{timezone_utc, PyDateTime, PyDelta, PyDeltaAccess};
9#[cfg(Py_LIMITED_API)]
10use crate::Py;
11use crate::{intern, Bound, FromPyObject, PyAny, PyErr, PyObject, PyResult, Python};
12use std::time::{Duration, SystemTime, UNIX_EPOCH};
13
14const SECONDS_PER_DAY: u64 = 24 * 60 * 60;
15
16impl FromPyObject<'_> for Duration {
17    fn extract_bound(obj: &Bound<'_, PyAny>) -> PyResult<Self> {
18        #[cfg(not(Py_LIMITED_API))]
19        let (days, seconds, microseconds) = {
20            let delta = obj.downcast::<PyDelta>()?;
21            (
22                delta.get_days(),
23                delta.get_seconds(),
24                delta.get_microseconds(),
25            )
26        };
27        #[cfg(Py_LIMITED_API)]
28        let (days, seconds, microseconds): (i32, i32, i32) = {
29            (
30                obj.getattr(intern!(obj.py(), "days"))?.extract()?,
31                obj.getattr(intern!(obj.py(), "seconds"))?.extract()?,
32                obj.getattr(intern!(obj.py(), "microseconds"))?.extract()?,
33            )
34        };
35
36        // We cast
37        let days = u64::try_from(days).map_err(|_| {
38            PyValueError::new_err(
39                "It is not possible to convert a negative timedelta to a Rust Duration",
40            )
41        })?;
42        let seconds = u64::try_from(seconds).unwrap(); // 0 <= seconds < 3600*24
43        let microseconds = u32::try_from(microseconds).unwrap(); // 0 <= microseconds < 1000000
44
45        // We convert
46        let total_seconds = days * SECONDS_PER_DAY + seconds; // We casted from i32, this can't overflow
47        let nanoseconds = microseconds.checked_mul(1_000).unwrap(); // 0 <= microseconds < 1000000
48
49        Ok(Duration::new(total_seconds, nanoseconds))
50    }
51}
52
53impl<'py> IntoPyObject<'py> for Duration {
54    #[cfg(not(Py_LIMITED_API))]
55    type Target = PyDelta;
56    #[cfg(Py_LIMITED_API)]
57    type Target = PyAny;
58    type Output = Bound<'py, Self::Target>;
59    type Error = PyErr;
60
61    fn into_pyobject(self, py: Python<'py>) -> Result<Self::Output, Self::Error> {
62        let days = self.as_secs() / SECONDS_PER_DAY;
63        let seconds = self.as_secs() % SECONDS_PER_DAY;
64        let microseconds = self.subsec_micros();
65
66        #[cfg(not(Py_LIMITED_API))]
67        {
68            PyDelta::new(
69                py,
70                days.try_into()?,
71                seconds.try_into().unwrap(),
72                microseconds.try_into().unwrap(),
73                false,
74            )
75        }
76        #[cfg(Py_LIMITED_API)]
77        {
78            static TIMEDELTA: GILOnceCell<Py<PyType>> = GILOnceCell::new();
79            TIMEDELTA
80                .import(py, "datetime", "timedelta")?
81                .call1((days, seconds, microseconds))
82        }
83    }
84}
85
86impl<'py> IntoPyObject<'py> for &Duration {
87    #[cfg(not(Py_LIMITED_API))]
88    type Target = PyDelta;
89    #[cfg(Py_LIMITED_API)]
90    type Target = PyAny;
91    type Output = Bound<'py, Self::Target>;
92    type Error = PyErr;
93
94    #[inline]
95    fn into_pyobject(self, py: Python<'py>) -> Result<Self::Output, Self::Error> {
96        (*self).into_pyobject(py)
97    }
98}
99
100// Conversions between SystemTime and datetime do not rely on the floating point timestamp of the
101// timestamp/fromtimestamp APIs to avoid possible precision loss but goes through the
102// timedelta/std::time::Duration types by taking for reference point the UNIX epoch.
103//
104// TODO: it might be nice to investigate using timestamps anyway, at least when the datetime is a safe range.
105
106impl FromPyObject<'_> for SystemTime {
107    fn extract_bound(obj: &Bound<'_, PyAny>) -> PyResult<Self> {
108        let duration_since_unix_epoch: Duration = obj
109            .call_method1(intern!(obj.py(), "__sub__"), (unix_epoch_py(obj.py())?,))?
110            .extract()?;
111        UNIX_EPOCH
112            .checked_add(duration_since_unix_epoch)
113            .ok_or_else(|| {
114                PyOverflowError::new_err("Overflow error when converting the time to Rust")
115            })
116    }
117}
118
119impl<'py> IntoPyObject<'py> for SystemTime {
120    type Target = PyAny;
121    type Output = Bound<'py, Self::Target>;
122    type Error = PyErr;
123
124    fn into_pyobject(self, py: Python<'py>) -> Result<Self::Output, Self::Error> {
125        let duration_since_unix_epoch =
126            self.duration_since(UNIX_EPOCH).unwrap().into_pyobject(py)?;
127        unix_epoch_py(py)?
128            .bind(py)
129            .call_method1(intern!(py, "__add__"), (duration_since_unix_epoch,))
130    }
131}
132
133impl<'py> IntoPyObject<'py> for &SystemTime {
134    type Target = PyAny;
135    type Output = Bound<'py, Self::Target>;
136    type Error = PyErr;
137
138    #[inline]
139    fn into_pyobject(self, py: Python<'py>) -> Result<Self::Output, Self::Error> {
140        (*self).into_pyobject(py)
141    }
142}
143
144fn unix_epoch_py(py: Python<'_>) -> PyResult<&PyObject> {
145    static UNIX_EPOCH: GILOnceCell<PyObject> = GILOnceCell::new();
146    UNIX_EPOCH.get_or_try_init(py, || {
147        #[cfg(not(Py_LIMITED_API))]
148        {
149            Ok::<_, PyErr>(
150                PyDateTime::new(py, 1970, 1, 1, 0, 0, 0, 0, Some(&timezone_utc(py)))?.into(),
151            )
152        }
153        #[cfg(Py_LIMITED_API)]
154        {
155            let datetime = py.import("datetime")?;
156            let utc = datetime.getattr("timezone")?.getattr("utc")?;
157            Ok::<_, PyErr>(
158                datetime
159                    .getattr("datetime")?
160                    .call1((1970, 1, 1, 0, 0, 0, 0, utc))
161                    .unwrap()
162                    .into(),
163            )
164        }
165    })
166}
167
168#[cfg(test)]
169mod tests {
170    use super::*;
171    use crate::types::PyDict;
172
173    #[test]
174    fn test_duration_frompyobject() {
175        Python::with_gil(|py| {
176            assert_eq!(
177                new_timedelta(py, 0, 0, 0).extract::<Duration>().unwrap(),
178                Duration::new(0, 0)
179            );
180            assert_eq!(
181                new_timedelta(py, 1, 0, 0).extract::<Duration>().unwrap(),
182                Duration::new(86400, 0)
183            );
184            assert_eq!(
185                new_timedelta(py, 0, 1, 0).extract::<Duration>().unwrap(),
186                Duration::new(1, 0)
187            );
188            assert_eq!(
189                new_timedelta(py, 0, 0, 1).extract::<Duration>().unwrap(),
190                Duration::new(0, 1_000)
191            );
192            assert_eq!(
193                new_timedelta(py, 1, 1, 1).extract::<Duration>().unwrap(),
194                Duration::new(86401, 1_000)
195            );
196            assert_eq!(
197                timedelta_class(py)
198                    .getattr("max")
199                    .unwrap()
200                    .extract::<Duration>()
201                    .unwrap(),
202                Duration::new(86399999999999, 999999000)
203            );
204        });
205    }
206
207    #[test]
208    fn test_duration_frompyobject_negative() {
209        Python::with_gil(|py| {
210            assert_eq!(
211                new_timedelta(py, 0, -1, 0)
212                    .extract::<Duration>()
213                    .unwrap_err()
214                    .to_string(),
215                "ValueError: It is not possible to convert a negative timedelta to a Rust Duration"
216            );
217        })
218    }
219
220    #[test]
221    fn test_duration_into_pyobject() {
222        Python::with_gil(|py| {
223            let assert_eq = |l: Bound<'_, PyAny>, r: Bound<'_, PyAny>| {
224                assert!(l.eq(r).unwrap());
225            };
226
227            assert_eq(
228                Duration::new(0, 0).into_pyobject(py).unwrap().into_any(),
229                new_timedelta(py, 0, 0, 0),
230            );
231            assert_eq(
232                Duration::new(86400, 0)
233                    .into_pyobject(py)
234                    .unwrap()
235                    .into_any(),
236                new_timedelta(py, 1, 0, 0),
237            );
238            assert_eq(
239                Duration::new(1, 0).into_pyobject(py).unwrap().into_any(),
240                new_timedelta(py, 0, 1, 0),
241            );
242            assert_eq(
243                Duration::new(0, 1_000)
244                    .into_pyobject(py)
245                    .unwrap()
246                    .into_any(),
247                new_timedelta(py, 0, 0, 1),
248            );
249            assert_eq(
250                Duration::new(0, 1).into_pyobject(py).unwrap().into_any(),
251                new_timedelta(py, 0, 0, 0),
252            );
253            assert_eq(
254                Duration::new(86401, 1_000)
255                    .into_pyobject(py)
256                    .unwrap()
257                    .into_any(),
258                new_timedelta(py, 1, 1, 1),
259            );
260            assert_eq(
261                Duration::new(86399999999999, 999999000)
262                    .into_pyobject(py)
263                    .unwrap()
264                    .into_any(),
265                timedelta_class(py).getattr("max").unwrap(),
266            );
267        });
268    }
269
270    #[test]
271    fn test_duration_into_pyobject_overflow() {
272        Python::with_gil(|py| {
273            assert!(Duration::MAX.into_pyobject(py).is_err());
274        })
275    }
276
277    #[test]
278    fn test_time_frompyobject() {
279        Python::with_gil(|py| {
280            assert_eq!(
281                new_datetime(py, 1970, 1, 1, 0, 0, 0, 0)
282                    .extract::<SystemTime>()
283                    .unwrap(),
284                UNIX_EPOCH
285            );
286            assert_eq!(
287                new_datetime(py, 2020, 2, 3, 4, 5, 6, 7)
288                    .extract::<SystemTime>()
289                    .unwrap(),
290                UNIX_EPOCH
291                    .checked_add(Duration::new(1580702706, 7000))
292                    .unwrap()
293            );
294            assert_eq!(
295                max_datetime(py).extract::<SystemTime>().unwrap(),
296                UNIX_EPOCH
297                    .checked_add(Duration::new(253402300799, 999999000))
298                    .unwrap()
299            );
300        });
301    }
302
303    #[test]
304    fn test_time_frompyobject_before_epoch() {
305        Python::with_gil(|py| {
306            assert_eq!(
307                new_datetime(py, 1950, 1, 1, 0, 0, 0, 0)
308                    .extract::<SystemTime>()
309                    .unwrap_err()
310                    .to_string(),
311                "ValueError: It is not possible to convert a negative timedelta to a Rust Duration"
312            );
313        })
314    }
315
316    #[test]
317    fn test_time_intopyobject() {
318        Python::with_gil(|py| {
319            let assert_eq = |l: Bound<'_, PyAny>, r: Bound<'_, PyAny>| {
320                assert!(l.eq(r).unwrap());
321            };
322
323            assert_eq(
324                UNIX_EPOCH
325                    .checked_add(Duration::new(1580702706, 7123))
326                    .unwrap()
327                    .into_pyobject(py)
328                    .unwrap(),
329                new_datetime(py, 2020, 2, 3, 4, 5, 6, 7),
330            );
331            assert_eq(
332                UNIX_EPOCH
333                    .checked_add(Duration::new(253402300799, 999999000))
334                    .unwrap()
335                    .into_pyobject(py)
336                    .unwrap(),
337                max_datetime(py),
338            );
339        });
340    }
341
342    #[allow(clippy::too_many_arguments)]
343    fn new_datetime(
344        py: Python<'_>,
345        year: i32,
346        month: u8,
347        day: u8,
348        hour: u8,
349        minute: u8,
350        second: u8,
351        microsecond: u32,
352    ) -> Bound<'_, PyAny> {
353        datetime_class(py)
354            .call1((
355                year,
356                month,
357                day,
358                hour,
359                minute,
360                second,
361                microsecond,
362                tz_utc(py),
363            ))
364            .unwrap()
365    }
366
367    fn max_datetime(py: Python<'_>) -> Bound<'_, PyAny> {
368        let naive_max = datetime_class(py).getattr("max").unwrap();
369        let kargs = PyDict::new(py);
370        kargs.set_item("tzinfo", tz_utc(py)).unwrap();
371        naive_max.call_method("replace", (), Some(&kargs)).unwrap()
372    }
373
374    #[test]
375    fn test_time_intopyobject_overflow() {
376        let big_system_time = UNIX_EPOCH
377            .checked_add(Duration::new(300000000000, 0))
378            .unwrap();
379        Python::with_gil(|py| {
380            assert!(big_system_time.into_pyobject(py).is_err());
381        })
382    }
383
384    fn tz_utc(py: Python<'_>) -> Bound<'_, PyAny> {
385        py.import("datetime")
386            .unwrap()
387            .getattr("timezone")
388            .unwrap()
389            .getattr("utc")
390            .unwrap()
391    }
392
393    fn new_timedelta(
394        py: Python<'_>,
395        days: i32,
396        seconds: i32,
397        microseconds: i32,
398    ) -> Bound<'_, PyAny> {
399        timedelta_class(py)
400            .call1((days, seconds, microseconds))
401            .unwrap()
402    }
403
404    fn datetime_class(py: Python<'_>) -> Bound<'_, PyAny> {
405        py.import("datetime").unwrap().getattr("datetime").unwrap()
406    }
407
408    fn timedelta_class(py: Python<'_>) -> Bound<'_, PyAny> {
409        py.import("datetime").unwrap().getattr("timedelta").unwrap()
410    }
411}
⚠️ Internal Docs ⚠️ Not Public API 👉 Official Docs Here