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//! ```
43use crate::exceptions::{PyTypeError, PyUserWarning, PyValueError};
44use crate::types::{
45 timezone_utc, PyDate, PyDateAccess, PyDateTime, PyDelta, PyDeltaAccess, PyTime, PyTimeAccess,
46 PyTzInfo, PyTzInfoAccess, PyUnicode,
47};
48use crate::{FromPyObject, IntoPy, PyAny, PyErr, PyObject, PyResult, Python, ToPyObject};
49use chrono::offset::{FixedOffset, Utc};
50use chrono::{
51 DateTime, Datelike, Duration, NaiveDate, NaiveDateTime, NaiveTime, Offset, TimeZone, Timelike,
52};
53use pyo3_ffi::{PyDateTime_IMPORT, PyTimeZone_FromOffset};
54
55impl 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
84impl IntoPy<PyObject> for Duration {
85 fn into_py(self, py: Python<'_>) -> PyObject {
86 ToPyObject::to_object(&self, py)
87 }
88}
89
90impl 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
103impl ToPyObject for NaiveDate {
104 fn to_object(&self, py: Python<'_>) -> PyObject {
105 (*self).into_py(py)
106 }
107}
108
109impl 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
118impl 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
125impl ToPyObject for NaiveTime {
126 fn to_object(&self, py: Python<'_>) -> PyObject {
127 (*self).into_py(py)
128 }
129}
130
131impl 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
148impl 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
155impl 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
163impl IntoPy<PyObject> for NaiveDateTime {
164 fn into_py(self, py: Python<'_>) -> PyObject {
165 ToPyObject::to_object(&self, py)
166 }
167}
168
169impl 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
184impl<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
196impl<Tz: TimeZone> IntoPy<PyObject> for DateTime<Tz> {
197 fn into_py(self, py: Python<'_>) -> PyObject {
198 ToPyObject::to_object(&self, py)
199 }
200}
201
202impl 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
218impl 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
234fn 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
243impl 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
252impl IntoPy<PyObject> for FixedOffset {
253 fn into_py(self, py: Python<'_>) -> PyObject {
254 ToPyObject::to_object(&self, py)
255 }
256}
257
258impl 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
291impl ToPyObject for Utc {
292 fn to_object(&self, py: Python<'_>) -> PyObject {
293 timezone_utc(py).to_object(py)
294 }
295}
296
297impl IntoPy<PyObject> for Utc {
298 fn into_py(self, py: Python<'_>) -> PyObject {
299 ToPyObject::to_object(&self, py)
300 }
301}
302
303impl 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
315struct DateArgs {
316 year: i32,
317 month: u8,
318 day: u8,
319}
320
321impl 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
331struct TimeArgs {
332 hour: u8,
333 min: u8,
334 sec: u8,
335 micro: u32,
336 truncated_leap_second: bool,
337}
338
339impl 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
355fn 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
375fn 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
387fn 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
396fn 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)]
407mod 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