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