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