Skip to main content

pyo3/conversions/
chrono.rs

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