pyo3/conversions/std/
time.rs

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