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