1 | #![cfg (all(feature = "chrono" , not(Py_LIMITED_API)))] |
2 | |
3 | //! Conversions to and from [chrono](https://docs.rs/chrono/)’s `Duration`, |
4 | //! `NaiveDate`, `NaiveTime`, `DateTime<Tz>`, `FixedOffset`, and `Utc`. |
5 | //! |
6 | //! Unavailable with the `abi3` feature. |
7 | //! |
8 | //! # Setup |
9 | //! |
10 | //! To use this feature, add this to your **`Cargo.toml`**: |
11 | //! |
12 | //! ```toml |
13 | //! [dependencies] |
14 | //! # change * to the latest versions |
15 | //! pyo3 = { version = "*", features = ["chrono"] } |
16 | //! chrono = "0.4" |
17 | #![doc = concat!("pyo3 = { version = \"" , env!("CARGO_PKG_VERSION" ), " \", features = [ \"chrono \"] }" )] |
18 | //! ``` |
19 | //! |
20 | //! Note that you must use compatible versions of chrono and PyO3. |
21 | //! The required chrono version may vary based on the version of PyO3. |
22 | //! |
23 | //! # Example: Convert a `PyDateTime` to chrono's `DateTime<Utc>` |
24 | //! |
25 | //! ```rust |
26 | //! use chrono::{Utc, DateTime}; |
27 | //! use pyo3::{Python, ToPyObject, types::PyDateTime}; |
28 | //! |
29 | //! fn main() { |
30 | //! pyo3::prepare_freethreaded_python(); |
31 | //! Python::with_gil(|py| { |
32 | //! // Create an UTC datetime in python |
33 | //! let py_tz = Utc.to_object(py); |
34 | //! let py_tz = py_tz.downcast(py).unwrap(); |
35 | //! let py_datetime = PyDateTime::new(py, 2022, 1, 1, 12, 0, 0, 0, Some(py_tz)).unwrap(); |
36 | //! println!("PyDateTime: {}" , py_datetime); |
37 | //! // Now convert it to chrono's DateTime<Utc> |
38 | //! let chrono_datetime: DateTime<Utc> = py_datetime.extract().unwrap(); |
39 | //! println!("DateTime<Utc>: {}" , chrono_datetime); |
40 | //! }); |
41 | //! } |
42 | //! ``` |
43 | use crate::exceptions::{PyTypeError, PyUserWarning, PyValueError}; |
44 | use crate::types::{ |
45 | timezone_utc, PyDate, PyDateAccess, PyDateTime, PyDelta, PyDeltaAccess, PyTime, PyTimeAccess, |
46 | PyTzInfo, PyTzInfoAccess, PyUnicode, |
47 | }; |
48 | use crate::{FromPyObject, IntoPy, PyAny, PyErr, PyObject, PyResult, Python, ToPyObject}; |
49 | use chrono::offset::{FixedOffset, Utc}; |
50 | use chrono::{ |
51 | DateTime, Datelike, Duration, NaiveDate, NaiveDateTime, NaiveTime, Offset, TimeZone, Timelike, |
52 | }; |
53 | use pyo3_ffi::{PyDateTime_IMPORT, PyTimeZone_FromOffset}; |
54 | |
55 | impl ToPyObject for Duration { |
56 | fn to_object(&self, py: Python<'_>) -> PyObject { |
57 | // Total number of days |
58 | let days = self.num_days(); |
59 | // Remainder of seconds |
60 | let secs_dur = *self - Duration::days(days); |
61 | // .try_into() converts i64 to i32, but this should never overflow |
62 | // since it's at most the number of seconds per day |
63 | let secs = secs_dur.num_seconds().try_into().unwrap(); |
64 | // Fractional part of the microseconds |
65 | let micros = (secs_dur - Duration::seconds(secs_dur.num_seconds())) |
66 | .num_microseconds() |
67 | // This should never panic since we are just getting the fractional |
68 | // part of the total microseconds, which should never overflow. |
69 | .unwrap() |
70 | // Same for the conversion from i64 to i32 |
71 | .try_into() |
72 | .unwrap(); |
73 | |
74 | // We do not need to check i64 to i32 cast from rust because |
75 | // python will panic with OverflowError. |
76 | // We pass true as the `normalize` parameter since we'd need to do several checks here to |
77 | // avoid that, and it shouldn't have a big performance impact. |
78 | let delta = PyDelta::new(py, days.try_into().unwrap_or(i32::MAX), secs, micros, true) |
79 | .expect("failed to construct delta" ); |
80 | delta.into() |
81 | } |
82 | } |
83 | |
84 | impl IntoPy<PyObject> for Duration { |
85 | fn into_py(self, py: Python<'_>) -> PyObject { |
86 | ToPyObject::to_object(&self, py) |
87 | } |
88 | } |
89 | |
90 | impl FromPyObject<'_> for Duration { |
91 | fn extract(ob: &PyAny) -> PyResult<Duration> { |
92 | let delta: &PyDelta = ob.downcast()?; |
93 | // Python size are much lower than rust size so we do not need bound checks. |
94 | // 0 <= microseconds < 1000000 |
95 | // 0 <= seconds < 3600*24 |
96 | // -999999999 <= days <= 999999999 |
97 | Ok(Duration::days(delta.get_days().into()) |
98 | + Duration::seconds(delta.get_seconds().into()) |
99 | + Duration::microseconds(delta.get_microseconds().into())) |
100 | } |
101 | } |
102 | |
103 | impl ToPyObject for NaiveDate { |
104 | fn to_object(&self, py: Python<'_>) -> PyObject { |
105 | (*self).into_py(py) |
106 | } |
107 | } |
108 | |
109 | impl IntoPy<PyObject> for NaiveDate { |
110 | fn into_py(self, py: Python<'_>) -> PyObject { |
111 | let DateArgs { year: i32, month: u8, day: u8 } = self.into(); |
112 | PyDate&PyDate::new(py, year, month, day) |
113 | .expect(msg:"failed to construct date" ) |
114 | .into() |
115 | } |
116 | } |
117 | |
118 | impl FromPyObject<'_> for NaiveDate { |
119 | fn extract(ob: &PyAny) -> PyResult<NaiveDate> { |
120 | let date: &PyDate = ob.downcast()?; |
121 | py_date_to_naive_date(py_date:date) |
122 | } |
123 | } |
124 | |
125 | impl ToPyObject for NaiveTime { |
126 | fn to_object(&self, py: Python<'_>) -> PyObject { |
127 | (*self).into_py(py) |
128 | } |
129 | } |
130 | |
131 | impl IntoPy<PyObject> for NaiveTime { |
132 | fn into_py(self, py: Python<'_>) -> PyObject { |
133 | let TimeArgs { |
134 | hour: u8, |
135 | min: u8, |
136 | sec: u8, |
137 | micro: u32, |
138 | truncated_leap_second: bool, |
139 | } = self.into(); |
140 | let time: &PyTime = PyTime::new(py, hour, min, sec, micro, None).expect(msg:"Failed to construct time" ); |
141 | if truncated_leap_second { |
142 | warn_truncated_leap_second(obj:time); |
143 | } |
144 | time.into() |
145 | } |
146 | } |
147 | |
148 | impl FromPyObject<'_> for NaiveTime { |
149 | fn extract(ob: &PyAny) -> PyResult<NaiveTime> { |
150 | let time: &PyTime = ob.downcast()?; |
151 | py_time_to_naive_time(py_time:time) |
152 | } |
153 | } |
154 | |
155 | impl ToPyObject for NaiveDateTime { |
156 | fn to_object(&self, py: Python<'_>) -> PyObject { |
157 | naive_datetime_to_py_datetime&PyDateTime(py, self, None) |
158 | .expect(msg:"failed to construct datetime" ) |
159 | .into() |
160 | } |
161 | } |
162 | |
163 | impl IntoPy<PyObject> for NaiveDateTime { |
164 | fn into_py(self, py: Python<'_>) -> PyObject { |
165 | ToPyObject::to_object(&self, py) |
166 | } |
167 | } |
168 | |
169 | impl FromPyObject<'_> for NaiveDateTime { |
170 | fn extract(ob: &PyAny) -> PyResult<NaiveDateTime> { |
171 | let dt: &PyDateTime = ob.downcast()?; |
172 | // If the user tries to convert a timezone aware datetime into a naive one, |
173 | // we return a hard error. We could silently remove tzinfo, or assume local timezone |
174 | // and do a conversion, but better leave this decision to the user of the library. |
175 | if dt.get_tzinfo().is_some() { |
176 | return Err(PyTypeError::new_err(args:"expected a datetime without tzinfo" )); |
177 | } |
178 | |
179 | let dt: NaiveDateTime = NaiveDateTime::new(date:py_date_to_naive_date(dt)?, time:py_time_to_naive_time(py_time:dt)?); |
180 | Ok(dt) |
181 | } |
182 | } |
183 | |
184 | impl<Tz: TimeZone> ToPyObject for DateTime<Tz> { |
185 | fn to_object(&self, py: Python<'_>) -> PyObject { |
186 | // FIXME: convert to better timezone representation here than just convert to fixed offset |
187 | // See https://github.com/PyO3/pyo3/issues/3266 |
188 | let tz: Py = self.offset().fix().to_object(py); |
189 | let tz: &PyTzInfo = tz.downcast(py).unwrap(); |
190 | naive_datetime_to_py_datetime&PyDateTime(py, &self.naive_local(), Some(tz)) |
191 | .expect(msg:"failed to construct datetime" ) |
192 | .into() |
193 | } |
194 | } |
195 | |
196 | impl<Tz: TimeZone> IntoPy<PyObject> for DateTime<Tz> { |
197 | fn into_py(self, py: Python<'_>) -> PyObject { |
198 | ToPyObject::to_object(&self, py) |
199 | } |
200 | } |
201 | |
202 | impl FromPyObject<'_> for DateTime<FixedOffset> { |
203 | fn extract(ob: &PyAny) -> PyResult<DateTime<FixedOffset>> { |
204 | let dt: &PyDateTime = ob.downcast()?; |
205 | let tz: FixedOffset = if let Some(tzinfo: &PyTzInfo) = dt.get_tzinfo() { |
206 | tzinfo.extract()? |
207 | } else { |
208 | return Err(PyTypeError::new_err( |
209 | args:"expected a datetime with non-None tzinfo" , |
210 | )); |
211 | }; |
212 | let dt: NaiveDateTime = NaiveDateTime::new(date:py_date_to_naive_date(dt)?, time:py_time_to_naive_time(py_time:dt)?); |
213 | // `FixedOffset` cannot have ambiguities so we don't have to worry about DST folds and such |
214 | Ok(dt.and_local_timezone(tz).unwrap()) |
215 | } |
216 | } |
217 | |
218 | impl FromPyObject<'_> for DateTime<Utc> { |
219 | fn extract(ob: &PyAny) -> PyResult<DateTime<Utc>> { |
220 | let dt: &PyDateTime = ob.downcast()?; |
221 | let _: Utc = if let Some(tzinfo: &PyTzInfo) = dt.get_tzinfo() { |
222 | tzinfo.extract()? |
223 | } else { |
224 | return Err(PyTypeError::new_err( |
225 | args:"expected a datetime with non-None tzinfo" , |
226 | )); |
227 | }; |
228 | let dt: NaiveDateTime = NaiveDateTime::new(date:py_date_to_naive_date(dt)?, time:py_time_to_naive_time(py_time:dt)?); |
229 | Ok(dt.and_utc()) |
230 | } |
231 | } |
232 | |
233 | // Utility function used to convert PyDelta to timezone |
234 | fn py_timezone_from_offset<'a>(py: &Python<'a>, td: &PyDelta) -> &'a PyAny { |
235 | // Safety: py.from_owned_ptr needs the cast to be valid. |
236 | // Since we are forcing a &PyDelta as input, the cast should always be valid. |
237 | unsafe { |
238 | PyDateTime_IMPORT(); |
239 | py.from_owned_ptr(PyTimeZone_FromOffset(offset:td.as_ptr())) |
240 | } |
241 | } |
242 | |
243 | impl ToPyObject for FixedOffset { |
244 | fn to_object(&self, py: Python<'_>) -> PyObject { |
245 | let seconds_offset: i32 = self.local_minus_utc(); |
246 | let td: &PyDelta = |
247 | PyDelta::new(py, 0, seconds_offset, 0, true).expect(msg:"failed to construct timedelta" ); |
248 | py_timezone_from_offset(&py, td).into() |
249 | } |
250 | } |
251 | |
252 | impl IntoPy<PyObject> for FixedOffset { |
253 | fn into_py(self, py: Python<'_>) -> PyObject { |
254 | ToPyObject::to_object(&self, py) |
255 | } |
256 | } |
257 | |
258 | impl FromPyObject<'_> for FixedOffset { |
259 | /// Convert python tzinfo to rust [`FixedOffset`]. |
260 | /// |
261 | /// Note that the conversion will result in precision lost in microseconds as chrono offset |
262 | /// does not supports microseconds. |
263 | fn extract(ob: &PyAny) -> PyResult<FixedOffset> { |
264 | let py_tzinfo: &PyTzInfo = ob.downcast()?; |
265 | // Passing `ob.py().None()` (so Python's None) to the `utcoffset` function will only |
266 | // work for timezones defined as fixed offsets in Python. |
267 | // Any other timezone would require a datetime as the parameter, and return |
268 | // None if the datetime is not provided. |
269 | // Trying to convert None to a PyDelta in the next line will then fail. |
270 | let py_timedelta = py_tzinfo.call_method1("utcoffset" , (ob.py().None(),))?; |
271 | let py_timedelta: &PyDelta = py_timedelta.downcast().map_err(|_| { |
272 | PyTypeError::new_err(format!( |
273 | " {:?} is not a fixed offset timezone" , |
274 | py_tzinfo |
275 | .repr() |
276 | .unwrap_or_else(|_| PyUnicode::new(ob.py(), "repr failed" )) |
277 | )) |
278 | })?; |
279 | let days = py_timedelta.get_days() as i64; |
280 | let seconds = py_timedelta.get_seconds() as i64; |
281 | // Here we won't get microseconds as noted before |
282 | // let microseconds = py_timedelta.get_microseconds() as i64; |
283 | let total_seconds = Duration::days(days) + Duration::seconds(seconds); |
284 | // This cast is safe since the timedelta is limited to -24 hours and 24 hours. |
285 | let total_seconds = total_seconds.num_seconds() as i32; |
286 | FixedOffset::east_opt(total_seconds) |
287 | .ok_or_else(|| PyValueError::new_err("fixed offset out of bounds" )) |
288 | } |
289 | } |
290 | |
291 | impl ToPyObject for Utc { |
292 | fn to_object(&self, py: Python<'_>) -> PyObject { |
293 | timezone_utc(py).to_object(py) |
294 | } |
295 | } |
296 | |
297 | impl IntoPy<PyObject> for Utc { |
298 | fn into_py(self, py: Python<'_>) -> PyObject { |
299 | ToPyObject::to_object(&self, py) |
300 | } |
301 | } |
302 | |
303 | impl FromPyObject<'_> for Utc { |
304 | fn extract(ob: &PyAny) -> PyResult<Utc> { |
305 | let py_tzinfo: &PyTzInfo = ob.downcast()?; |
306 | let py_utc: &PyTzInfo = timezone_utc(ob.py()); |
307 | if py_tzinfo.eq(py_utc)? { |
308 | Ok(Utc) |
309 | } else { |
310 | Err(PyValueError::new_err(args:"expected datetime.timezone.utc" )) |
311 | } |
312 | } |
313 | } |
314 | |
315 | struct DateArgs { |
316 | year: i32, |
317 | month: u8, |
318 | day: u8, |
319 | } |
320 | |
321 | impl From<NaiveDate> for DateArgs { |
322 | fn from(value: NaiveDate) -> Self { |
323 | Self { |
324 | year: value.year(), |
325 | month: value.month() as u8, |
326 | day: value.day() as u8, |
327 | } |
328 | } |
329 | } |
330 | |
331 | struct TimeArgs { |
332 | hour: u8, |
333 | min: u8, |
334 | sec: u8, |
335 | micro: u32, |
336 | truncated_leap_second: bool, |
337 | } |
338 | |
339 | impl From<NaiveTime> for TimeArgs { |
340 | fn from(value: NaiveTime) -> Self { |
341 | let ns: u32 = value.nanosecond(); |
342 | let checked_sub: Option = ns.checked_sub(1_000_000_000); |
343 | let truncated_leap_second: bool = checked_sub.is_some(); |
344 | let micro: u32 = checked_sub.unwrap_or(default:ns) / 1000; |
345 | Self { |
346 | hour: value.hour() as u8, |
347 | min: value.minute() as u8, |
348 | sec: value.second() as u8, |
349 | micro, |
350 | truncated_leap_second, |
351 | } |
352 | } |
353 | } |
354 | |
355 | fn naive_datetime_to_py_datetime<'py>( |
356 | py: Python<'py>, |
357 | naive_datetime: &NaiveDateTime, |
358 | tzinfo: Option<&PyTzInfo>, |
359 | ) -> PyResult<&'py PyDateTime> { |
360 | let DateArgs { year: i32, month: u8, day: u8 } = naive_datetime.date().into(); |
361 | let TimeArgs { |
362 | hour: u8, |
363 | min: u8, |
364 | sec: u8, |
365 | micro: u32, |
366 | truncated_leap_second: bool, |
367 | } = naive_datetime.time().into(); |
368 | let datetime: &PyDateTime = PyDateTime::new(py, year, month, day, hour, minute:min, second:sec, microsecond:micro, tzinfo)?; |
369 | if truncated_leap_second { |
370 | warn_truncated_leap_second(obj:datetime); |
371 | } |
372 | Ok(datetime) |
373 | } |
374 | |
375 | fn warn_truncated_leap_second(obj: &PyAny) { |
376 | let py: Python<'_> = obj.py(); |
377 | if let Err(e: PyErr) = PyErr::warn( |
378 | py, |
379 | category:py.get_type::<PyUserWarning>(), |
380 | message:"ignored leap-second, `datetime` does not support leap-seconds" , |
381 | stacklevel:0, |
382 | ) { |
383 | e.write_unraisable(py, obj:Some(obj)) |
384 | }; |
385 | } |
386 | |
387 | fn py_date_to_naive_date(py_date: &impl PyDateAccess) -> PyResult<NaiveDate> { |
388 | NaiveDate::from_ymd_opt( |
389 | py_date.get_year(), |
390 | py_date.get_month().into(), |
391 | py_date.get_day().into(), |
392 | ) |
393 | .ok_or_else(|| PyValueError::new_err(args:"invalid or out-of-range date" )) |
394 | } |
395 | |
396 | fn py_time_to_naive_time(py_time: &impl PyTimeAccess) -> PyResult<NaiveTime> { |
397 | NaiveTime::from_hms_micro_opt( |
398 | py_time.get_hour().into(), |
399 | py_time.get_minute().into(), |
400 | py_time.get_second().into(), |
401 | py_time.get_microsecond(), |
402 | ) |
403 | .ok_or_else(|| PyValueError::new_err(args:"invalid or out-of-range time" )) |
404 | } |
405 | |
406 | #[cfg (test)] |
407 | mod tests { |
408 | use std::{cmp::Ordering, panic}; |
409 | |
410 | use crate::{tests::common::CatchWarnings, PyTypeInfo}; |
411 | |
412 | use super::*; |
413 | |
414 | #[test ] |
415 | // Only Python>=3.9 has the zoneinfo package |
416 | // We skip the test on windows too since we'd need to install |
417 | // tzdata there to make this work. |
418 | #[cfg (all(Py_3_9, not(target_os = "windows" )))] |
419 | fn test_zoneinfo_is_not_fixed_offset() { |
420 | Python::with_gil(|py| { |
421 | let locals = crate::types::PyDict::new(py); |
422 | py.run( |
423 | "import zoneinfo; zi = zoneinfo.ZoneInfo('Europe/London')" , |
424 | None, |
425 | Some(locals), |
426 | ) |
427 | .unwrap(); |
428 | let result: PyResult<FixedOffset> = locals.get_item("zi" ).unwrap().unwrap().extract(); |
429 | assert!(result.is_err()); |
430 | let res = result.err().unwrap(); |
431 | // Also check the error message is what we expect |
432 | let msg = res.value(py).repr().unwrap().to_string(); |
433 | assert_eq!(msg, "TypeError(' \"zoneinfo.ZoneInfo(key= \\'Europe/London \\') \" is not a fixed offset timezone')" ); |
434 | }); |
435 | } |
436 | |
437 | #[test ] |
438 | fn test_timezone_aware_to_naive_fails() { |
439 | // Test that if a user tries to convert a python's timezone aware datetime into a naive |
440 | // one, the conversion fails. |
441 | Python::with_gil(|py| { |
442 | let utc = timezone_utc(py); |
443 | let py_datetime = PyDateTime::new(py, 2022, 1, 1, 1, 0, 0, 0, Some(utc)).unwrap(); |
444 | // Now test that converting a PyDateTime with tzinfo to a NaiveDateTime fails |
445 | let res: PyResult<NaiveDateTime> = py_datetime.extract(); |
446 | assert!(res.is_err()); |
447 | let res = res.err().unwrap(); |
448 | // Also check the error message is what we expect |
449 | let msg = res.value(py).repr().unwrap().to_string(); |
450 | assert_eq!(msg, "TypeError('expected a datetime without tzinfo')" ); |
451 | }); |
452 | } |
453 | |
454 | #[test ] |
455 | fn test_naive_to_timezone_aware_fails() { |
456 | // Test that if a user tries to convert a python's timezone aware datetime into a naive |
457 | // one, the conversion fails. |
458 | Python::with_gil(|py| { |
459 | let py_datetime = PyDateTime::new(py, 2022, 1, 1, 1, 0, 0, 0, None).unwrap(); |
460 | // Now test that converting a PyDateTime with tzinfo to a NaiveDateTime fails |
461 | let res: PyResult<DateTime<Utc>> = py_datetime.extract(); |
462 | assert!(res.is_err()); |
463 | let res = res.err().unwrap(); |
464 | // Also check the error message is what we expect |
465 | let msg = res.value(py).repr().unwrap().to_string(); |
466 | assert_eq!(msg, "TypeError('expected a datetime with non-None tzinfo')" ); |
467 | |
468 | // Now test that converting a PyDateTime with tzinfo to a NaiveDateTime fails |
469 | let res: PyResult<DateTime<FixedOffset>> = py_datetime.extract(); |
470 | assert!(res.is_err()); |
471 | let res = res.err().unwrap(); |
472 | // Also check the error message is what we expect |
473 | let msg = res.value(py).repr().unwrap().to_string(); |
474 | assert_eq!(msg, "TypeError('expected a datetime with non-None tzinfo')" ); |
475 | }); |
476 | } |
477 | |
478 | #[test ] |
479 | fn test_invalid_types_fail() { |
480 | // Test that if a user tries to convert a python's timezone aware datetime into a naive |
481 | // one, the conversion fails. |
482 | Python::with_gil(|py| { |
483 | let none = py.None().into_ref(py); |
484 | assert_eq!( |
485 | none.extract::<Duration>().unwrap_err().to_string(), |
486 | "TypeError: 'NoneType' object cannot be converted to 'PyDelta'" |
487 | ); |
488 | assert_eq!( |
489 | none.extract::<FixedOffset>().unwrap_err().to_string(), |
490 | "TypeError: 'NoneType' object cannot be converted to 'PyTzInfo'" |
491 | ); |
492 | assert_eq!( |
493 | none.extract::<Utc>().unwrap_err().to_string(), |
494 | "TypeError: 'NoneType' object cannot be converted to 'PyTzInfo'" |
495 | ); |
496 | assert_eq!( |
497 | none.extract::<NaiveTime>().unwrap_err().to_string(), |
498 | "TypeError: 'NoneType' object cannot be converted to 'PyTime'" |
499 | ); |
500 | assert_eq!( |
501 | none.extract::<NaiveDate>().unwrap_err().to_string(), |
502 | "TypeError: 'NoneType' object cannot be converted to 'PyDate'" |
503 | ); |
504 | assert_eq!( |
505 | none.extract::<NaiveDateTime>().unwrap_err().to_string(), |
506 | "TypeError: 'NoneType' object cannot be converted to 'PyDateTime'" |
507 | ); |
508 | assert_eq!( |
509 | none.extract::<DateTime<Utc>>().unwrap_err().to_string(), |
510 | "TypeError: 'NoneType' object cannot be converted to 'PyDateTime'" |
511 | ); |
512 | assert_eq!( |
513 | none.extract::<DateTime<FixedOffset>>() |
514 | .unwrap_err() |
515 | .to_string(), |
516 | "TypeError: 'NoneType' object cannot be converted to 'PyDateTime'" |
517 | ); |
518 | }); |
519 | } |
520 | |
521 | #[test ] |
522 | fn test_pyo3_timedelta_topyobject() { |
523 | // Utility function used to check different durations. |
524 | // The `name` parameter is used to identify the check in case of a failure. |
525 | let check = |name: &'static str, delta: Duration, py_days, py_seconds, py_ms| { |
526 | Python::with_gil(|py| { |
527 | let delta = delta.to_object(py); |
528 | let delta: &PyDelta = delta.extract(py).unwrap(); |
529 | let py_delta = PyDelta::new(py, py_days, py_seconds, py_ms, true).unwrap(); |
530 | assert!( |
531 | delta.eq(py_delta).unwrap(), |
532 | " {}: {} != {}" , |
533 | name, |
534 | delta, |
535 | py_delta |
536 | ); |
537 | }); |
538 | }; |
539 | |
540 | let delta = Duration::days(-1) + Duration::seconds(1) + Duration::microseconds(-10); |
541 | check("delta normalization" , delta, -1, 1, -10); |
542 | |
543 | // Check the minimum value allowed by PyDelta, which is different |
544 | // from the minimum value allowed in Duration. This should pass. |
545 | let delta = Duration::seconds(-86399999913600); // min |
546 | check("delta min value" , delta, -999999999, 0, 0); |
547 | |
548 | // Same, for max value |
549 | let delta = Duration::seconds(86399999999999) + Duration::nanoseconds(999999000); // max |
550 | check("delta max value" , delta, 999999999, 86399, 999999); |
551 | |
552 | // Also check that trying to convert an out of bound value panics. |
553 | Python::with_gil(|py| { |
554 | assert!(panic::catch_unwind(|| Duration::min_value().to_object(py)).is_err()); |
555 | assert!(panic::catch_unwind(|| Duration::max_value().to_object(py)).is_err()); |
556 | }); |
557 | } |
558 | |
559 | #[test ] |
560 | fn test_pyo3_timedelta_frompyobject() { |
561 | // Utility function used to check different durations. |
562 | // The `name` parameter is used to identify the check in case of a failure. |
563 | let check = |name: &'static str, delta: Duration, py_days, py_seconds, py_ms| { |
564 | Python::with_gil(|py| { |
565 | let py_delta = PyDelta::new(py, py_days, py_seconds, py_ms, true).unwrap(); |
566 | let py_delta: Duration = py_delta.extract().unwrap(); |
567 | assert_eq!(py_delta, delta, " {}: {} != {}" , name, py_delta, delta); |
568 | }) |
569 | }; |
570 | |
571 | // Check the minimum value allowed by PyDelta, which is different |
572 | // from the minimum value allowed in Duration. This should pass. |
573 | check( |
574 | "min py_delta value" , |
575 | Duration::seconds(-86399999913600), |
576 | -999999999, |
577 | 0, |
578 | 0, |
579 | ); |
580 | // Same, for max value |
581 | check( |
582 | "max py_delta value" , |
583 | Duration::seconds(86399999999999) + Duration::microseconds(999999), |
584 | 999999999, |
585 | 86399, |
586 | 999999, |
587 | ); |
588 | |
589 | // This check is to assert that we can't construct every possible Duration from a PyDelta |
590 | // since they have different bounds. |
591 | Python::with_gil(|py| { |
592 | let low_days: i32 = -1000000000; |
593 | // This is possible |
594 | assert!(panic::catch_unwind(|| Duration::days(low_days as i64)).is_ok()); |
595 | // This panics on PyDelta::new |
596 | assert!(panic::catch_unwind(|| { |
597 | let py_delta = PyDelta::new(py, low_days, 0, 0, true).unwrap(); |
598 | if let Ok(_duration) = py_delta.extract::<Duration>() { |
599 | // So we should never get here |
600 | } |
601 | }) |
602 | .is_err()); |
603 | |
604 | let high_days: i32 = 1000000000; |
605 | // This is possible |
606 | assert!(panic::catch_unwind(|| Duration::days(high_days as i64)).is_ok()); |
607 | // This panics on PyDelta::new |
608 | assert!(panic::catch_unwind(|| { |
609 | let py_delta = PyDelta::new(py, high_days, 0, 0, true).unwrap(); |
610 | if let Ok(_duration) = py_delta.extract::<Duration>() { |
611 | // So we should never get here |
612 | } |
613 | }) |
614 | .is_err()); |
615 | }); |
616 | } |
617 | |
618 | #[test ] |
619 | fn test_pyo3_date_topyobject() { |
620 | let eq_ymd = |name: &'static str, year, month, day| { |
621 | Python::with_gil(|py| { |
622 | let date = NaiveDate::from_ymd_opt(year, month, day) |
623 | .unwrap() |
624 | .to_object(py); |
625 | let date: &PyDate = date.extract(py).unwrap(); |
626 | let py_date = PyDate::new(py, year, month as u8, day as u8).unwrap(); |
627 | assert_eq!( |
628 | date.compare(py_date).unwrap(), |
629 | Ordering::Equal, |
630 | " {}: {} != {}" , |
631 | name, |
632 | date, |
633 | py_date |
634 | ); |
635 | }) |
636 | }; |
637 | |
638 | eq_ymd("past date" , 2012, 2, 29); |
639 | eq_ymd("min date" , 1, 1, 1); |
640 | eq_ymd("future date" , 3000, 6, 5); |
641 | eq_ymd("max date" , 9999, 12, 31); |
642 | } |
643 | |
644 | #[test ] |
645 | fn test_pyo3_date_frompyobject() { |
646 | let eq_ymd = |name: &'static str, year, month, day| { |
647 | Python::with_gil(|py| { |
648 | let py_date = PyDate::new(py, year, month as u8, day as u8).unwrap(); |
649 | let py_date: NaiveDate = py_date.extract().unwrap(); |
650 | let date = NaiveDate::from_ymd_opt(year, month, day).unwrap(); |
651 | assert_eq!(py_date, date, " {}: {} != {}" , name, date, py_date); |
652 | }) |
653 | }; |
654 | |
655 | eq_ymd("past date" , 2012, 2, 29); |
656 | eq_ymd("min date" , 1, 1, 1); |
657 | eq_ymd("future date" , 3000, 6, 5); |
658 | eq_ymd("max date" , 9999, 12, 31); |
659 | } |
660 | |
661 | #[test ] |
662 | fn test_pyo3_datetime_topyobject_utc() { |
663 | Python::with_gil(|py| { |
664 | let check_utc = |
665 | |name: &'static str, year, month, day, hour, minute, second, ms, py_ms| { |
666 | let datetime = NaiveDate::from_ymd_opt(year, month, day) |
667 | .unwrap() |
668 | .and_hms_micro_opt(hour, minute, second, ms) |
669 | .unwrap() |
670 | .and_utc(); |
671 | let datetime = datetime.to_object(py); |
672 | let datetime: &PyDateTime = datetime.extract(py).unwrap(); |
673 | let py_tz = timezone_utc(py); |
674 | let py_datetime = PyDateTime::new( |
675 | py, |
676 | year, |
677 | month as u8, |
678 | day as u8, |
679 | hour as u8, |
680 | minute as u8, |
681 | second as u8, |
682 | py_ms, |
683 | Some(py_tz), |
684 | ) |
685 | .unwrap(); |
686 | assert_eq!( |
687 | datetime.compare(py_datetime).unwrap(), |
688 | Ordering::Equal, |
689 | " {}: {} != {}" , |
690 | name, |
691 | datetime, |
692 | py_datetime |
693 | ); |
694 | }; |
695 | |
696 | check_utc("regular" , 2014, 5, 6, 7, 8, 9, 999_999, 999_999); |
697 | |
698 | assert_warnings!( |
699 | py, |
700 | check_utc("leap second" , 2014, 5, 6, 7, 8, 59, 1_999_999, 999_999), |
701 | [( |
702 | PyUserWarning, |
703 | "ignored leap-second, `datetime` does not support leap-seconds" |
704 | )] |
705 | ); |
706 | }) |
707 | } |
708 | |
709 | #[test ] |
710 | fn test_pyo3_datetime_topyobject_fixed_offset() { |
711 | Python::with_gil(|py| { |
712 | let check_fixed_offset = |
713 | |name: &'static str, year, month, day, hour, minute, second, ms, py_ms| { |
714 | let offset = FixedOffset::east_opt(3600).unwrap(); |
715 | let datetime = NaiveDate::from_ymd_opt(year, month, day) |
716 | .unwrap() |
717 | .and_hms_micro_opt(hour, minute, second, ms) |
718 | .unwrap() |
719 | .and_local_timezone(offset) |
720 | .unwrap(); |
721 | let datetime = datetime.to_object(py); |
722 | let datetime: &PyDateTime = datetime.extract(py).unwrap(); |
723 | let py_tz = offset.to_object(py); |
724 | let py_tz = py_tz.downcast(py).unwrap(); |
725 | let py_datetime = PyDateTime::new( |
726 | py, |
727 | year, |
728 | month as u8, |
729 | day as u8, |
730 | hour as u8, |
731 | minute as u8, |
732 | second as u8, |
733 | py_ms, |
734 | Some(py_tz), |
735 | ) |
736 | .unwrap(); |
737 | assert_eq!( |
738 | datetime.compare(py_datetime).unwrap(), |
739 | Ordering::Equal, |
740 | " {}: {} != {}" , |
741 | name, |
742 | datetime, |
743 | py_datetime |
744 | ); |
745 | }; |
746 | |
747 | check_fixed_offset("regular" , 2014, 5, 6, 7, 8, 9, 999_999, 999_999); |
748 | |
749 | assert_warnings!( |
750 | py, |
751 | check_fixed_offset("leap second" , 2014, 5, 6, 7, 8, 59, 1_999_999, 999_999), |
752 | [( |
753 | PyUserWarning, |
754 | "ignored leap-second, `datetime` does not support leap-seconds" |
755 | )] |
756 | ); |
757 | }) |
758 | } |
759 | |
760 | #[test ] |
761 | fn test_pyo3_datetime_frompyobject_utc() { |
762 | Python::with_gil(|py| { |
763 | let year = 2014; |
764 | let month = 5; |
765 | let day = 6; |
766 | let hour = 7; |
767 | let minute = 8; |
768 | let second = 9; |
769 | let micro = 999_999; |
770 | let py_tz = timezone_utc(py); |
771 | let py_datetime = PyDateTime::new( |
772 | py, |
773 | year, |
774 | month, |
775 | day, |
776 | hour, |
777 | minute, |
778 | second, |
779 | micro, |
780 | Some(py_tz), |
781 | ) |
782 | .unwrap(); |
783 | let py_datetime: DateTime<Utc> = py_datetime.extract().unwrap(); |
784 | let datetime = NaiveDate::from_ymd_opt(year, month.into(), day.into()) |
785 | .unwrap() |
786 | .and_hms_micro_opt(hour.into(), minute.into(), second.into(), micro) |
787 | .unwrap() |
788 | .and_utc(); |
789 | assert_eq!(py_datetime, datetime,); |
790 | }) |
791 | } |
792 | |
793 | #[test ] |
794 | fn test_pyo3_datetime_frompyobject_fixed_offset() { |
795 | Python::with_gil(|py| { |
796 | let year = 2014; |
797 | let month = 5; |
798 | let day = 6; |
799 | let hour = 7; |
800 | let minute = 8; |
801 | let second = 9; |
802 | let micro = 999_999; |
803 | let offset = FixedOffset::east_opt(3600).unwrap(); |
804 | let py_tz = offset.to_object(py); |
805 | let py_tz = py_tz.downcast(py).unwrap(); |
806 | let py_datetime = PyDateTime::new( |
807 | py, |
808 | year, |
809 | month, |
810 | day, |
811 | hour, |
812 | minute, |
813 | second, |
814 | micro, |
815 | Some(py_tz), |
816 | ) |
817 | .unwrap(); |
818 | let datetime_from_py: DateTime<FixedOffset> = py_datetime.extract().unwrap(); |
819 | let datetime = NaiveDate::from_ymd_opt(year, month.into(), day.into()) |
820 | .unwrap() |
821 | .and_hms_micro_opt(hour.into(), minute.into(), second.into(), micro) |
822 | .unwrap(); |
823 | let datetime = datetime.and_local_timezone(offset).unwrap(); |
824 | |
825 | assert_eq!(datetime_from_py, datetime); |
826 | assert!( |
827 | py_datetime.extract::<DateTime<Utc>>().is_err(), |
828 | "Extracting Utc from nonzero FixedOffset timezone will fail" |
829 | ); |
830 | |
831 | let utc = timezone_utc(py); |
832 | let py_datetime_utc = |
833 | PyDateTime::new(py, year, month, day, hour, minute, second, micro, Some(utc)) |
834 | .unwrap(); |
835 | assert!( |
836 | py_datetime_utc.extract::<DateTime<FixedOffset>>().is_ok(), |
837 | "Extracting FixedOffset from Utc timezone will succeed" |
838 | ); |
839 | }) |
840 | } |
841 | |
842 | #[test ] |
843 | fn test_pyo3_offset_fixed_topyobject() { |
844 | Python::with_gil(|py| { |
845 | // Chrono offset |
846 | let offset = FixedOffset::east_opt(3600).unwrap().to_object(py); |
847 | // Python timezone from timedelta |
848 | let td = PyDelta::new(py, 0, 3600, 0, true).unwrap(); |
849 | let py_timedelta = py_timezone_from_offset(&py, td); |
850 | // Should be equal |
851 | assert!(offset.as_ref(py).eq(py_timedelta).unwrap()); |
852 | |
853 | // Same but with negative values |
854 | let offset = FixedOffset::east_opt(-3600).unwrap().to_object(py); |
855 | let td = PyDelta::new(py, 0, -3600, 0, true).unwrap(); |
856 | let py_timedelta = py_timezone_from_offset(&py, td); |
857 | assert!(offset.as_ref(py).eq(py_timedelta).unwrap()); |
858 | }) |
859 | } |
860 | |
861 | #[test ] |
862 | fn test_pyo3_offset_fixed_frompyobject() { |
863 | Python::with_gil(|py| { |
864 | let py_timedelta = PyDelta::new(py, 0, 3600, 0, true).unwrap(); |
865 | let py_tzinfo = py_timezone_from_offset(&py, py_timedelta); |
866 | let offset: FixedOffset = py_tzinfo.extract().unwrap(); |
867 | assert_eq!(FixedOffset::east_opt(3600).unwrap(), offset); |
868 | }) |
869 | } |
870 | |
871 | #[test ] |
872 | fn test_pyo3_offset_utc_topyobject() { |
873 | Python::with_gil(|py| { |
874 | let utc = Utc.to_object(py); |
875 | let py_utc = timezone_utc(py); |
876 | assert!(utc.as_ref(py).is(py_utc)); |
877 | }) |
878 | } |
879 | |
880 | #[test ] |
881 | fn test_pyo3_offset_utc_frompyobject() { |
882 | Python::with_gil(|py| { |
883 | let py_utc = timezone_utc(py); |
884 | let py_utc: Utc = py_utc.extract().unwrap(); |
885 | assert_eq!(Utc, py_utc); |
886 | |
887 | let py_timedelta = PyDelta::new(py, 0, 0, 0, true).unwrap(); |
888 | let py_timezone_utc = py_timezone_from_offset(&py, py_timedelta); |
889 | let py_timezone_utc: Utc = py_timezone_utc.extract().unwrap(); |
890 | assert_eq!(Utc, py_timezone_utc); |
891 | |
892 | let py_timedelta = PyDelta::new(py, 0, 3600, 0, true).unwrap(); |
893 | let py_timezone = py_timezone_from_offset(&py, py_timedelta); |
894 | assert!(py_timezone.extract::<Utc>().is_err()); |
895 | }) |
896 | } |
897 | |
898 | #[test ] |
899 | fn test_pyo3_time_topyobject() { |
900 | Python::with_gil(|py| { |
901 | let check_time = |name: &'static str, hour, minute, second, ms, py_ms| { |
902 | let time = NaiveTime::from_hms_micro_opt(hour, minute, second, ms) |
903 | .unwrap() |
904 | .to_object(py); |
905 | let time: &PyTime = time.extract(py).unwrap(); |
906 | let py_time = |
907 | PyTime::new(py, hour as u8, minute as u8, second as u8, py_ms, None).unwrap(); |
908 | assert!( |
909 | time.eq(py_time).unwrap(), |
910 | " {}: {} != {}" , |
911 | name, |
912 | time, |
913 | py_time |
914 | ); |
915 | }; |
916 | |
917 | check_time("regular" , 3, 5, 7, 999_999, 999_999); |
918 | |
919 | assert_warnings!( |
920 | py, |
921 | check_time("leap second" , 3, 5, 59, 1_999_999, 999_999), |
922 | [( |
923 | PyUserWarning, |
924 | "ignored leap-second, `datetime` does not support leap-seconds" |
925 | )] |
926 | ); |
927 | }) |
928 | } |
929 | |
930 | #[test ] |
931 | fn test_pyo3_time_frompyobject() { |
932 | let hour = 3; |
933 | let minute = 5; |
934 | let second = 7; |
935 | let micro = 999_999; |
936 | Python::with_gil(|py| { |
937 | let py_time = |
938 | PyTime::new(py, hour as u8, minute as u8, second as u8, micro, None).unwrap(); |
939 | let py_time: NaiveTime = py_time.extract().unwrap(); |
940 | let time = NaiveTime::from_hms_micro_opt(hour, minute, second, micro).unwrap(); |
941 | assert_eq!(py_time, time); |
942 | }) |
943 | } |
944 | |
945 | #[cfg (all(test, not(target_arch = "wasm32" )))] |
946 | mod proptests { |
947 | use super::*; |
948 | use crate::types::IntoPyDict; |
949 | |
950 | use proptest::prelude::*; |
951 | |
952 | proptest! { |
953 | |
954 | // Range is limited to 1970 to 2038 due to windows limitations |
955 | #[test] |
956 | fn test_pyo3_offset_fixed_frompyobject_created_in_python(timestamp in 0..(i32::MAX as i64), timedelta in -86399i32..=86399i32) { |
957 | Python::with_gil(|py| { |
958 | |
959 | let globals = [("datetime" , py.import("datetime" ).unwrap())].into_py_dict(py); |
960 | let code = format!("datetime.datetime.fromtimestamp({}).replace(tzinfo=datetime.timezone(datetime.timedelta(seconds={})))" , timestamp, timedelta); |
961 | let t = py.eval(&code, Some(globals), None).unwrap(); |
962 | |
963 | // Get ISO 8601 string from python |
964 | let py_iso_str = t.call_method0("isoformat" ).unwrap(); |
965 | |
966 | // Get ISO 8601 string from rust |
967 | let t = t.extract::<DateTime<FixedOffset>>().unwrap(); |
968 | // Python doesn't print the seconds of the offset if they are 0 |
969 | let rust_iso_str = if timedelta % 60 == 0 { |
970 | t.format("%Y-%m-%dT%H:%M:%S%:z" ).to_string() |
971 | } else { |
972 | t.format("%Y-%m-%dT%H:%M:%S%::z" ).to_string() |
973 | }; |
974 | |
975 | // They should be equal |
976 | assert_eq!(py_iso_str.to_string(), rust_iso_str); |
977 | }) |
978 | } |
979 | |
980 | #[test] |
981 | fn test_duration_roundtrip(days in -999999999i64..=999999999i64) { |
982 | // Test roundtrip conversion rust->python->rust for all allowed |
983 | // python values of durations (from -999999999 to 999999999 days), |
984 | Python::with_gil(|py| { |
985 | let dur = Duration::days(days); |
986 | let py_delta = dur.into_py(py); |
987 | let roundtripped: Duration = py_delta.extract(py).expect("Round trip" ); |
988 | assert_eq!(dur, roundtripped); |
989 | }) |
990 | } |
991 | |
992 | #[test] |
993 | fn test_fixed_offset_roundtrip(secs in -86399i32..=86399i32) { |
994 | Python::with_gil(|py| { |
995 | let offset = FixedOffset::east_opt(secs).unwrap(); |
996 | let py_offset = offset.into_py(py); |
997 | let roundtripped: FixedOffset = py_offset.extract(py).expect("Round trip" ); |
998 | assert_eq!(offset, roundtripped); |
999 | }) |
1000 | } |
1001 | |
1002 | #[test] |
1003 | fn test_naive_date_roundtrip( |
1004 | year in 1i32..=9999i32, |
1005 | month in 1u32..=12u32, |
1006 | day in 1u32..=31u32 |
1007 | ) { |
1008 | // Test roundtrip conversion rust->python->rust for all allowed |
1009 | // python dates (from year 1 to year 9999) |
1010 | Python::with_gil(|py| { |
1011 | // We use to `from_ymd_opt` constructor so that we only test valid `NaiveDate`s. |
1012 | // This is to skip the test if we are creating an invalid date, like February 31. |
1013 | if let Some(date) = NaiveDate::from_ymd_opt(year, month, day) { |
1014 | let py_date = date.to_object(py); |
1015 | let roundtripped: NaiveDate = py_date.extract(py).expect("Round trip" ); |
1016 | assert_eq!(date, roundtripped); |
1017 | } |
1018 | }) |
1019 | } |
1020 | |
1021 | #[test] |
1022 | fn test_naive_time_roundtrip( |
1023 | hour in 0u32..=23u32, |
1024 | min in 0u32..=59u32, |
1025 | sec in 0u32..=59u32, |
1026 | micro in 0u32..=1_999_999u32 |
1027 | ) { |
1028 | // Test roundtrip conversion rust->python->rust for naive times. |
1029 | // Python time has a resolution of microseconds, so we only test |
1030 | // NaiveTimes with microseconds resolution, even if NaiveTime has nanosecond |
1031 | // resolution. |
1032 | Python::with_gil(|py| { |
1033 | if let Some(time) = NaiveTime::from_hms_micro_opt(hour, min, sec, micro) { |
1034 | // Wrap in CatchWarnings to avoid to_object firing warning for truncated leap second |
1035 | let py_time = CatchWarnings::enter(py, |_| Ok(time.to_object(py))).unwrap(); |
1036 | let roundtripped: NaiveTime = py_time.extract(py).expect("Round trip" ); |
1037 | // Leap seconds are not roundtripped |
1038 | let expected_roundtrip_time = micro.checked_sub(1_000_000).map(|micro| NaiveTime::from_hms_micro_opt(hour, min, sec, micro).unwrap()).unwrap_or(time); |
1039 | assert_eq!(expected_roundtrip_time, roundtripped); |
1040 | } |
1041 | }) |
1042 | } |
1043 | |
1044 | #[test] |
1045 | fn test_naive_datetime_roundtrip( |
1046 | year in 1i32..=9999i32, |
1047 | month in 1u32..=12u32, |
1048 | day in 1u32..=31u32, |
1049 | hour in 0u32..=24u32, |
1050 | min in 0u32..=60u32, |
1051 | sec in 0u32..=60u32, |
1052 | micro in 0u32..=999_999u32 |
1053 | ) { |
1054 | Python::with_gil(|py| { |
1055 | let date_opt = NaiveDate::from_ymd_opt(year, month, day); |
1056 | let time_opt = NaiveTime::from_hms_micro_opt(hour, min, sec, micro); |
1057 | if let (Some(date), Some(time)) = (date_opt, time_opt) { |
1058 | let dt = NaiveDateTime::new(date, time); |
1059 | let pydt = dt.to_object(py); |
1060 | let roundtripped: NaiveDateTime = pydt.extract(py).expect("Round trip" ); |
1061 | assert_eq!(dt, roundtripped); |
1062 | } |
1063 | }) |
1064 | } |
1065 | |
1066 | #[test] |
1067 | fn test_utc_datetime_roundtrip( |
1068 | year in 1i32..=9999i32, |
1069 | month in 1u32..=12u32, |
1070 | day in 1u32..=31u32, |
1071 | hour in 0u32..=23u32, |
1072 | min in 0u32..=59u32, |
1073 | sec in 0u32..=59u32, |
1074 | micro in 0u32..=1_999_999u32 |
1075 | ) { |
1076 | Python::with_gil(|py| { |
1077 | let date_opt = NaiveDate::from_ymd_opt(year, month, day); |
1078 | let time_opt = NaiveTime::from_hms_micro_opt(hour, min, sec, micro); |
1079 | if let (Some(date), Some(time)) = (date_opt, time_opt) { |
1080 | let dt: DateTime<Utc> = NaiveDateTime::new(date, time).and_utc(); |
1081 | // Wrap in CatchWarnings to avoid into_py firing warning for truncated leap second |
1082 | let py_dt = CatchWarnings::enter(py, |_| Ok(dt.into_py(py))).unwrap(); |
1083 | let roundtripped: DateTime<Utc> = py_dt.extract(py).expect("Round trip" ); |
1084 | // Leap seconds are not roundtripped |
1085 | let expected_roundtrip_time = micro.checked_sub(1_000_000).map(|micro| NaiveTime::from_hms_micro_opt(hour, min, sec, micro).unwrap()).unwrap_or(time); |
1086 | let expected_roundtrip_dt: DateTime<Utc> = NaiveDateTime::new(date, expected_roundtrip_time).and_utc(); |
1087 | assert_eq!(expected_roundtrip_dt, roundtripped); |
1088 | } |
1089 | }) |
1090 | } |
1091 | |
1092 | #[test] |
1093 | fn test_fixed_offset_datetime_roundtrip( |
1094 | year in 1i32..=9999i32, |
1095 | month in 1u32..=12u32, |
1096 | day in 1u32..=31u32, |
1097 | hour in 0u32..=23u32, |
1098 | min in 0u32..=59u32, |
1099 | sec in 0u32..=59u32, |
1100 | micro in 0u32..=1_999_999u32, |
1101 | offset_secs in -86399i32..=86399i32 |
1102 | ) { |
1103 | Python::with_gil(|py| { |
1104 | let date_opt = NaiveDate::from_ymd_opt(year, month, day); |
1105 | let time_opt = NaiveTime::from_hms_micro_opt(hour, min, sec, micro); |
1106 | let offset = FixedOffset::east_opt(offset_secs).unwrap(); |
1107 | if let (Some(date), Some(time)) = (date_opt, time_opt) { |
1108 | let dt: DateTime<FixedOffset> = NaiveDateTime::new(date, time).and_local_timezone(offset).unwrap(); |
1109 | // Wrap in CatchWarnings to avoid into_py firing warning for truncated leap second |
1110 | let py_dt = CatchWarnings::enter(py, |_| Ok(dt.into_py(py))).unwrap(); |
1111 | let roundtripped: DateTime<FixedOffset> = py_dt.extract(py).expect("Round trip" ); |
1112 | // Leap seconds are not roundtripped |
1113 | let expected_roundtrip_time = micro.checked_sub(1_000_000).map(|micro| NaiveTime::from_hms_micro_opt(hour, min, sec, micro).unwrap()).unwrap_or(time); |
1114 | let expected_roundtrip_dt: DateTime<FixedOffset> = NaiveDateTime::new(date, expected_roundtrip_time).and_local_timezone(offset).unwrap(); |
1115 | assert_eq!(expected_roundtrip_dt, roundtripped); |
1116 | } |
1117 | }) |
1118 | } |
1119 | } |
1120 | } |
1121 | } |
1122 | |