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 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(); 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))
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
92impl 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}