pyo3/conversions/std/
time.rs

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