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 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(); let microseconds = u32::try_from(microseconds).unwrap(); let total_seconds = days * SECONDS_PER_DAY + seconds; let nanoseconds = microseconds.checked_mul(1_000).unwrap(); 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
99impl 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}