Skip to main content

pyo3/conversions/std/
time.rs

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