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