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::{PyDateTime, PyDelta, PyTzInfo};
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            let utc = PyTzInfo::utc(py)?;
132            Ok::<_, PyErr>(PyDateTime::new(py, 1970, 1, 1, 0, 0, 0, 0, Some(&utc))?.into())
133        })?
134        .bind_borrowed(py))
135}
136
137#[cfg(test)]
138mod tests {
139    use super::*;
140    use crate::types::PyDict;
141
142    #[test]
143    fn test_duration_frompyobject() {
144        Python::with_gil(|py| {
145            assert_eq!(
146                new_timedelta(py, 0, 0, 0).extract::<Duration>().unwrap(),
147                Duration::new(0, 0)
148            );
149            assert_eq!(
150                new_timedelta(py, 1, 0, 0).extract::<Duration>().unwrap(),
151                Duration::new(86400, 0)
152            );
153            assert_eq!(
154                new_timedelta(py, 0, 1, 0).extract::<Duration>().unwrap(),
155                Duration::new(1, 0)
156            );
157            assert_eq!(
158                new_timedelta(py, 0, 0, 1).extract::<Duration>().unwrap(),
159                Duration::new(0, 1_000)
160            );
161            assert_eq!(
162                new_timedelta(py, 1, 1, 1).extract::<Duration>().unwrap(),
163                Duration::new(86401, 1_000)
164            );
165            assert_eq!(
166                timedelta_class(py)
167                    .getattr("max")
168                    .unwrap()
169                    .extract::<Duration>()
170                    .unwrap(),
171                Duration::new(86399999999999, 999999000)
172            );
173        });
174    }
175
176    #[test]
177    fn test_duration_frompyobject_negative() {
178        Python::with_gil(|py| {
179            assert_eq!(
180                new_timedelta(py, 0, -1, 0)
181                    .extract::<Duration>()
182                    .unwrap_err()
183                    .to_string(),
184                "ValueError: It is not possible to convert a negative timedelta to a Rust Duration"
185            );
186        })
187    }
188
189    #[test]
190    fn test_duration_into_pyobject() {
191        Python::with_gil(|py| {
192            let assert_eq = |l: Bound<'_, PyAny>, r: Bound<'_, PyAny>| {
193                assert!(l.eq(r).unwrap());
194            };
195
196            assert_eq(
197                Duration::new(0, 0).into_pyobject(py).unwrap().into_any(),
198                new_timedelta(py, 0, 0, 0),
199            );
200            assert_eq(
201                Duration::new(86400, 0)
202                    .into_pyobject(py)
203                    .unwrap()
204                    .into_any(),
205                new_timedelta(py, 1, 0, 0),
206            );
207            assert_eq(
208                Duration::new(1, 0).into_pyobject(py).unwrap().into_any(),
209                new_timedelta(py, 0, 1, 0),
210            );
211            assert_eq(
212                Duration::new(0, 1_000)
213                    .into_pyobject(py)
214                    .unwrap()
215                    .into_any(),
216                new_timedelta(py, 0, 0, 1),
217            );
218            assert_eq(
219                Duration::new(0, 1).into_pyobject(py).unwrap().into_any(),
220                new_timedelta(py, 0, 0, 0),
221            );
222            assert_eq(
223                Duration::new(86401, 1_000)
224                    .into_pyobject(py)
225                    .unwrap()
226                    .into_any(),
227                new_timedelta(py, 1, 1, 1),
228            );
229            assert_eq(
230                Duration::new(86399999999999, 999999000)
231                    .into_pyobject(py)
232                    .unwrap()
233                    .into_any(),
234                timedelta_class(py).getattr("max").unwrap(),
235            );
236        });
237    }
238
239    #[test]
240    fn test_duration_into_pyobject_overflow() {
241        Python::with_gil(|py| {
242            assert!(Duration::MAX.into_pyobject(py).is_err());
243        })
244    }
245
246    #[test]
247    fn test_time_frompyobject() {
248        Python::with_gil(|py| {
249            assert_eq!(
250                new_datetime(py, 1970, 1, 1, 0, 0, 0, 0)
251                    .extract::<SystemTime>()
252                    .unwrap(),
253                UNIX_EPOCH
254            );
255            assert_eq!(
256                new_datetime(py, 2020, 2, 3, 4, 5, 6, 7)
257                    .extract::<SystemTime>()
258                    .unwrap(),
259                UNIX_EPOCH
260                    .checked_add(Duration::new(1580702706, 7000))
261                    .unwrap()
262            );
263            assert_eq!(
264                max_datetime(py).extract::<SystemTime>().unwrap(),
265                UNIX_EPOCH
266                    .checked_add(Duration::new(253402300799, 999999000))
267                    .unwrap()
268            );
269        });
270    }
271
272    #[test]
273    fn test_time_frompyobject_before_epoch() {
274        Python::with_gil(|py| {
275            assert_eq!(
276                new_datetime(py, 1950, 1, 1, 0, 0, 0, 0)
277                    .extract::<SystemTime>()
278                    .unwrap_err()
279                    .to_string(),
280                "ValueError: It is not possible to convert a negative timedelta to a Rust Duration"
281            );
282        })
283    }
284
285    #[test]
286    fn test_time_intopyobject() {
287        Python::with_gil(|py| {
288            let assert_eq = |l: Bound<'_, PyDateTime>, r: Bound<'_, PyDateTime>| {
289                assert!(l.eq(r).unwrap());
290            };
291
292            assert_eq(
293                UNIX_EPOCH
294                    .checked_add(Duration::new(1580702706, 7123))
295                    .unwrap()
296                    .into_pyobject(py)
297                    .unwrap(),
298                new_datetime(py, 2020, 2, 3, 4, 5, 6, 7),
299            );
300            assert_eq(
301                UNIX_EPOCH
302                    .checked_add(Duration::new(253402300799, 999999000))
303                    .unwrap()
304                    .into_pyobject(py)
305                    .unwrap(),
306                max_datetime(py),
307            );
308        });
309    }
310
311    #[allow(clippy::too_many_arguments)]
312    fn new_datetime(
313        py: Python<'_>,
314        year: i32,
315        month: u8,
316        day: u8,
317        hour: u8,
318        minute: u8,
319        second: u8,
320        microsecond: u32,
321    ) -> Bound<'_, PyDateTime> {
322        let utc = PyTzInfo::utc(py).unwrap();
323        PyDateTime::new(
324            py,
325            year,
326            month,
327            day,
328            hour,
329            minute,
330            second,
331            microsecond,
332            Some(&utc),
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
341            .set_item("tzinfo", PyTzInfo::utc(py).unwrap())
342            .unwrap();
343        naive_max
344            .call_method("replace", (), Some(&kargs))
345            .unwrap()
346            .downcast_into()
347            .unwrap()
348    }
349
350    #[test]
351    fn test_time_intopyobject_overflow() {
352        let big_system_time = UNIX_EPOCH
353            .checked_add(Duration::new(300000000000, 0))
354            .unwrap();
355        Python::with_gil(|py| {
356            assert!(big_system_time.into_pyobject(py).is_err());
357        })
358    }
359
360    fn new_timedelta(
361        py: Python<'_>,
362        days: i32,
363        seconds: i32,
364        microseconds: i32,
365    ) -> Bound<'_, PyAny> {
366        timedelta_class(py)
367            .call1((days, seconds, microseconds))
368            .unwrap()
369    }
370
371    fn datetime_class(py: Python<'_>) -> Bound<'_, PyAny> {
372        py.import("datetime").unwrap().getattr("datetime").unwrap()
373    }
374
375    fn timedelta_class(py: Python<'_>) -> Bound<'_, PyAny> {
376        py.import("datetime").unwrap().getattr("timedelta").unwrap()
377    }
378}
⚠️ Internal Docs ⚠️ Not Public API 👉 Official Docs Here