Skip to main content

pyo3/conversions/
chrono.rs

1#![cfg(feature = "chrono")]
2
3//! Conversions to and from [chrono](https://docs.rs/chrono/)’s `Duration`,
4//! `NaiveDate`, `NaiveTime`, `DateTime<Tz>`, `FixedOffset`, and `Utc`.
5//!
6//! # Setup
7//!
8//! To use this feature, add this to your **`Cargo.toml`**:
9//!
10//! ```toml
11//! [dependencies]
12//! chrono = "0.4"
13#![doc = concat!("pyo3 = { version = \"", env!("CARGO_PKG_VERSION"),  "\", features = [\"chrono\"] }")]
14//! ```
15//!
16//! Note that you must use compatible versions of chrono and PyO3.
17//! The required chrono version may vary based on the version of PyO3.
18//!
19//! # Example: Convert a `datetime.datetime` to chrono's `DateTime<Utc>`
20//!
21//! ```rust
22//! use chrono::{DateTime, Duration, TimeZone, Utc};
23//! use pyo3::{Python, PyResult, IntoPyObject, types::PyAnyMethods};
24//!
25//! fn main() -> PyResult<()> {
26//!     Python::initialize();
27//!     Python::attach(|py| {
28//!         // Build some chrono values
29//!         let chrono_datetime = Utc.with_ymd_and_hms(2022, 1, 1, 12, 0, 0).unwrap();
30//!         let chrono_duration = Duration::seconds(1);
31//!         // Convert them to Python
32//!         let py_datetime = chrono_datetime.into_pyobject(py)?;
33//!         let py_timedelta = chrono_duration.into_pyobject(py)?;
34//!         // Do an operation in Python
35//!         let py_sum = py_datetime.call_method1("__add__", (py_timedelta,))?;
36//!         // Convert back to Rust
37//!         let chrono_sum: DateTime<Utc> = py_sum.extract()?;
38//!         println!("DateTime<Utc>: {}", chrono_datetime);
39//!         Ok(())
40//!     })
41//! }
42//! ```
43
44use crate::conversion::{FromPyObjectOwned, IntoPyObject};
45use crate::exceptions::{PyTypeError, PyUserWarning, PyValueError};
46#[cfg(feature = "experimental-inspect")]
47use crate::inspect::PyStaticExpr;
48use crate::intern;
49use crate::types::PyNone;
50use crate::types::any::PyAnyMethods;
51use crate::types::{PyDate, PyDateTime, PyDelta, PyTime, PyTzInfo, PyTzInfoAccess};
52use crate::types::{PyDateAccess, PyDeltaAccess, PyTimeAccess};
53use crate::{Borrowed, Bound, FromPyObject, IntoPyObjectExt, PyAny, PyErr, PyResult, Python};
54#[cfg(feature = "chrono-local")]
55use crate::{
56    Py,
57    exceptions::PyRuntimeError,
58    sync::PyOnceLock,
59    types::{PyString, PyStringMethods},
60};
61#[cfg(feature = "experimental-inspect")]
62use crate::{PyTypeInfo, type_hint_identifier};
63#[cfg(feature = "chrono-local")]
64use chrono::Local;
65use chrono::offset::{FixedOffset, Utc};
66use chrono::{
67    DateTime, Datelike, Duration, LocalResult, NaiveDate, NaiveDateTime, NaiveTime, Offset,
68    TimeZone, Timelike,
69};
70
71impl<'py> IntoPyObject<'py> for Duration {
72    type Target = PyDelta;
73    type Output = Bound<'py, Self::Target>;
74    type Error = PyErr;
75
76    #[cfg(feature = "experimental-inspect")]
77    const OUTPUT_TYPE: PyStaticExpr = PyDelta::TYPE_HINT;
78
79    fn into_pyobject(self, py: Python<'py>) -> Result<Self::Output, Self::Error> {
80        // Total number of days
81        let days = self.num_days();
82        // Remainder of seconds
83        let secs_dur = self - Duration::days(days);
84        let secs = secs_dur.num_seconds();
85        // Fractional part of the microseconds
86        let micros = (secs_dur - Duration::seconds(secs_dur.num_seconds()))
87            .num_microseconds()
88            // This should never panic since we are just getting the fractional
89            // part of the total microseconds, which should never overflow.
90            .unwrap();
91        // We do not need to check the days i64 to i32 cast from rust because
92        // python will panic with OverflowError.
93        // We pass true as the `normalize` parameter since we'd need to do several checks here to
94        // avoid that, and it shouldn't have a big performance impact.
95        // The seconds and microseconds cast should never overflow since it's at most the number of seconds per day
96        PyDelta::new(
97            py,
98            days.try_into().unwrap_or(i32::MAX),
99            secs.try_into()?,
100            micros.try_into()?,
101            true,
102        )
103    }
104}
105
106impl<'py> IntoPyObject<'py> for &Duration {
107    type Target = PyDelta;
108    type Output = Bound<'py, Self::Target>;
109    type Error = PyErr;
110
111    #[cfg(feature = "experimental-inspect")]
112    const OUTPUT_TYPE: PyStaticExpr = Duration::OUTPUT_TYPE;
113
114    #[inline]
115    fn into_pyobject(self, py: Python<'py>) -> Result<Self::Output, Self::Error> {
116        (*self).into_pyobject(py)
117    }
118}
119
120impl FromPyObject<'_, '_> for Duration {
121    type Error = PyErr;
122
123    #[cfg(feature = "experimental-inspect")]
124    const INPUT_TYPE: PyStaticExpr = PyDelta::TYPE_HINT;
125
126    fn extract(ob: Borrowed<'_, '_, PyAny>) -> Result<Self, Self::Error> {
127        let delta = ob.cast::<PyDelta>()?;
128        // Python size are much lower than rust size so we do not need bound checks.
129        // 0 <= microseconds < 1000000
130        // 0 <= seconds < 3600*24
131        // -999999999 <= days <= 999999999
132        Ok(Duration::days(delta.get_days().into())
133            + Duration::seconds(delta.get_seconds().into())
134            + Duration::microseconds(delta.get_microseconds().into()))
135    }
136}
137
138impl<'py> IntoPyObject<'py> for NaiveDate {
139    type Target = PyDate;
140    type Output = Bound<'py, Self::Target>;
141    type Error = PyErr;
142
143    #[cfg(feature = "experimental-inspect")]
144    const OUTPUT_TYPE: PyStaticExpr = PyDate::TYPE_HINT;
145
146    fn into_pyobject(self, py: Python<'py>) -> Result<Self::Output, Self::Error> {
147        let DateArgs { year, month, day } = (&self).into();
148        PyDate::new(py, year, month, day)
149    }
150}
151
152impl<'py> IntoPyObject<'py> for &NaiveDate {
153    type Target = PyDate;
154    type Output = Bound<'py, Self::Target>;
155    type Error = PyErr;
156
157    #[cfg(feature = "experimental-inspect")]
158    const OUTPUT_TYPE: PyStaticExpr = NaiveDate::OUTPUT_TYPE;
159
160    #[inline]
161    fn into_pyobject(self, py: Python<'py>) -> Result<Self::Output, Self::Error> {
162        (*self).into_pyobject(py)
163    }
164}
165
166impl FromPyObject<'_, '_> for NaiveDate {
167    type Error = PyErr;
168
169    #[cfg(feature = "experimental-inspect")]
170    const INPUT_TYPE: PyStaticExpr = PyDate::TYPE_HINT;
171
172    fn extract(ob: Borrowed<'_, '_, PyAny>) -> Result<Self, Self::Error> {
173        let date = &*ob.cast::<PyDate>()?;
174        py_date_to_naive_date(date)
175    }
176}
177
178impl<'py> IntoPyObject<'py> for NaiveTime {
179    type Target = PyTime;
180    type Output = Bound<'py, Self::Target>;
181    type Error = PyErr;
182
183    #[cfg(feature = "experimental-inspect")]
184    const OUTPUT_TYPE: PyStaticExpr = PyTime::TYPE_HINT;
185
186    fn into_pyobject(self, py: Python<'py>) -> Result<Self::Output, Self::Error> {
187        let TimeArgs {
188            hour,
189            min,
190            sec,
191            micro,
192            truncated_leap_second,
193        } = (&self).into();
194
195        let time = PyTime::new(py, hour, min, sec, micro, None)?;
196
197        if truncated_leap_second {
198            warn_truncated_leap_second(&time);
199        }
200
201        Ok(time)
202    }
203}
204
205impl<'py> IntoPyObject<'py> for &NaiveTime {
206    type Target = PyTime;
207    type Output = Bound<'py, Self::Target>;
208    type Error = PyErr;
209
210    #[cfg(feature = "experimental-inspect")]
211    const OUTPUT_TYPE: PyStaticExpr = NaiveTime::OUTPUT_TYPE;
212
213    #[inline]
214    fn into_pyobject(self, py: Python<'py>) -> Result<Self::Output, Self::Error> {
215        (*self).into_pyobject(py)
216    }
217}
218
219impl FromPyObject<'_, '_> for NaiveTime {
220    type Error = PyErr;
221
222    #[cfg(feature = "experimental-inspect")]
223    const INPUT_TYPE: PyStaticExpr = PyTime::TYPE_HINT;
224
225    fn extract(ob: Borrowed<'_, '_, PyAny>) -> Result<Self, Self::Error> {
226        let time = &*ob.cast::<PyTime>()?;
227        py_time_to_naive_time(time)
228    }
229}
230
231impl<'py> IntoPyObject<'py> for NaiveDateTime {
232    type Target = PyDateTime;
233    type Output = Bound<'py, Self::Target>;
234    type Error = PyErr;
235
236    #[cfg(feature = "experimental-inspect")]
237    const OUTPUT_TYPE: PyStaticExpr = PyDateTime::TYPE_HINT;
238
239    fn into_pyobject(self, py: Python<'py>) -> Result<Self::Output, Self::Error> {
240        let DateArgs { year, month, day } = (&self.date()).into();
241        let TimeArgs {
242            hour,
243            min,
244            sec,
245            micro,
246            truncated_leap_second,
247        } = (&self.time()).into();
248
249        let datetime = PyDateTime::new(py, year, month, day, hour, min, sec, micro, None)?;
250
251        if truncated_leap_second {
252            warn_truncated_leap_second(&datetime);
253        }
254
255        Ok(datetime)
256    }
257}
258
259impl<'py> IntoPyObject<'py> for &NaiveDateTime {
260    type Target = PyDateTime;
261    type Output = Bound<'py, Self::Target>;
262    type Error = PyErr;
263
264    #[cfg(feature = "experimental-inspect")]
265    const OUTPUT_TYPE: PyStaticExpr = NaiveDateTime::OUTPUT_TYPE;
266
267    #[inline]
268    fn into_pyobject(self, py: Python<'py>) -> Result<Self::Output, Self::Error> {
269        (*self).into_pyobject(py)
270    }
271}
272
273impl FromPyObject<'_, '_> for NaiveDateTime {
274    type Error = PyErr;
275
276    #[cfg(feature = "experimental-inspect")]
277    const INPUT_TYPE: PyStaticExpr = PyDateTime::TYPE_HINT;
278
279    fn extract(dt: Borrowed<'_, '_, PyAny>) -> Result<Self, Self::Error> {
280        let dt = &*dt.cast::<PyDateTime>()?;
281
282        // If the user tries to convert a timezone aware datetime into a naive one,
283        // we return a hard error. We could silently remove tzinfo, or assume local timezone
284        // and do a conversion, but better leave this decision to the user of the library.
285        let has_tzinfo = dt.get_tzinfo().is_some();
286        if has_tzinfo {
287            return Err(PyTypeError::new_err("expected a datetime without tzinfo"));
288        }
289
290        let dt = NaiveDateTime::new(py_date_to_naive_date(dt)?, py_time_to_naive_time(dt)?);
291        Ok(dt)
292    }
293}
294
295impl<'py, Tz: TimeZone> IntoPyObject<'py> for DateTime<Tz>
296where
297    Tz: IntoPyObject<'py>,
298{
299    type Target = PyDateTime;
300    type Output = Bound<'py, Self::Target>;
301    type Error = PyErr;
302
303    #[cfg(feature = "experimental-inspect")]
304    const OUTPUT_TYPE: PyStaticExpr = <&DateTime<Tz>>::OUTPUT_TYPE;
305
306    #[inline]
307    fn into_pyobject(self, py: Python<'py>) -> Result<Self::Output, Self::Error> {
308        (&self).into_pyobject(py)
309    }
310}
311
312impl<'py, Tz: TimeZone> IntoPyObject<'py> for &DateTime<Tz>
313where
314    Tz: IntoPyObject<'py>,
315{
316    type Target = PyDateTime;
317    type Output = Bound<'py, Self::Target>;
318    type Error = PyErr;
319
320    #[cfg(feature = "experimental-inspect")]
321    const OUTPUT_TYPE: PyStaticExpr = PyDateTime::TYPE_HINT;
322
323    fn into_pyobject(self, py: Python<'py>) -> Result<Self::Output, Self::Error> {
324        let tz = self.timezone().into_bound_py_any(py)?.cast_into()?;
325
326        let DateArgs { year, month, day } = (&self.naive_local().date()).into();
327        let TimeArgs {
328            hour,
329            min,
330            sec,
331            micro,
332            truncated_leap_second,
333        } = (&self.naive_local().time()).into();
334
335        let fold = matches!(
336            self.timezone().offset_from_local_datetime(&self.naive_local()),
337            LocalResult::Ambiguous(_, latest) if self.offset().fix() == latest.fix()
338        );
339
340        let datetime = PyDateTime::new_with_fold(
341            py,
342            year,
343            month,
344            day,
345            hour,
346            min,
347            sec,
348            micro,
349            Some(&tz),
350            fold,
351        )?;
352
353        if truncated_leap_second {
354            warn_truncated_leap_second(&datetime);
355        }
356
357        Ok(datetime)
358    }
359}
360
361impl<'py, Tz> FromPyObject<'_, 'py> for DateTime<Tz>
362where
363    Tz: TimeZone + FromPyObjectOwned<'py>,
364{
365    type Error = PyErr;
366
367    #[cfg(feature = "experimental-inspect")]
368    const INPUT_TYPE: PyStaticExpr = PyDateTime::TYPE_HINT;
369
370    fn extract(dt: Borrowed<'_, 'py, PyAny>) -> Result<Self, Self::Error> {
371        let dt = &*dt.cast::<PyDateTime>()?;
372        let tzinfo = dt.get_tzinfo();
373
374        let tz = if let Some(tzinfo) = tzinfo {
375            tzinfo.extract().map_err(Into::into)?
376        } else {
377            // Special case: allow naive `datetime` objects for `DateTime<Local>`, interpreting them as local time.
378            #[cfg(feature = "chrono-local")]
379            if let Some(tz) = Tz::as_local_tz(crate::conversion::private::Token) {
380                return py_datetime_to_datetime_with_timezone(dt, tz);
381            }
382
383            return Err(PyTypeError::new_err(
384                "expected a datetime with non-None tzinfo",
385            ));
386        };
387
388        py_datetime_to_datetime_with_timezone(dt, tz)
389    }
390}
391
392impl<'py> IntoPyObject<'py> for FixedOffset {
393    type Target = PyTzInfo;
394    type Output = Bound<'py, Self::Target>;
395    type Error = PyErr;
396
397    #[cfg(feature = "experimental-inspect")]
398    const OUTPUT_TYPE: PyStaticExpr = type_hint_identifier!("datetime", "timezone");
399
400    fn into_pyobject(self, py: Python<'py>) -> Result<Self::Output, Self::Error> {
401        let seconds_offset = self.local_minus_utc();
402        let td = PyDelta::new(py, 0, seconds_offset, 0, true)?;
403        PyTzInfo::fixed_offset(py, td)
404    }
405}
406
407impl<'py> IntoPyObject<'py> for &FixedOffset {
408    type Target = PyTzInfo;
409    type Output = Bound<'py, Self::Target>;
410    type Error = PyErr;
411
412    #[cfg(feature = "experimental-inspect")]
413    const OUTPUT_TYPE: PyStaticExpr = FixedOffset::OUTPUT_TYPE;
414
415    #[inline]
416    fn into_pyobject(self, py: Python<'py>) -> Result<Self::Output, Self::Error> {
417        (*self).into_pyobject(py)
418    }
419}
420
421impl FromPyObject<'_, '_> for FixedOffset {
422    type Error = PyErr;
423
424    #[cfg(feature = "experimental-inspect")]
425    const INPUT_TYPE: PyStaticExpr = PyTzInfo::TYPE_HINT;
426
427    /// Convert python tzinfo to rust [`FixedOffset`].
428    ///
429    /// Note that the conversion will result in precision lost in microseconds as chrono offset
430    /// does not supports microseconds.
431    fn extract(ob: Borrowed<'_, '_, PyAny>) -> Result<Self, Self::Error> {
432        let ob = ob.cast::<PyTzInfo>()?;
433
434        // Passing Python's None to the `utcoffset` function will only
435        // work for timezones defined as fixed offsets in Python.
436        // Any other timezone would require a datetime as the parameter, and return
437        // None if the datetime is not provided.
438        // Trying to convert None to a PyDelta in the next line will then fail.
439        let py_timedelta =
440            ob.call_method1(intern!(ob.py(), "utcoffset"), (PyNone::get(ob.py()),))?;
441        if py_timedelta.is_none() {
442            return Err(PyTypeError::new_err(format!(
443                "{ob:?} is not a fixed offset timezone"
444            )));
445        }
446        let total_seconds: Duration = py_timedelta.extract()?;
447        // This cast is safe since the timedelta is limited to -24 hours and 24 hours.
448        let total_seconds = total_seconds.num_seconds() as i32;
449        FixedOffset::east_opt(total_seconds)
450            .ok_or_else(|| PyValueError::new_err("fixed offset out of bounds"))
451    }
452}
453
454impl<'py> IntoPyObject<'py> for Utc {
455    type Target = PyTzInfo;
456    type Output = Borrowed<'static, 'py, Self::Target>;
457    type Error = PyErr;
458
459    #[cfg(feature = "experimental-inspect")]
460    const OUTPUT_TYPE: PyStaticExpr = type_hint_identifier!("datetime", "timezone");
461
462    fn into_pyobject(self, py: Python<'py>) -> Result<Self::Output, Self::Error> {
463        PyTzInfo::utc(py)
464    }
465}
466
467impl<'py> IntoPyObject<'py> for &Utc {
468    type Target = PyTzInfo;
469    type Output = Borrowed<'static, 'py, Self::Target>;
470    type Error = PyErr;
471
472    #[cfg(feature = "experimental-inspect")]
473    const OUTPUT_TYPE: PyStaticExpr = Utc::OUTPUT_TYPE;
474
475    #[inline]
476    fn into_pyobject(self, py: Python<'py>) -> Result<Self::Output, Self::Error> {
477        (*self).into_pyobject(py)
478    }
479}
480
481impl FromPyObject<'_, '_> for Utc {
482    type Error = PyErr;
483
484    #[cfg(feature = "experimental-inspect")]
485    const INPUT_TYPE: PyStaticExpr = Utc::OUTPUT_TYPE;
486
487    fn extract(ob: Borrowed<'_, '_, PyAny>) -> Result<Self, Self::Error> {
488        let py_utc = Utc.into_pyobject(ob.py())?;
489        if ob.eq(py_utc)? {
490            Ok(Utc)
491        } else {
492            Err(PyValueError::new_err("expected datetime.timezone.utc"))
493        }
494    }
495}
496
497#[cfg(feature = "chrono-local")]
498impl<'py> IntoPyObject<'py> for Local {
499    type Target = PyTzInfo;
500    type Output = Borrowed<'static, 'py, Self::Target>;
501    type Error = PyErr;
502
503    #[cfg(feature = "experimental-inspect")]
504    const OUTPUT_TYPE: PyStaticExpr = type_hint_identifier!("zoneinfo", "ZoneInfo");
505
506    fn into_pyobject(self, py: Python<'py>) -> Result<Self::Output, Self::Error> {
507        static LOCAL_TZ: PyOnceLock<Py<PyTzInfo>> = PyOnceLock::new();
508        let tz = LOCAL_TZ
509            .get_or_try_init(py, || {
510                let iana_name = iana_time_zone::get_timezone().map_err(|e| {
511                    PyRuntimeError::new_err(format!("Could not get local timezone: {e}"))
512                })?;
513                PyTzInfo::timezone(py, iana_name).map(Bound::unbind)
514            })?
515            .bind_borrowed(py);
516        Ok(tz)
517    }
518}
519
520#[cfg(feature = "chrono-local")]
521impl<'py> IntoPyObject<'py> for &Local {
522    type Target = PyTzInfo;
523    type Output = Borrowed<'static, 'py, Self::Target>;
524    type Error = PyErr;
525
526    #[cfg(feature = "experimental-inspect")]
527    const OUTPUT_TYPE: PyStaticExpr = Local::OUTPUT_TYPE;
528
529    #[inline]
530    fn into_pyobject(self, py: Python<'py>) -> Result<Self::Output, Self::Error> {
531        (*self).into_pyobject(py)
532    }
533}
534
535#[cfg(feature = "chrono-local")]
536impl FromPyObject<'_, '_> for Local {
537    type Error = PyErr;
538
539    #[cfg(feature = "experimental-inspect")]
540    const INPUT_TYPE: PyStaticExpr = Local::OUTPUT_TYPE;
541
542    fn extract(ob: Borrowed<'_, '_, PyAny>) -> PyResult<Local> {
543        let local_tz = Local.into_pyobject(ob.py())?;
544        if ob.eq(local_tz)? {
545            Ok(Local)
546        } else {
547            let name = local_tz.getattr("key")?.cast_into::<PyString>()?;
548            Err(PyValueError::new_err(format!(
549                "expected local timezone {}",
550                name.to_cow()?
551            )))
552        }
553    }
554
555    #[inline]
556    fn as_local_tz(_: crate::conversion::private::Token) -> Option<Self> {
557        Some(Local)
558    }
559}
560
561struct DateArgs {
562    year: i32,
563    month: u8,
564    day: u8,
565}
566
567impl From<&NaiveDate> for DateArgs {
568    fn from(value: &NaiveDate) -> Self {
569        Self {
570            year: value.year(),
571            month: value.month() as u8,
572            day: value.day() as u8,
573        }
574    }
575}
576
577struct TimeArgs {
578    hour: u8,
579    min: u8,
580    sec: u8,
581    micro: u32,
582    truncated_leap_second: bool,
583}
584
585impl From<&NaiveTime> for TimeArgs {
586    fn from(value: &NaiveTime) -> Self {
587        let ns = value.nanosecond();
588        let checked_sub = ns.checked_sub(1_000_000_000);
589        let truncated_leap_second = checked_sub.is_some();
590        let micro = checked_sub.unwrap_or(ns) / 1000;
591        Self {
592            hour: value.hour() as u8,
593            min: value.minute() as u8,
594            sec: value.second() as u8,
595            micro,
596            truncated_leap_second,
597        }
598    }
599}
600
601fn warn_truncated_leap_second(obj: &Bound<'_, PyAny>) {
602    let py = obj.py();
603    if let Err(e) = PyErr::warn(
604        py,
605        &py.get_type::<PyUserWarning>(),
606        c"ignored leap-second, `datetime` does not support leap-seconds",
607        0,
608    ) {
609        e.write_unraisable(py, Some(obj))
610    };
611}
612
613fn py_date_to_naive_date(
614    py_date: impl core::ops::Deref<Target = impl PyDateAccess>,
615) -> PyResult<NaiveDate> {
616    NaiveDate::from_ymd_opt(
617        py_date.get_year(),
618        py_date.get_month().into(),
619        py_date.get_day().into(),
620    )
621    .ok_or_else(|| PyValueError::new_err("invalid or out-of-range date"))
622}
623
624fn py_time_to_naive_time(
625    py_time: impl core::ops::Deref<Target = impl PyTimeAccess>,
626) -> PyResult<NaiveTime> {
627    NaiveTime::from_hms_micro_opt(
628        py_time.get_hour().into(),
629        py_time.get_minute().into(),
630        py_time.get_second().into(),
631        py_time.get_microsecond(),
632    )
633    .ok_or_else(|| PyValueError::new_err("invalid or out-of-range time"))
634}
635
636fn py_datetime_to_datetime_with_timezone<Tz: TimeZone>(
637    dt: &Bound<'_, PyDateTime>,
638    tz: Tz,
639) -> PyResult<DateTime<Tz>> {
640    let naive_dt = NaiveDateTime::new(py_date_to_naive_date(dt)?, py_time_to_naive_time(dt)?);
641    match naive_dt.and_local_timezone(tz) {
642        LocalResult::Single(value) => Ok(value),
643        LocalResult::Ambiguous(earliest, latest) => {
644            if dt.get_fold() {
645                Ok(latest)
646            } else {
647                Ok(earliest)
648            }
649        }
650        LocalResult::None => Err(PyValueError::new_err(format!(
651            "The datetime {dt:?} contains an incompatible timezone"
652        ))),
653    }
654}
655
656#[cfg(test)]
657mod tests {
658    use super::*;
659    use crate::platform::prelude::*;
660    use crate::{BoundObject, test_utils::assert_warnings, types::PyTuple};
661    use core::cmp::Ordering;
662
663    #[test]
664    // Only Python>=3.9 has the zoneinfo package
665    // We skip the test on windows too since we'd need to install
666    // tzdata there to make this work.
667    #[cfg(not(target_os = "windows"))]
668    fn test_zoneinfo_is_not_fixed_offset() {
669        use crate::types::any::PyAnyMethods;
670        use crate::types::dict::PyDictMethods;
671
672        Python::attach(|py| {
673            let locals = crate::types::PyDict::new(py);
674            py.run(
675                c"import zoneinfo; zi = zoneinfo.ZoneInfo('Europe/London')",
676                None,
677                Some(&locals),
678            )
679            .unwrap();
680            let result: PyResult<FixedOffset> = locals.get_item("zi").unwrap().unwrap().extract();
681            assert!(result.is_err());
682            let res = result.err().unwrap();
683            // Also check the error message is what we expect
684            let msg = res.value(py).repr().unwrap().to_string();
685            assert_eq!(
686                msg,
687                "TypeError(\"zoneinfo.ZoneInfo(key='Europe/London') is not a fixed offset timezone\")"
688            );
689        });
690    }
691
692    #[test]
693    fn test_timezone_aware_to_naive_fails() {
694        // Test that if a user tries to convert a python's timezone aware datetime into a naive
695        // one, the conversion fails.
696        Python::attach(|py| {
697            let py_datetime =
698                new_py_datetime_ob(py, "datetime", (2022, 1, 1, 1, 0, 0, 0, python_utc(py)));
699            // Now test that converting a PyDateTime with tzinfo to a NaiveDateTime fails
700            let res: PyResult<NaiveDateTime> = py_datetime.extract();
701            assert_eq!(
702                res.unwrap_err().value(py).repr().unwrap().to_string(),
703                "TypeError('expected a datetime without tzinfo')"
704            );
705        });
706    }
707
708    #[test]
709    fn test_naive_to_timezone_aware_fails() {
710        // Test that if a user tries to convert a python's timezone aware datetime into a naive
711        // one, the conversion fails.
712        Python::attach(|py| {
713            let py_datetime = new_py_datetime_ob(py, "datetime", (2022, 1, 1, 1, 0, 0, 0));
714            // Now test that converting a PyDateTime with tzinfo to a NaiveDateTime fails
715            let res: PyResult<DateTime<Utc>> = py_datetime.extract();
716            assert_eq!(
717                res.unwrap_err().value(py).repr().unwrap().to_string(),
718                "TypeError('expected a datetime with non-None tzinfo')"
719            );
720
721            // Now test that converting a PyDateTime with tzinfo to a NaiveDateTime fails
722            let res: PyResult<DateTime<FixedOffset>> = py_datetime.extract();
723            assert_eq!(
724                res.unwrap_err().value(py).repr().unwrap().to_string(),
725                "TypeError('expected a datetime with non-None tzinfo')"
726            );
727        });
728    }
729
730    #[test]
731    fn test_invalid_types_fail() {
732        // Test that if a user tries to convert a python's timezone aware datetime into a naive
733        // one, the conversion fails.
734        Python::attach(|py| {
735            let none = py.None().into_bound(py);
736            assert_eq!(
737                none.extract::<Duration>().unwrap_err().to_string(),
738                "TypeError: 'None' is not an instance of 'timedelta'"
739            );
740            assert_eq!(
741                none.extract::<FixedOffset>().unwrap_err().to_string(),
742                "TypeError: 'None' is not an instance of 'tzinfo'"
743            );
744            assert_eq!(
745                none.extract::<Utc>().unwrap_err().to_string(),
746                "ValueError: expected datetime.timezone.utc"
747            );
748            assert_eq!(
749                none.extract::<NaiveTime>().unwrap_err().to_string(),
750                "TypeError: 'None' is not an instance of 'time'"
751            );
752            assert_eq!(
753                none.extract::<NaiveDate>().unwrap_err().to_string(),
754                "TypeError: 'None' is not an instance of 'date'"
755            );
756            assert_eq!(
757                none.extract::<NaiveDateTime>().unwrap_err().to_string(),
758                "TypeError: 'None' is not an instance of 'datetime'"
759            );
760            assert_eq!(
761                none.extract::<DateTime<Utc>>().unwrap_err().to_string(),
762                "TypeError: 'None' is not an instance of 'datetime'"
763            );
764            assert_eq!(
765                none.extract::<DateTime<FixedOffset>>()
766                    .unwrap_err()
767                    .to_string(),
768                "TypeError: 'None' is not an instance of 'datetime'"
769            );
770        });
771    }
772
773    #[test]
774    fn test_pyo3_timedelta_into_pyobject() {
775        // Utility function used to check different durations.
776        // The `name` parameter is used to identify the check in case of a failure.
777        let check = |name: &'static str, delta: Duration, py_days, py_seconds, py_ms| {
778            Python::attach(|py| {
779                let delta = delta.into_pyobject(py).unwrap();
780                let py_delta = new_py_datetime_ob(py, "timedelta", (py_days, py_seconds, py_ms));
781                assert!(
782                    delta.eq(&py_delta).unwrap(),
783                    "{name}: {delta} != {py_delta}"
784                );
785            });
786        };
787
788        let delta = Duration::days(-1) + Duration::seconds(1) + Duration::microseconds(-10);
789        check("delta normalization", delta, -1, 1, -10);
790
791        // Check the minimum value allowed by PyDelta, which is different
792        // from the minimum value allowed in Duration. This should pass.
793        let delta = Duration::seconds(-86399999913600); // min
794        check("delta min value", delta, -999999999, 0, 0);
795
796        // Same, for max value
797        let delta = Duration::seconds(86399999999999) + Duration::nanoseconds(999999000); // max
798        check("delta max value", delta, 999999999, 86399, 999999);
799
800        // Also check that trying to convert an out of bound value errors.
801        Python::attach(|py| {
802            // min_value and max_value were deprecated in chrono 0.4.39
803            #[allow(deprecated)]
804            {
805                assert!(Duration::min_value().into_pyobject(py).is_err());
806                assert!(Duration::max_value().into_pyobject(py).is_err());
807            }
808        });
809    }
810
811    #[test]
812    fn test_pyo3_timedelta_frompyobject() {
813        // Utility function used to check different durations.
814        // The `name` parameter is used to identify the check in case of a failure.
815        let check = |name: &'static str, delta: Duration, py_days, py_seconds, py_ms| {
816            Python::attach(|py| {
817                let py_delta = new_py_datetime_ob(py, "timedelta", (py_days, py_seconds, py_ms));
818                let py_delta: Duration = py_delta.extract().unwrap();
819                assert_eq!(py_delta, delta, "{name}: {py_delta} != {delta}");
820            })
821        };
822
823        // Check the minimum value allowed by PyDelta, which is different
824        // from the minimum value allowed in Duration. This should pass.
825        check(
826            "min py_delta value",
827            Duration::seconds(-86399999913600),
828            -999999999,
829            0,
830            0,
831        );
832        // Same, for max value
833        check(
834            "max py_delta value",
835            Duration::seconds(86399999999999) + Duration::microseconds(999999),
836            999999999,
837            86399,
838            999999,
839        );
840
841        // This check is to assert that we can't construct every possible Duration from a PyDelta
842        // since they have different bounds.
843        Python::attach(|py| {
844            let low_days: i32 = -1000000000;
845            // This is possible
846            assert!(std::panic::catch_unwind(|| Duration::days(low_days as i64)).is_ok());
847            // This panics on PyDelta::new
848            assert!(
849                std::panic::catch_unwind(|| {
850                    let py_delta = new_py_datetime_ob(py, "timedelta", (low_days, 0, 0));
851                    if let Ok(_duration) = py_delta.extract::<Duration>() {
852                        // So we should never get here
853                    }
854                })
855                .is_err()
856            );
857
858            let high_days: i32 = 1000000000;
859            // This is possible
860            assert!(std::panic::catch_unwind(|| Duration::days(high_days as i64)).is_ok());
861            // This panics on PyDelta::new
862            assert!(
863                std::panic::catch_unwind(|| {
864                    let py_delta = new_py_datetime_ob(py, "timedelta", (high_days, 0, 0));
865                    if let Ok(_duration) = py_delta.extract::<Duration>() {
866                        // So we should never get here
867                    }
868                })
869                .is_err()
870            );
871        });
872    }
873
874    #[test]
875    fn test_pyo3_date_into_pyobject() {
876        let eq_ymd = |name: &'static str, year, month, day| {
877            Python::attach(|py| {
878                let date = NaiveDate::from_ymd_opt(year, month, day)
879                    .unwrap()
880                    .into_pyobject(py)
881                    .unwrap();
882                let py_date = new_py_datetime_ob(py, "date", (year, month, day));
883                assert_eq!(
884                    date.compare(&py_date).unwrap(),
885                    Ordering::Equal,
886                    "{name}: {date} != {py_date}"
887                );
888            })
889        };
890
891        eq_ymd("past date", 2012, 2, 29);
892        eq_ymd("min date", 1, 1, 1);
893        eq_ymd("future date", 3000, 6, 5);
894        eq_ymd("max date", 9999, 12, 31);
895    }
896
897    #[test]
898    fn test_pyo3_date_frompyobject() {
899        let eq_ymd = |name: &'static str, year, month, day| {
900            Python::attach(|py| {
901                let py_date = new_py_datetime_ob(py, "date", (year, month, day));
902                let py_date: NaiveDate = py_date.extract().unwrap();
903                let date = NaiveDate::from_ymd_opt(year, month, day).unwrap();
904                assert_eq!(py_date, date, "{name}: {date} != {py_date}");
905            })
906        };
907
908        eq_ymd("past date", 2012, 2, 29);
909        eq_ymd("min date", 1, 1, 1);
910        eq_ymd("future date", 3000, 6, 5);
911        eq_ymd("max date", 9999, 12, 31);
912    }
913
914    #[test]
915    fn test_pyo3_datetime_into_pyobject_utc() {
916        Python::attach(|py| {
917            let check_utc =
918                |name: &'static str, year, month, day, hour, minute, second, ms, py_ms| {
919                    let datetime = NaiveDate::from_ymd_opt(year, month, day)
920                        .unwrap()
921                        .and_hms_micro_opt(hour, minute, second, ms)
922                        .unwrap()
923                        .and_utc();
924                    let datetime = datetime.into_pyobject(py).unwrap();
925                    let py_datetime = new_py_datetime_ob(
926                        py,
927                        "datetime",
928                        (
929                            year,
930                            month,
931                            day,
932                            hour,
933                            minute,
934                            second,
935                            py_ms,
936                            python_utc(py),
937                        ),
938                    );
939                    assert_eq!(
940                        datetime.compare(&py_datetime).unwrap(),
941                        Ordering::Equal,
942                        "{name}: {datetime} != {py_datetime}"
943                    );
944                };
945
946            check_utc("regular", 2014, 5, 6, 7, 8, 9, 999_999, 999_999);
947
948            assert_warnings!(
949                py,
950                check_utc("leap second", 2014, 5, 6, 7, 8, 59, 1_999_999, 999_999),
951                [(
952                    PyUserWarning,
953                    "ignored leap-second, `datetime` does not support leap-seconds"
954                )]
955            );
956        })
957    }
958
959    #[test]
960    fn test_pyo3_datetime_into_pyobject_fixed_offset() {
961        Python::attach(|py| {
962            let check_fixed_offset =
963                |name: &'static str, year, month, day, hour, minute, second, ms, py_ms| {
964                    let offset = FixedOffset::east_opt(3600).unwrap();
965                    let datetime = NaiveDate::from_ymd_opt(year, month, day)
966                        .unwrap()
967                        .and_hms_micro_opt(hour, minute, second, ms)
968                        .unwrap()
969                        .and_local_timezone(offset)
970                        .unwrap();
971                    let datetime = datetime.into_pyobject(py).unwrap();
972                    let py_tz = offset.into_pyobject(py).unwrap();
973                    let py_datetime = new_py_datetime_ob(
974                        py,
975                        "datetime",
976                        (year, month, day, hour, minute, second, py_ms, py_tz),
977                    );
978                    assert_eq!(
979                        datetime.compare(&py_datetime).unwrap(),
980                        Ordering::Equal,
981                        "{name}: {datetime} != {py_datetime}"
982                    );
983                };
984
985            check_fixed_offset("regular", 2014, 5, 6, 7, 8, 9, 999_999, 999_999);
986
987            assert_warnings!(
988                py,
989                check_fixed_offset("leap second", 2014, 5, 6, 7, 8, 59, 1_999_999, 999_999),
990                [(
991                    PyUserWarning,
992                    "ignored leap-second, `datetime` does not support leap-seconds"
993                )]
994            );
995        })
996    }
997
998    #[test]
999    #[cfg(all(feature = "chrono-tz", not(windows)))]
1000    fn test_pyo3_datetime_into_pyobject_tz() {
1001        Python::attach(|py| {
1002            let datetime = NaiveDate::from_ymd_opt(2024, 12, 11)
1003                .unwrap()
1004                .and_hms_opt(23, 3, 13)
1005                .unwrap()
1006                .and_local_timezone(chrono_tz::Tz::Europe__London)
1007                .unwrap();
1008            let datetime = datetime.into_pyobject(py).unwrap();
1009            let py_datetime = new_py_datetime_ob(
1010                py,
1011                "datetime",
1012                (
1013                    2024,
1014                    12,
1015                    11,
1016                    23,
1017                    3,
1018                    13,
1019                    0,
1020                    python_zoneinfo(py, "Europe/London"),
1021                ),
1022            );
1023            assert_eq!(datetime.compare(&py_datetime).unwrap(), Ordering::Equal);
1024        })
1025    }
1026
1027    #[test]
1028    fn test_pyo3_datetime_frompyobject_utc() {
1029        Python::attach(|py| {
1030            let year = 2014;
1031            let month = 5;
1032            let day = 6;
1033            let hour = 7;
1034            let minute = 8;
1035            let second = 9;
1036            let micro = 999_999;
1037            let tz_utc = PyTzInfo::utc(py).unwrap();
1038            let py_datetime = new_py_datetime_ob(
1039                py,
1040                "datetime",
1041                (year, month, day, hour, minute, second, micro, tz_utc),
1042            );
1043            let py_datetime: DateTime<Utc> = py_datetime.extract().unwrap();
1044            let datetime = NaiveDate::from_ymd_opt(year, month, day)
1045                .unwrap()
1046                .and_hms_micro_opt(hour, minute, second, micro)
1047                .unwrap()
1048                .and_utc();
1049            assert_eq!(py_datetime, datetime,);
1050        })
1051    }
1052
1053    #[test]
1054    #[cfg(feature = "chrono-local")]
1055    fn test_pyo3_naive_datetime_frompyobject_local() {
1056        Python::attach(|py| {
1057            let year = 2014;
1058            let month = 5;
1059            let day = 6;
1060            let hour = 7;
1061            let minute = 8;
1062            let second = 9;
1063            let micro = 999_999;
1064            let py_datetime = new_py_datetime_ob(
1065                py,
1066                "datetime",
1067                (year, month, day, hour, minute, second, micro),
1068            );
1069            let py_datetime: DateTime<Local> = py_datetime.extract().unwrap();
1070            let expected_datetime = NaiveDate::from_ymd_opt(year, month, day)
1071                .unwrap()
1072                .and_hms_micro_opt(hour, minute, second, micro)
1073                .unwrap()
1074                .and_local_timezone(Local)
1075                .unwrap();
1076            assert_eq!(py_datetime, expected_datetime);
1077        })
1078    }
1079
1080    #[test]
1081    fn test_pyo3_datetime_frompyobject_fixed_offset() {
1082        Python::attach(|py| {
1083            let year = 2014;
1084            let month = 5;
1085            let day = 6;
1086            let hour = 7;
1087            let minute = 8;
1088            let second = 9;
1089            let micro = 999_999;
1090            let offset = FixedOffset::east_opt(3600).unwrap();
1091            let py_tz = offset.into_pyobject(py).unwrap();
1092            let py_datetime = new_py_datetime_ob(
1093                py,
1094                "datetime",
1095                (year, month, day, hour, minute, second, micro, py_tz),
1096            );
1097            let datetime_from_py: DateTime<FixedOffset> = py_datetime.extract().unwrap();
1098            let datetime = NaiveDate::from_ymd_opt(year, month, day)
1099                .unwrap()
1100                .and_hms_micro_opt(hour, minute, second, micro)
1101                .unwrap();
1102            let datetime = datetime.and_local_timezone(offset).unwrap();
1103
1104            assert_eq!(datetime_from_py, datetime);
1105            assert!(
1106                py_datetime.extract::<DateTime<Utc>>().is_err(),
1107                "Extracting Utc from nonzero FixedOffset timezone will fail"
1108            );
1109
1110            let utc = python_utc(py);
1111            let py_datetime_utc = new_py_datetime_ob(
1112                py,
1113                "datetime",
1114                (year, month, day, hour, minute, second, micro, utc),
1115            );
1116            assert!(
1117                py_datetime_utc.extract::<DateTime<FixedOffset>>().is_ok(),
1118                "Extracting FixedOffset from Utc timezone will succeed"
1119            );
1120        })
1121    }
1122
1123    #[test]
1124    fn test_pyo3_offset_fixed_into_pyobject() {
1125        Python::attach(|py| {
1126            // Chrono offset
1127            let offset = FixedOffset::east_opt(3600)
1128                .unwrap()
1129                .into_pyobject(py)
1130                .unwrap();
1131            // Python timezone from timedelta
1132            let td = new_py_datetime_ob(py, "timedelta", (0, 3600, 0));
1133            let py_timedelta = new_py_datetime_ob(py, "timezone", (td,));
1134            // Should be equal
1135            assert!(offset.eq(py_timedelta).unwrap());
1136
1137            // Same but with negative values
1138            let offset = FixedOffset::east_opt(-3600)
1139                .unwrap()
1140                .into_pyobject(py)
1141                .unwrap();
1142            let td = new_py_datetime_ob(py, "timedelta", (0, -3600, 0));
1143            let py_timedelta = new_py_datetime_ob(py, "timezone", (td,));
1144            assert!(offset.eq(py_timedelta).unwrap());
1145        })
1146    }
1147
1148    #[test]
1149    fn test_pyo3_offset_fixed_frompyobject() {
1150        Python::attach(|py| {
1151            let py_timedelta = new_py_datetime_ob(py, "timedelta", (0, 3600, 0));
1152            let py_tzinfo = new_py_datetime_ob(py, "timezone", (py_timedelta,));
1153            let offset: FixedOffset = py_tzinfo.extract().unwrap();
1154            assert_eq!(FixedOffset::east_opt(3600).unwrap(), offset);
1155        })
1156    }
1157
1158    #[test]
1159    fn test_pyo3_offset_utc_into_pyobject() {
1160        Python::attach(|py| {
1161            let utc = Utc.into_pyobject(py).unwrap();
1162            let py_utc = python_utc(py);
1163            assert!(utc.is(&py_utc));
1164        })
1165    }
1166
1167    #[test]
1168    fn test_pyo3_offset_utc_frompyobject() {
1169        Python::attach(|py| {
1170            let py_utc = python_utc(py);
1171            let py_utc: Utc = py_utc.extract().unwrap();
1172            assert_eq!(Utc, py_utc);
1173
1174            let py_timedelta = new_py_datetime_ob(py, "timedelta", (0, 0, 0));
1175            let py_timezone_utc = new_py_datetime_ob(py, "timezone", (py_timedelta,));
1176            let py_timezone_utc: Utc = py_timezone_utc.extract().unwrap();
1177            assert_eq!(Utc, py_timezone_utc);
1178
1179            let py_timedelta = new_py_datetime_ob(py, "timedelta", (0, 3600, 0));
1180            let py_timezone = new_py_datetime_ob(py, "timezone", (py_timedelta,));
1181            assert!(py_timezone.extract::<Utc>().is_err());
1182        })
1183    }
1184
1185    #[test]
1186    fn test_pyo3_time_into_pyobject() {
1187        Python::attach(|py| {
1188            let check_time = |name: &'static str, hour, minute, second, ms, py_ms| {
1189                let time = NaiveTime::from_hms_micro_opt(hour, minute, second, ms)
1190                    .unwrap()
1191                    .into_pyobject(py)
1192                    .unwrap();
1193                let py_time = new_py_datetime_ob(py, "time", (hour, minute, second, py_ms));
1194                assert!(time.eq(&py_time).unwrap(), "{name}: {time} != {py_time}");
1195            };
1196
1197            check_time("regular", 3, 5, 7, 999_999, 999_999);
1198
1199            assert_warnings!(
1200                py,
1201                check_time("leap second", 3, 5, 59, 1_999_999, 999_999),
1202                [(
1203                    PyUserWarning,
1204                    "ignored leap-second, `datetime` does not support leap-seconds"
1205                )]
1206            );
1207        })
1208    }
1209
1210    #[test]
1211    fn test_pyo3_time_frompyobject() {
1212        let hour = 3;
1213        let minute = 5;
1214        let second = 7;
1215        let micro = 999_999;
1216        Python::attach(|py| {
1217            let py_time = new_py_datetime_ob(py, "time", (hour, minute, second, micro));
1218            let py_time: NaiveTime = py_time.extract().unwrap();
1219            let time = NaiveTime::from_hms_micro_opt(hour, minute, second, micro).unwrap();
1220            assert_eq!(py_time, time);
1221        })
1222    }
1223
1224    fn new_py_datetime_ob<'py, A>(py: Python<'py>, name: &str, args: A) -> Bound<'py, PyAny>
1225    where
1226        A: IntoPyObject<'py, Target = PyTuple>,
1227    {
1228        py.import("datetime")
1229            .unwrap()
1230            .getattr(name)
1231            .unwrap()
1232            .call1(
1233                args.into_pyobject(py)
1234                    .map_err(Into::into)
1235                    .unwrap()
1236                    .into_bound(),
1237            )
1238            .unwrap()
1239    }
1240
1241    fn python_utc(py: Python<'_>) -> Bound<'_, PyAny> {
1242        py.import("datetime")
1243            .unwrap()
1244            .getattr("timezone")
1245            .unwrap()
1246            .getattr("utc")
1247            .unwrap()
1248    }
1249
1250    #[cfg(all(feature = "chrono-tz", not(windows)))]
1251    fn python_zoneinfo<'py>(py: Python<'py>, timezone: &str) -> Bound<'py, PyAny> {
1252        py.import("zoneinfo")
1253            .unwrap()
1254            .getattr("ZoneInfo")
1255            .unwrap()
1256            .call1((timezone,))
1257            .unwrap()
1258    }
1259
1260    #[cfg(not(any(target_arch = "wasm32")))]
1261    mod proptests {
1262        use super::*;
1263        use crate::test_utils::CatchWarnings;
1264        use crate::types::IntoPyDict;
1265        use alloc::ffi::CString;
1266        use proptest::prelude::*;
1267
1268        proptest! {
1269
1270            // Range is limited to 1970 to 2038 due to windows limitations
1271            #[test]
1272            fn test_pyo3_offset_fixed_frompyobject_created_in_python(timestamp in 0..(i32::MAX as i64), timedelta in -86399i32..=86399i32) {
1273                Python::attach(|py| {
1274
1275                    let globals = [("datetime", py.import("datetime").unwrap())].into_py_dict(py).unwrap();
1276                    let code = format!("datetime.datetime.fromtimestamp({timestamp}).replace(tzinfo=datetime.timezone(datetime.timedelta(seconds={timedelta})))");
1277                    let t = py.eval(&CString::new(code).unwrap(), Some(&globals), None).unwrap();
1278
1279                    // Get ISO 8601 string from python
1280                    let py_iso_str = t.call_method0("isoformat").unwrap();
1281
1282                    // Get ISO 8601 string from rust
1283                    let t = t.extract::<DateTime<FixedOffset>>().unwrap();
1284                    // Python doesn't print the seconds of the offset if they are 0
1285                    let rust_iso_str = if timedelta % 60 == 0 {
1286                        t.format("%Y-%m-%dT%H:%M:%S%:z").to_string()
1287                    } else {
1288                        t.format("%Y-%m-%dT%H:%M:%S%::z").to_string()
1289                    };
1290
1291                    // They should be equal
1292                    assert_eq!(py_iso_str.to_string(), rust_iso_str);
1293                })
1294            }
1295
1296            #[test]
1297            fn test_duration_roundtrip(days in -999999999i64..=999999999i64) {
1298                // Test roundtrip conversion rust->python->rust for all allowed
1299                // python values of durations (from -999999999 to 999999999 days),
1300                Python::attach(|py| {
1301                    let dur = Duration::days(days);
1302                    let py_delta = dur.into_pyobject(py).unwrap();
1303                    let roundtripped: Duration = py_delta.extract().expect("Round trip");
1304                    assert_eq!(dur, roundtripped);
1305                })
1306            }
1307
1308            #[test]
1309            fn test_fixed_offset_roundtrip(secs in -86399i32..=86399i32) {
1310                Python::attach(|py| {
1311                    let offset = FixedOffset::east_opt(secs).unwrap();
1312                    let py_offset = offset.into_pyobject(py).unwrap();
1313                    let roundtripped: FixedOffset = py_offset.extract().expect("Round trip");
1314                    assert_eq!(offset, roundtripped);
1315                })
1316            }
1317
1318            #[test]
1319            fn test_naive_date_roundtrip(
1320                year in 1i32..=9999i32,
1321                month in 1u32..=12u32,
1322                day in 1u32..=31u32
1323            ) {
1324                // Test roundtrip conversion rust->python->rust for all allowed
1325                // python dates (from year 1 to year 9999)
1326                Python::attach(|py| {
1327                    // We use to `from_ymd_opt` constructor so that we only test valid `NaiveDate`s.
1328                    // This is to skip the test if we are creating an invalid date, like February 31.
1329                    if let Some(date) = NaiveDate::from_ymd_opt(year, month, day) {
1330                        let py_date = date.into_pyobject(py).unwrap();
1331                        let roundtripped: NaiveDate = py_date.extract().expect("Round trip");
1332                        assert_eq!(date, roundtripped);
1333                    }
1334                })
1335            }
1336
1337            #[test]
1338            fn test_naive_time_roundtrip(
1339                hour in 0u32..=23u32,
1340                min in 0u32..=59u32,
1341                sec in 0u32..=59u32,
1342                micro in 0u32..=1_999_999u32
1343            ) {
1344                // Test roundtrip conversion rust->python->rust for naive times.
1345                // Python time has a resolution of microseconds, so we only test
1346                // NaiveTimes with microseconds resolution, even if NaiveTime has nanosecond
1347                // resolution.
1348                Python::attach(|py| {
1349                    if let Some(time) = NaiveTime::from_hms_micro_opt(hour, min, sec, micro) {
1350                        // Wrap in CatchWarnings to avoid to_object firing warning for truncated leap second
1351                        let py_time = CatchWarnings::enter(py, |_| time.into_pyobject(py)).unwrap();
1352                        let roundtripped: NaiveTime = py_time.extract().expect("Round trip");
1353                        // Leap seconds are not roundtripped
1354                        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);
1355                        assert_eq!(expected_roundtrip_time, roundtripped);
1356                    }
1357                })
1358            }
1359
1360            #[test]
1361            fn test_naive_datetime_roundtrip(
1362                year in 1i32..=9999i32,
1363                month in 1u32..=12u32,
1364                day in 1u32..=31u32,
1365                hour in 0u32..=24u32,
1366                min in 0u32..=60u32,
1367                sec in 0u32..=60u32,
1368                micro in 0u32..=999_999u32
1369            ) {
1370                Python::attach(|py| {
1371                    let date_opt = NaiveDate::from_ymd_opt(year, month, day);
1372                    let time_opt = NaiveTime::from_hms_micro_opt(hour, min, sec, micro);
1373                    if let (Some(date), Some(time)) = (date_opt, time_opt) {
1374                        let dt = NaiveDateTime::new(date, time);
1375                        let pydt = dt.into_pyobject(py).unwrap();
1376                        let roundtripped: NaiveDateTime = pydt.extract().expect("Round trip");
1377                        assert_eq!(dt, roundtripped);
1378                    }
1379                })
1380            }
1381
1382            #[test]
1383            fn test_utc_datetime_roundtrip(
1384                year in 1i32..=9999i32,
1385                month in 1u32..=12u32,
1386                day in 1u32..=31u32,
1387                hour in 0u32..=23u32,
1388                min in 0u32..=59u32,
1389                sec in 0u32..=59u32,
1390                micro in 0u32..=1_999_999u32
1391            ) {
1392                Python::attach(|py| {
1393                    let date_opt = NaiveDate::from_ymd_opt(year, month, day);
1394                    let time_opt = NaiveTime::from_hms_micro_opt(hour, min, sec, micro);
1395                    if let (Some(date), Some(time)) = (date_opt, time_opt) {
1396                        let dt: DateTime<Utc> = NaiveDateTime::new(date, time).and_utc();
1397                        // Wrap in CatchWarnings to avoid into_py firing warning for truncated leap second
1398                        let py_dt = CatchWarnings::enter(py, |_| dt.into_pyobject(py)).unwrap();
1399                        let roundtripped: DateTime<Utc> = py_dt.extract().expect("Round trip");
1400                        // Leap seconds are not roundtripped
1401                        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);
1402                        let expected_roundtrip_dt: DateTime<Utc> = NaiveDateTime::new(date, expected_roundtrip_time).and_utc();
1403                        assert_eq!(expected_roundtrip_dt, roundtripped);
1404                    }
1405                })
1406            }
1407
1408            #[test]
1409            fn test_fixed_offset_datetime_roundtrip(
1410                year in 1i32..=9999i32,
1411                month in 1u32..=12u32,
1412                day in 1u32..=31u32,
1413                hour in 0u32..=23u32,
1414                min in 0u32..=59u32,
1415                sec in 0u32..=59u32,
1416                micro in 0u32..=1_999_999u32,
1417                offset_secs in -86399i32..=86399i32
1418            ) {
1419                Python::attach(|py| {
1420                    let date_opt = NaiveDate::from_ymd_opt(year, month, day);
1421                    let time_opt = NaiveTime::from_hms_micro_opt(hour, min, sec, micro);
1422                    let offset = FixedOffset::east_opt(offset_secs).unwrap();
1423                    if let (Some(date), Some(time)) = (date_opt, time_opt) {
1424                        let dt: DateTime<FixedOffset> = NaiveDateTime::new(date, time).and_local_timezone(offset).unwrap();
1425                        // Wrap in CatchWarnings to avoid into_py firing warning for truncated leap second
1426                        let py_dt = CatchWarnings::enter(py, |_| dt.into_pyobject(py)).unwrap();
1427                        let roundtripped: DateTime<FixedOffset> = py_dt.extract().expect("Round trip");
1428                        // Leap seconds are not roundtripped
1429                        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);
1430                        let expected_roundtrip_dt: DateTime<FixedOffset> = NaiveDateTime::new(date, expected_roundtrip_time).and_local_timezone(offset).unwrap();
1431                        assert_eq!(expected_roundtrip_dt, roundtripped);
1432                    }
1433                })
1434            }
1435
1436            #[test]
1437            #[cfg(all(feature = "chrono-local", not(target_os = "windows")))]
1438            fn test_local_datetime_roundtrip(
1439                year in 1i32..=9999i32,
1440                month in 1u32..=12u32,
1441                day in 1u32..=31u32,
1442                hour in 0u32..=23u32,
1443                min in 0u32..=59u32,
1444                sec in 0u32..=59u32,
1445                micro in 0u32..=1_999_999u32,
1446            ) {
1447                Python::attach(|py| {
1448                    let date_opt = NaiveDate::from_ymd_opt(year, month, day);
1449                    let time_opt = NaiveTime::from_hms_micro_opt(hour, min, sec, micro);
1450                    if let (Some(date), Some(time)) = (date_opt, time_opt) {
1451                        let dts = match NaiveDateTime::new(date, time).and_local_timezone(Local) {
1452                            LocalResult::None => return,
1453                            LocalResult::Single(dt) => [Some((dt, false)), None],
1454                            LocalResult::Ambiguous(dt1, dt2) => [Some((dt1, false)), Some((dt2, true))],
1455                        };
1456                        for (dt, fold) in dts.iter().filter_map(|input| *input) {
1457                            // Wrap in CatchWarnings to avoid into_py firing warning for truncated leap second
1458                            let py_dt = CatchWarnings::enter(py, |_| dt.into_pyobject(py)).unwrap();
1459                            let roundtripped: DateTime<Local> = py_dt.extract().expect("Round trip");
1460                            // Leap seconds are not roundtripped
1461                            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);
1462                            let expected_roundtrip_dt: DateTime<Local> = if fold {
1463                                NaiveDateTime::new(date, expected_roundtrip_time).and_local_timezone(Local).latest()
1464                            } else {
1465                                NaiveDateTime::new(date, expected_roundtrip_time).and_local_timezone(Local).earliest()
1466                            }.unwrap();
1467                            assert_eq!(expected_roundtrip_dt, roundtripped);
1468                        }
1469                    }
1470                })
1471            }
1472        }
1473    }
1474}