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::TypeHint;
48use crate::intern;
49#[cfg(feature = "experimental-inspect")]
50use crate::type_object::PyTypeInfo;
51use crate::types::any::PyAnyMethods;
52use crate::types::PyNone;
53use crate::types::{PyDate, PyDateTime, PyDelta, PyTime, PyTzInfo, PyTzInfoAccess};
54#[cfg(not(Py_LIMITED_API))]
55use crate::types::{PyDateAccess, PyDeltaAccess, PyTimeAccess};
56#[cfg(feature = "chrono-local")]
57use crate::{
58    exceptions::PyRuntimeError,
59    sync::PyOnceLock,
60    types::{PyString, PyStringMethods},
61    Py,
62};
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: TypeHint = 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: TypeHint = 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: TypeHint = 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: TypeHint = 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: TypeHint = 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: TypeHint = 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: TypeHint = 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: TypeHint = 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: TypeHint = 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: TypeHint = 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: TypeHint = 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: TypeHint = 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: TypeHint = <&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: TypeHint = 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: TypeHint = 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: TypeHint = PyTzInfo::TYPE_HINT;
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: TypeHint = 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: TypeHint = 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: TypeHint = PyTzInfo::TYPE_HINT;
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: TypeHint = 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    fn extract(ob: Borrowed<'_, '_, PyAny>) -> Result<Self, Self::Error> {
505        let py_utc = PyTzInfo::utc(ob.py())?;
506        if ob.eq(py_utc)? {
507            Ok(Utc)
508        } else {
509            Err(PyValueError::new_err("expected datetime.timezone.utc"))
510        }
511    }
512}
513
514#[cfg(feature = "chrono-local")]
515impl<'py> IntoPyObject<'py> for Local {
516    type Target = PyTzInfo;
517    type Output = Borrowed<'static, 'py, Self::Target>;
518    type Error = PyErr;
519
520    #[cfg(feature = "experimental-inspect")]
521    const OUTPUT_TYPE: TypeHint = PyTzInfo::TYPE_HINT;
522
523    fn into_pyobject(self, py: Python<'py>) -> Result<Self::Output, Self::Error> {
524        static LOCAL_TZ: PyOnceLock<Py<PyTzInfo>> = PyOnceLock::new();
525        let tz = LOCAL_TZ
526            .get_or_try_init(py, || {
527                let iana_name = iana_time_zone::get_timezone().map_err(|e| {
528                    PyRuntimeError::new_err(format!("Could not get local timezone: {e}"))
529                })?;
530                PyTzInfo::timezone(py, iana_name).map(Bound::unbind)
531            })?
532            .bind_borrowed(py);
533        Ok(tz)
534    }
535}
536
537#[cfg(feature = "chrono-local")]
538impl<'py> IntoPyObject<'py> for &Local {
539    type Target = PyTzInfo;
540    type Output = Borrowed<'static, 'py, Self::Target>;
541    type Error = PyErr;
542
543    #[cfg(feature = "experimental-inspect")]
544    const OUTPUT_TYPE: TypeHint = Local::OUTPUT_TYPE;
545
546    #[inline]
547    fn into_pyobject(self, py: Python<'py>) -> Result<Self::Output, Self::Error> {
548        (*self).into_pyobject(py)
549    }
550}
551
552#[cfg(feature = "chrono-local")]
553impl FromPyObject<'_, '_> for Local {
554    type Error = PyErr;
555
556    fn extract(ob: Borrowed<'_, '_, PyAny>) -> PyResult<Local> {
557        let local_tz = Local.into_pyobject(ob.py())?;
558        if ob.eq(local_tz)? {
559            Ok(Local)
560        } else {
561            let name = local_tz.getattr("key")?.cast_into::<PyString>()?;
562            Err(PyValueError::new_err(format!(
563                "expected local timezone {}",
564                name.to_cow()?
565            )))
566        }
567    }
568
569    #[inline]
570    fn as_local_tz(_: crate::conversion::private::Token) -> Option<Self> {
571        Some(Local)
572    }
573}
574
575struct DateArgs {
576    year: i32,
577    month: u8,
578    day: u8,
579}
580
581impl From<&NaiveDate> for DateArgs {
582    fn from(value: &NaiveDate) -> Self {
583        Self {
584            year: value.year(),
585            month: value.month() as u8,
586            day: value.day() as u8,
587        }
588    }
589}
590
591struct TimeArgs {
592    hour: u8,
593    min: u8,
594    sec: u8,
595    micro: u32,
596    truncated_leap_second: bool,
597}
598
599impl From<&NaiveTime> for TimeArgs {
600    fn from(value: &NaiveTime) -> Self {
601        let ns = value.nanosecond();
602        let checked_sub = ns.checked_sub(1_000_000_000);
603        let truncated_leap_second = checked_sub.is_some();
604        let micro = checked_sub.unwrap_or(ns) / 1000;
605        Self {
606            hour: value.hour() as u8,
607            min: value.minute() as u8,
608            sec: value.second() as u8,
609            micro,
610            truncated_leap_second,
611        }
612    }
613}
614
615fn warn_truncated_leap_second(obj: &Bound<'_, PyAny>) {
616    let py = obj.py();
617    if let Err(e) = PyErr::warn(
618        py,
619        &py.get_type::<PyUserWarning>(),
620        c"ignored leap-second, `datetime` does not support leap-seconds",
621        0,
622    ) {
623        e.write_unraisable(py, Some(obj))
624    };
625}
626
627#[cfg(not(Py_LIMITED_API))]
628fn py_date_to_naive_date(
629    py_date: impl std::ops::Deref<Target = impl PyDateAccess>,
630) -> PyResult<NaiveDate> {
631    NaiveDate::from_ymd_opt(
632        py_date.get_year(),
633        py_date.get_month().into(),
634        py_date.get_day().into(),
635    )
636    .ok_or_else(|| PyValueError::new_err("invalid or out-of-range date"))
637}
638
639#[cfg(Py_LIMITED_API)]
640fn py_date_to_naive_date(py_date: &Bound<'_, PyAny>) -> PyResult<NaiveDate> {
641    NaiveDate::from_ymd_opt(
642        py_date.getattr(intern!(py_date.py(), "year"))?.extract()?,
643        py_date.getattr(intern!(py_date.py(), "month"))?.extract()?,
644        py_date.getattr(intern!(py_date.py(), "day"))?.extract()?,
645    )
646    .ok_or_else(|| PyValueError::new_err("invalid or out-of-range date"))
647}
648
649#[cfg(not(Py_LIMITED_API))]
650fn py_time_to_naive_time(
651    py_time: impl std::ops::Deref<Target = impl PyTimeAccess>,
652) -> PyResult<NaiveTime> {
653    NaiveTime::from_hms_micro_opt(
654        py_time.get_hour().into(),
655        py_time.get_minute().into(),
656        py_time.get_second().into(),
657        py_time.get_microsecond(),
658    )
659    .ok_or_else(|| PyValueError::new_err("invalid or out-of-range time"))
660}
661
662#[cfg(Py_LIMITED_API)]
663fn py_time_to_naive_time(py_time: &Bound<'_, PyAny>) -> PyResult<NaiveTime> {
664    NaiveTime::from_hms_micro_opt(
665        py_time.getattr(intern!(py_time.py(), "hour"))?.extract()?,
666        py_time
667            .getattr(intern!(py_time.py(), "minute"))?
668            .extract()?,
669        py_time
670            .getattr(intern!(py_time.py(), "second"))?
671            .extract()?,
672        py_time
673            .getattr(intern!(py_time.py(), "microsecond"))?
674            .extract()?,
675    )
676    .ok_or_else(|| PyValueError::new_err("invalid or out-of-range time"))
677}
678
679fn py_datetime_to_datetime_with_timezone<Tz: TimeZone>(
680    dt: &Bound<'_, PyDateTime>,
681    tz: Tz,
682) -> PyResult<DateTime<Tz>> {
683    let naive_dt = NaiveDateTime::new(py_date_to_naive_date(dt)?, py_time_to_naive_time(dt)?);
684    match naive_dt.and_local_timezone(tz) {
685        LocalResult::Single(value) => Ok(value),
686        LocalResult::Ambiguous(earliest, latest) => {
687            #[cfg(not(Py_LIMITED_API))]
688            let fold = dt.get_fold();
689
690            #[cfg(Py_LIMITED_API)]
691            let fold = dt.getattr(intern!(dt.py(), "fold"))?.extract::<usize>()? > 0;
692
693            if fold {
694                Ok(latest)
695            } else {
696                Ok(earliest)
697            }
698        }
699        LocalResult::None => Err(PyValueError::new_err(format!(
700            "The datetime {dt:?} contains an incompatible timezone"
701        ))),
702    }
703}
704
705#[cfg(test)]
706mod tests {
707    use super::*;
708    use crate::{test_utils::assert_warnings, types::PyTuple, BoundObject};
709    use std::{cmp::Ordering, panic};
710
711    #[test]
712    // Only Python>=3.9 has the zoneinfo package
713    // We skip the test on windows too since we'd need to install
714    // tzdata there to make this work.
715    #[cfg(all(Py_3_9, not(target_os = "windows")))]
716    fn test_zoneinfo_is_not_fixed_offset() {
717        use crate::types::any::PyAnyMethods;
718        use crate::types::dict::PyDictMethods;
719
720        Python::attach(|py| {
721            let locals = crate::types::PyDict::new(py);
722            py.run(
723                c"import zoneinfo; zi = zoneinfo.ZoneInfo('Europe/London')",
724                None,
725                Some(&locals),
726            )
727            .unwrap();
728            let result: PyResult<FixedOffset> = locals.get_item("zi").unwrap().unwrap().extract();
729            assert!(result.is_err());
730            let res = result.err().unwrap();
731            // Also check the error message is what we expect
732            let msg = res.value(py).repr().unwrap().to_string();
733            assert_eq!(msg, "TypeError(\"zoneinfo.ZoneInfo(key='Europe/London') is not a fixed offset timezone\")");
734        });
735    }
736
737    #[test]
738    fn test_timezone_aware_to_naive_fails() {
739        // Test that if a user tries to convert a python's timezone aware datetime into a naive
740        // one, the conversion fails.
741        Python::attach(|py| {
742            let py_datetime =
743                new_py_datetime_ob(py, "datetime", (2022, 1, 1, 1, 0, 0, 0, python_utc(py)));
744            // Now test that converting a PyDateTime with tzinfo to a NaiveDateTime fails
745            let res: PyResult<NaiveDateTime> = py_datetime.extract();
746            assert_eq!(
747                res.unwrap_err().value(py).repr().unwrap().to_string(),
748                "TypeError('expected a datetime without tzinfo')"
749            );
750        });
751    }
752
753    #[test]
754    fn test_naive_to_timezone_aware_fails() {
755        // Test that if a user tries to convert a python's timezone aware datetime into a naive
756        // one, the conversion fails.
757        Python::attach(|py| {
758            let py_datetime = new_py_datetime_ob(py, "datetime", (2022, 1, 1, 1, 0, 0, 0));
759            // Now test that converting a PyDateTime with tzinfo to a NaiveDateTime fails
760            let res: PyResult<DateTime<Utc>> = py_datetime.extract();
761            assert_eq!(
762                res.unwrap_err().value(py).repr().unwrap().to_string(),
763                "TypeError('expected a datetime with non-None tzinfo')"
764            );
765
766            // Now test that converting a PyDateTime with tzinfo to a NaiveDateTime fails
767            let res: PyResult<DateTime<FixedOffset>> = py_datetime.extract();
768            assert_eq!(
769                res.unwrap_err().value(py).repr().unwrap().to_string(),
770                "TypeError('expected a datetime with non-None tzinfo')"
771            );
772        });
773    }
774
775    #[test]
776    fn test_invalid_types_fail() {
777        // Test that if a user tries to convert a python's timezone aware datetime into a naive
778        // one, the conversion fails.
779        Python::attach(|py| {
780            let none = py.None().into_bound(py);
781            assert_eq!(
782                none.extract::<Duration>().unwrap_err().to_string(),
783                "TypeError: 'NoneType' object cannot be cast as 'timedelta'"
784            );
785            assert_eq!(
786                none.extract::<FixedOffset>().unwrap_err().to_string(),
787                "TypeError: 'NoneType' object cannot be cast as 'tzinfo'"
788            );
789            assert_eq!(
790                none.extract::<Utc>().unwrap_err().to_string(),
791                "ValueError: expected datetime.timezone.utc"
792            );
793            assert_eq!(
794                none.extract::<NaiveTime>().unwrap_err().to_string(),
795                "TypeError: 'NoneType' object cannot be cast as 'time'"
796            );
797            assert_eq!(
798                none.extract::<NaiveDate>().unwrap_err().to_string(),
799                "TypeError: 'NoneType' object cannot be cast as 'date'"
800            );
801            assert_eq!(
802                none.extract::<NaiveDateTime>().unwrap_err().to_string(),
803                "TypeError: 'NoneType' object cannot be cast as 'datetime'"
804            );
805            assert_eq!(
806                none.extract::<DateTime<Utc>>().unwrap_err().to_string(),
807                "TypeError: 'NoneType' object cannot be cast as 'datetime'"
808            );
809            assert_eq!(
810                none.extract::<DateTime<FixedOffset>>()
811                    .unwrap_err()
812                    .to_string(),
813                "TypeError: 'NoneType' object cannot be cast as 'datetime'"
814            );
815        });
816    }
817
818    #[test]
819    fn test_pyo3_timedelta_into_pyobject() {
820        // Utility function used to check different durations.
821        // The `name` parameter is used to identify the check in case of a failure.
822        let check = |name: &'static str, delta: Duration, py_days, py_seconds, py_ms| {
823            Python::attach(|py| {
824                let delta = delta.into_pyobject(py).unwrap();
825                let py_delta = new_py_datetime_ob(py, "timedelta", (py_days, py_seconds, py_ms));
826                assert!(
827                    delta.eq(&py_delta).unwrap(),
828                    "{name}: {delta} != {py_delta}"
829                );
830            });
831        };
832
833        let delta = Duration::days(-1) + Duration::seconds(1) + Duration::microseconds(-10);
834        check("delta normalization", delta, -1, 1, -10);
835
836        // Check the minimum value allowed by PyDelta, which is different
837        // from the minimum value allowed in Duration. This should pass.
838        let delta = Duration::seconds(-86399999913600); // min
839        check("delta min value", delta, -999999999, 0, 0);
840
841        // Same, for max value
842        let delta = Duration::seconds(86399999999999) + Duration::nanoseconds(999999000); // max
843        check("delta max value", delta, 999999999, 86399, 999999);
844
845        // Also check that trying to convert an out of bound value errors.
846        Python::attach(|py| {
847            // min_value and max_value were deprecated in chrono 0.4.39
848            #[allow(deprecated)]
849            {
850                assert!(Duration::min_value().into_pyobject(py).is_err());
851                assert!(Duration::max_value().into_pyobject(py).is_err());
852            }
853        });
854    }
855
856    #[test]
857    fn test_pyo3_timedelta_frompyobject() {
858        // Utility function used to check different durations.
859        // The `name` parameter is used to identify the check in case of a failure.
860        let check = |name: &'static str, delta: Duration, py_days, py_seconds, py_ms| {
861            Python::attach(|py| {
862                let py_delta = new_py_datetime_ob(py, "timedelta", (py_days, py_seconds, py_ms));
863                let py_delta: Duration = py_delta.extract().unwrap();
864                assert_eq!(py_delta, delta, "{name}: {py_delta} != {delta}");
865            })
866        };
867
868        // Check the minimum value allowed by PyDelta, which is different
869        // from the minimum value allowed in Duration. This should pass.
870        check(
871            "min py_delta value",
872            Duration::seconds(-86399999913600),
873            -999999999,
874            0,
875            0,
876        );
877        // Same, for max value
878        check(
879            "max py_delta value",
880            Duration::seconds(86399999999999) + Duration::microseconds(999999),
881            999999999,
882            86399,
883            999999,
884        );
885
886        // This check is to assert that we can't construct every possible Duration from a PyDelta
887        // since they have different bounds.
888        Python::attach(|py| {
889            let low_days: i32 = -1000000000;
890            // This is possible
891            assert!(panic::catch_unwind(|| Duration::days(low_days as i64)).is_ok());
892            // This panics on PyDelta::new
893            assert!(panic::catch_unwind(|| {
894                let py_delta = new_py_datetime_ob(py, "timedelta", (low_days, 0, 0));
895                if let Ok(_duration) = py_delta.extract::<Duration>() {
896                    // So we should never get here
897                }
898            })
899            .is_err());
900
901            let high_days: i32 = 1000000000;
902            // This is possible
903            assert!(panic::catch_unwind(|| Duration::days(high_days as i64)).is_ok());
904            // This panics on PyDelta::new
905            assert!(panic::catch_unwind(|| {
906                let py_delta = new_py_datetime_ob(py, "timedelta", (high_days, 0, 0));
907                if let Ok(_duration) = py_delta.extract::<Duration>() {
908                    // So we should never get here
909                }
910            })
911            .is_err());
912        });
913    }
914
915    #[test]
916    fn test_pyo3_date_into_pyobject() {
917        let eq_ymd = |name: &'static str, year, month, day| {
918            Python::attach(|py| {
919                let date = NaiveDate::from_ymd_opt(year, month, day)
920                    .unwrap()
921                    .into_pyobject(py)
922                    .unwrap();
923                let py_date = new_py_datetime_ob(py, "date", (year, month, day));
924                assert_eq!(
925                    date.compare(&py_date).unwrap(),
926                    Ordering::Equal,
927                    "{name}: {date} != {py_date}"
928                );
929            })
930        };
931
932        eq_ymd("past date", 2012, 2, 29);
933        eq_ymd("min date", 1, 1, 1);
934        eq_ymd("future date", 3000, 6, 5);
935        eq_ymd("max date", 9999, 12, 31);
936    }
937
938    #[test]
939    fn test_pyo3_date_frompyobject() {
940        let eq_ymd = |name: &'static str, year, month, day| {
941            Python::attach(|py| {
942                let py_date = new_py_datetime_ob(py, "date", (year, month, day));
943                let py_date: NaiveDate = py_date.extract().unwrap();
944                let date = NaiveDate::from_ymd_opt(year, month, day).unwrap();
945                assert_eq!(py_date, date, "{name}: {date} != {py_date}");
946            })
947        };
948
949        eq_ymd("past date", 2012, 2, 29);
950        eq_ymd("min date", 1, 1, 1);
951        eq_ymd("future date", 3000, 6, 5);
952        eq_ymd("max date", 9999, 12, 31);
953    }
954
955    #[test]
956    fn test_pyo3_datetime_into_pyobject_utc() {
957        Python::attach(|py| {
958            let check_utc =
959                |name: &'static str, year, month, day, hour, minute, second, ms, py_ms| {
960                    let datetime = NaiveDate::from_ymd_opt(year, month, day)
961                        .unwrap()
962                        .and_hms_micro_opt(hour, minute, second, ms)
963                        .unwrap()
964                        .and_utc();
965                    let datetime = datetime.into_pyobject(py).unwrap();
966                    let py_datetime = new_py_datetime_ob(
967                        py,
968                        "datetime",
969                        (
970                            year,
971                            month,
972                            day,
973                            hour,
974                            minute,
975                            second,
976                            py_ms,
977                            python_utc(py),
978                        ),
979                    );
980                    assert_eq!(
981                        datetime.compare(&py_datetime).unwrap(),
982                        Ordering::Equal,
983                        "{name}: {datetime} != {py_datetime}"
984                    );
985                };
986
987            check_utc("regular", 2014, 5, 6, 7, 8, 9, 999_999, 999_999);
988
989            assert_warnings!(
990                py,
991                check_utc("leap second", 2014, 5, 6, 7, 8, 59, 1_999_999, 999_999),
992                [(
993                    PyUserWarning,
994                    "ignored leap-second, `datetime` does not support leap-seconds"
995                )]
996            );
997        })
998    }
999
1000    #[test]
1001    fn test_pyo3_datetime_into_pyobject_fixed_offset() {
1002        Python::attach(|py| {
1003            let check_fixed_offset =
1004                |name: &'static str, year, month, day, hour, minute, second, ms, py_ms| {
1005                    let offset = FixedOffset::east_opt(3600).unwrap();
1006                    let datetime = NaiveDate::from_ymd_opt(year, month, day)
1007                        .unwrap()
1008                        .and_hms_micro_opt(hour, minute, second, ms)
1009                        .unwrap()
1010                        .and_local_timezone(offset)
1011                        .unwrap();
1012                    let datetime = datetime.into_pyobject(py).unwrap();
1013                    let py_tz = offset.into_pyobject(py).unwrap();
1014                    let py_datetime = new_py_datetime_ob(
1015                        py,
1016                        "datetime",
1017                        (year, month, day, hour, minute, second, py_ms, py_tz),
1018                    );
1019                    assert_eq!(
1020                        datetime.compare(&py_datetime).unwrap(),
1021                        Ordering::Equal,
1022                        "{name}: {datetime} != {py_datetime}"
1023                    );
1024                };
1025
1026            check_fixed_offset("regular", 2014, 5, 6, 7, 8, 9, 999_999, 999_999);
1027
1028            assert_warnings!(
1029                py,
1030                check_fixed_offset("leap second", 2014, 5, 6, 7, 8, 59, 1_999_999, 999_999),
1031                [(
1032                    PyUserWarning,
1033                    "ignored leap-second, `datetime` does not support leap-seconds"
1034                )]
1035            );
1036        })
1037    }
1038
1039    #[test]
1040    #[cfg(all(Py_3_9, feature = "chrono-tz", not(windows)))]
1041    fn test_pyo3_datetime_into_pyobject_tz() {
1042        Python::attach(|py| {
1043            let datetime = NaiveDate::from_ymd_opt(2024, 12, 11)
1044                .unwrap()
1045                .and_hms_opt(23, 3, 13)
1046                .unwrap()
1047                .and_local_timezone(chrono_tz::Tz::Europe__London)
1048                .unwrap();
1049            let datetime = datetime.into_pyobject(py).unwrap();
1050            let py_datetime = new_py_datetime_ob(
1051                py,
1052                "datetime",
1053                (
1054                    2024,
1055                    12,
1056                    11,
1057                    23,
1058                    3,
1059                    13,
1060                    0,
1061                    python_zoneinfo(py, "Europe/London"),
1062                ),
1063            );
1064            assert_eq!(datetime.compare(&py_datetime).unwrap(), Ordering::Equal);
1065        })
1066    }
1067
1068    #[test]
1069    fn test_pyo3_datetime_frompyobject_utc() {
1070        Python::attach(|py| {
1071            let year = 2014;
1072            let month = 5;
1073            let day = 6;
1074            let hour = 7;
1075            let minute = 8;
1076            let second = 9;
1077            let micro = 999_999;
1078            let tz_utc = PyTzInfo::utc(py).unwrap();
1079            let py_datetime = new_py_datetime_ob(
1080                py,
1081                "datetime",
1082                (year, month, day, hour, minute, second, micro, tz_utc),
1083            );
1084            let py_datetime: DateTime<Utc> = py_datetime.extract().unwrap();
1085            let datetime = NaiveDate::from_ymd_opt(year, month, day)
1086                .unwrap()
1087                .and_hms_micro_opt(hour, minute, second, micro)
1088                .unwrap()
1089                .and_utc();
1090            assert_eq!(py_datetime, datetime,);
1091        })
1092    }
1093
1094    #[test]
1095    #[cfg(feature = "chrono-local")]
1096    fn test_pyo3_naive_datetime_frompyobject_local() {
1097        Python::attach(|py| {
1098            let year = 2014;
1099            let month = 5;
1100            let day = 6;
1101            let hour = 7;
1102            let minute = 8;
1103            let second = 9;
1104            let micro = 999_999;
1105            let py_datetime = new_py_datetime_ob(
1106                py,
1107                "datetime",
1108                (year, month, day, hour, minute, second, micro),
1109            );
1110            let py_datetime: DateTime<Local> = py_datetime.extract().unwrap();
1111            let expected_datetime = NaiveDate::from_ymd_opt(year, month, day)
1112                .unwrap()
1113                .and_hms_micro_opt(hour, minute, second, micro)
1114                .unwrap()
1115                .and_local_timezone(Local)
1116                .unwrap();
1117            assert_eq!(py_datetime, expected_datetime);
1118        })
1119    }
1120
1121    #[test]
1122    fn test_pyo3_datetime_frompyobject_fixed_offset() {
1123        Python::attach(|py| {
1124            let year = 2014;
1125            let month = 5;
1126            let day = 6;
1127            let hour = 7;
1128            let minute = 8;
1129            let second = 9;
1130            let micro = 999_999;
1131            let offset = FixedOffset::east_opt(3600).unwrap();
1132            let py_tz = offset.into_pyobject(py).unwrap();
1133            let py_datetime = new_py_datetime_ob(
1134                py,
1135                "datetime",
1136                (year, month, day, hour, minute, second, micro, py_tz),
1137            );
1138            let datetime_from_py: DateTime<FixedOffset> = py_datetime.extract().unwrap();
1139            let datetime = NaiveDate::from_ymd_opt(year, month, day)
1140                .unwrap()
1141                .and_hms_micro_opt(hour, minute, second, micro)
1142                .unwrap();
1143            let datetime = datetime.and_local_timezone(offset).unwrap();
1144
1145            assert_eq!(datetime_from_py, datetime);
1146            assert!(
1147                py_datetime.extract::<DateTime<Utc>>().is_err(),
1148                "Extracting Utc from nonzero FixedOffset timezone will fail"
1149            );
1150
1151            let utc = python_utc(py);
1152            let py_datetime_utc = new_py_datetime_ob(
1153                py,
1154                "datetime",
1155                (year, month, day, hour, minute, second, micro, utc),
1156            );
1157            assert!(
1158                py_datetime_utc.extract::<DateTime<FixedOffset>>().is_ok(),
1159                "Extracting FixedOffset from Utc timezone will succeed"
1160            );
1161        })
1162    }
1163
1164    #[test]
1165    fn test_pyo3_offset_fixed_into_pyobject() {
1166        Python::attach(|py| {
1167            // Chrono offset
1168            let offset = FixedOffset::east_opt(3600)
1169                .unwrap()
1170                .into_pyobject(py)
1171                .unwrap();
1172            // Python timezone from timedelta
1173            let td = new_py_datetime_ob(py, "timedelta", (0, 3600, 0));
1174            let py_timedelta = new_py_datetime_ob(py, "timezone", (td,));
1175            // Should be equal
1176            assert!(offset.eq(py_timedelta).unwrap());
1177
1178            // Same but with negative values
1179            let offset = FixedOffset::east_opt(-3600)
1180                .unwrap()
1181                .into_pyobject(py)
1182                .unwrap();
1183            let td = new_py_datetime_ob(py, "timedelta", (0, -3600, 0));
1184            let py_timedelta = new_py_datetime_ob(py, "timezone", (td,));
1185            assert!(offset.eq(py_timedelta).unwrap());
1186        })
1187    }
1188
1189    #[test]
1190    fn test_pyo3_offset_fixed_frompyobject() {
1191        Python::attach(|py| {
1192            let py_timedelta = new_py_datetime_ob(py, "timedelta", (0, 3600, 0));
1193            let py_tzinfo = new_py_datetime_ob(py, "timezone", (py_timedelta,));
1194            let offset: FixedOffset = py_tzinfo.extract().unwrap();
1195            assert_eq!(FixedOffset::east_opt(3600).unwrap(), offset);
1196        })
1197    }
1198
1199    #[test]
1200    fn test_pyo3_offset_utc_into_pyobject() {
1201        Python::attach(|py| {
1202            let utc = Utc.into_pyobject(py).unwrap();
1203            let py_utc = python_utc(py);
1204            assert!(utc.is(&py_utc));
1205        })
1206    }
1207
1208    #[test]
1209    fn test_pyo3_offset_utc_frompyobject() {
1210        Python::attach(|py| {
1211            let py_utc = python_utc(py);
1212            let py_utc: Utc = py_utc.extract().unwrap();
1213            assert_eq!(Utc, py_utc);
1214
1215            let py_timedelta = new_py_datetime_ob(py, "timedelta", (0, 0, 0));
1216            let py_timezone_utc = new_py_datetime_ob(py, "timezone", (py_timedelta,));
1217            let py_timezone_utc: Utc = py_timezone_utc.extract().unwrap();
1218            assert_eq!(Utc, py_timezone_utc);
1219
1220            let py_timedelta = new_py_datetime_ob(py, "timedelta", (0, 3600, 0));
1221            let py_timezone = new_py_datetime_ob(py, "timezone", (py_timedelta,));
1222            assert!(py_timezone.extract::<Utc>().is_err());
1223        })
1224    }
1225
1226    #[test]
1227    fn test_pyo3_time_into_pyobject() {
1228        Python::attach(|py| {
1229            let check_time = |name: &'static str, hour, minute, second, ms, py_ms| {
1230                let time = NaiveTime::from_hms_micro_opt(hour, minute, second, ms)
1231                    .unwrap()
1232                    .into_pyobject(py)
1233                    .unwrap();
1234                let py_time = new_py_datetime_ob(py, "time", (hour, minute, second, py_ms));
1235                assert!(time.eq(&py_time).unwrap(), "{name}: {time} != {py_time}");
1236            };
1237
1238            check_time("regular", 3, 5, 7, 999_999, 999_999);
1239
1240            assert_warnings!(
1241                py,
1242                check_time("leap second", 3, 5, 59, 1_999_999, 999_999),
1243                [(
1244                    PyUserWarning,
1245                    "ignored leap-second, `datetime` does not support leap-seconds"
1246                )]
1247            );
1248        })
1249    }
1250
1251    #[test]
1252    fn test_pyo3_time_frompyobject() {
1253        let hour = 3;
1254        let minute = 5;
1255        let second = 7;
1256        let micro = 999_999;
1257        Python::attach(|py| {
1258            let py_time = new_py_datetime_ob(py, "time", (hour, minute, second, micro));
1259            let py_time: NaiveTime = py_time.extract().unwrap();
1260            let time = NaiveTime::from_hms_micro_opt(hour, minute, second, micro).unwrap();
1261            assert_eq!(py_time, time);
1262        })
1263    }
1264
1265    fn new_py_datetime_ob<'py, A>(py: Python<'py>, name: &str, args: A) -> Bound<'py, PyAny>
1266    where
1267        A: IntoPyObject<'py, Target = PyTuple>,
1268    {
1269        py.import("datetime")
1270            .unwrap()
1271            .getattr(name)
1272            .unwrap()
1273            .call1(
1274                args.into_pyobject(py)
1275                    .map_err(Into::into)
1276                    .unwrap()
1277                    .into_bound(),
1278            )
1279            .unwrap()
1280    }
1281
1282    fn python_utc(py: Python<'_>) -> Bound<'_, PyAny> {
1283        py.import("datetime")
1284            .unwrap()
1285            .getattr("timezone")
1286            .unwrap()
1287            .getattr("utc")
1288            .unwrap()
1289    }
1290
1291    #[cfg(all(Py_3_9, feature = "chrono-tz", not(windows)))]
1292    fn python_zoneinfo<'py>(py: Python<'py>, timezone: &str) -> Bound<'py, PyAny> {
1293        py.import("zoneinfo")
1294            .unwrap()
1295            .getattr("ZoneInfo")
1296            .unwrap()
1297            .call1((timezone,))
1298            .unwrap()
1299    }
1300
1301    #[cfg(not(any(target_arch = "wasm32")))]
1302    mod proptests {
1303        use super::*;
1304        use crate::test_utils::CatchWarnings;
1305        use crate::types::IntoPyDict;
1306        use proptest::prelude::*;
1307        use std::ffi::CString;
1308
1309        proptest! {
1310
1311            // Range is limited to 1970 to 2038 due to windows limitations
1312            #[test]
1313            fn test_pyo3_offset_fixed_frompyobject_created_in_python(timestamp in 0..(i32::MAX as i64), timedelta in -86399i32..=86399i32) {
1314                Python::attach(|py| {
1315
1316                    let globals = [("datetime", py.import("datetime").unwrap())].into_py_dict(py).unwrap();
1317                    let code = format!("datetime.datetime.fromtimestamp({timestamp}).replace(tzinfo=datetime.timezone(datetime.timedelta(seconds={timedelta})))");
1318                    let t = py.eval(&CString::new(code).unwrap(), Some(&globals), None).unwrap();
1319
1320                    // Get ISO 8601 string from python
1321                    let py_iso_str = t.call_method0("isoformat").unwrap();
1322
1323                    // Get ISO 8601 string from rust
1324                    let t = t.extract::<DateTime<FixedOffset>>().unwrap();
1325                    // Python doesn't print the seconds of the offset if they are 0
1326                    let rust_iso_str = if timedelta % 60 == 0 {
1327                        t.format("%Y-%m-%dT%H:%M:%S%:z").to_string()
1328                    } else {
1329                        t.format("%Y-%m-%dT%H:%M:%S%::z").to_string()
1330                    };
1331
1332                    // They should be equal
1333                    assert_eq!(py_iso_str.to_string(), rust_iso_str);
1334                })
1335            }
1336
1337            #[test]
1338            fn test_duration_roundtrip(days in -999999999i64..=999999999i64) {
1339                // Test roundtrip conversion rust->python->rust for all allowed
1340                // python values of durations (from -999999999 to 999999999 days),
1341                Python::attach(|py| {
1342                    let dur = Duration::days(days);
1343                    let py_delta = dur.into_pyobject(py).unwrap();
1344                    let roundtripped: Duration = py_delta.extract().expect("Round trip");
1345                    assert_eq!(dur, roundtripped);
1346                })
1347            }
1348
1349            #[test]
1350            fn test_fixed_offset_roundtrip(secs in -86399i32..=86399i32) {
1351                Python::attach(|py| {
1352                    let offset = FixedOffset::east_opt(secs).unwrap();
1353                    let py_offset = offset.into_pyobject(py).unwrap();
1354                    let roundtripped: FixedOffset = py_offset.extract().expect("Round trip");
1355                    assert_eq!(offset, roundtripped);
1356                })
1357            }
1358
1359            #[test]
1360            fn test_naive_date_roundtrip(
1361                year in 1i32..=9999i32,
1362                month in 1u32..=12u32,
1363                day in 1u32..=31u32
1364            ) {
1365                // Test roundtrip conversion rust->python->rust for all allowed
1366                // python dates (from year 1 to year 9999)
1367                Python::attach(|py| {
1368                    // We use to `from_ymd_opt` constructor so that we only test valid `NaiveDate`s.
1369                    // This is to skip the test if we are creating an invalid date, like February 31.
1370                    if let Some(date) = NaiveDate::from_ymd_opt(year, month, day) {
1371                        let py_date = date.into_pyobject(py).unwrap();
1372                        let roundtripped: NaiveDate = py_date.extract().expect("Round trip");
1373                        assert_eq!(date, roundtripped);
1374                    }
1375                })
1376            }
1377
1378            #[test]
1379            fn test_naive_time_roundtrip(
1380                hour in 0u32..=23u32,
1381                min in 0u32..=59u32,
1382                sec in 0u32..=59u32,
1383                micro in 0u32..=1_999_999u32
1384            ) {
1385                // Test roundtrip conversion rust->python->rust for naive times.
1386                // Python time has a resolution of microseconds, so we only test
1387                // NaiveTimes with microseconds resolution, even if NaiveTime has nanosecond
1388                // resolution.
1389                Python::attach(|py| {
1390                    if let Some(time) = NaiveTime::from_hms_micro_opt(hour, min, sec, micro) {
1391                        // Wrap in CatchWarnings to avoid to_object firing warning for truncated leap second
1392                        let py_time = CatchWarnings::enter(py, |_| time.into_pyobject(py)).unwrap();
1393                        let roundtripped: NaiveTime = py_time.extract().expect("Round trip");
1394                        // Leap seconds are not roundtripped
1395                        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);
1396                        assert_eq!(expected_roundtrip_time, roundtripped);
1397                    }
1398                })
1399            }
1400
1401            #[test]
1402            fn test_naive_datetime_roundtrip(
1403                year in 1i32..=9999i32,
1404                month in 1u32..=12u32,
1405                day in 1u32..=31u32,
1406                hour in 0u32..=24u32,
1407                min in 0u32..=60u32,
1408                sec in 0u32..=60u32,
1409                micro in 0u32..=999_999u32
1410            ) {
1411                Python::attach(|py| {
1412                    let date_opt = NaiveDate::from_ymd_opt(year, month, day);
1413                    let time_opt = NaiveTime::from_hms_micro_opt(hour, min, sec, micro);
1414                    if let (Some(date), Some(time)) = (date_opt, time_opt) {
1415                        let dt = NaiveDateTime::new(date, time);
1416                        let pydt = dt.into_pyobject(py).unwrap();
1417                        let roundtripped: NaiveDateTime = pydt.extract().expect("Round trip");
1418                        assert_eq!(dt, roundtripped);
1419                    }
1420                })
1421            }
1422
1423            #[test]
1424            fn test_utc_datetime_roundtrip(
1425                year in 1i32..=9999i32,
1426                month in 1u32..=12u32,
1427                day in 1u32..=31u32,
1428                hour in 0u32..=23u32,
1429                min in 0u32..=59u32,
1430                sec in 0u32..=59u32,
1431                micro in 0u32..=1_999_999u32
1432            ) {
1433                Python::attach(|py| {
1434                    let date_opt = NaiveDate::from_ymd_opt(year, month, day);
1435                    let time_opt = NaiveTime::from_hms_micro_opt(hour, min, sec, micro);
1436                    if let (Some(date), Some(time)) = (date_opt, time_opt) {
1437                        let dt: DateTime<Utc> = NaiveDateTime::new(date, time).and_utc();
1438                        // Wrap in CatchWarnings to avoid into_py firing warning for truncated leap second
1439                        let py_dt = CatchWarnings::enter(py, |_| dt.into_pyobject(py)).unwrap();
1440                        let roundtripped: DateTime<Utc> = py_dt.extract().expect("Round trip");
1441                        // Leap seconds are not roundtripped
1442                        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);
1443                        let expected_roundtrip_dt: DateTime<Utc> = NaiveDateTime::new(date, expected_roundtrip_time).and_utc();
1444                        assert_eq!(expected_roundtrip_dt, roundtripped);
1445                    }
1446                })
1447            }
1448
1449            #[test]
1450            fn test_fixed_offset_datetime_roundtrip(
1451                year in 1i32..=9999i32,
1452                month in 1u32..=12u32,
1453                day in 1u32..=31u32,
1454                hour in 0u32..=23u32,
1455                min in 0u32..=59u32,
1456                sec in 0u32..=59u32,
1457                micro in 0u32..=1_999_999u32,
1458                offset_secs in -86399i32..=86399i32
1459            ) {
1460                Python::attach(|py| {
1461                    let date_opt = NaiveDate::from_ymd_opt(year, month, day);
1462                    let time_opt = NaiveTime::from_hms_micro_opt(hour, min, sec, micro);
1463                    let offset = FixedOffset::east_opt(offset_secs).unwrap();
1464                    if let (Some(date), Some(time)) = (date_opt, time_opt) {
1465                        let dt: DateTime<FixedOffset> = NaiveDateTime::new(date, time).and_local_timezone(offset).unwrap();
1466                        // Wrap in CatchWarnings to avoid into_py firing warning for truncated leap second
1467                        let py_dt = CatchWarnings::enter(py, |_| dt.into_pyobject(py)).unwrap();
1468                        let roundtripped: DateTime<FixedOffset> = py_dt.extract().expect("Round trip");
1469                        // Leap seconds are not roundtripped
1470                        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);
1471                        let expected_roundtrip_dt: DateTime<FixedOffset> = NaiveDateTime::new(date, expected_roundtrip_time).and_local_timezone(offset).unwrap();
1472                        assert_eq!(expected_roundtrip_dt, roundtripped);
1473                    }
1474                })
1475            }
1476
1477            #[test]
1478            #[cfg(all(feature = "chrono-local", not(target_os = "windows")))]
1479            fn test_local_datetime_roundtrip(
1480                year in 1i32..=9999i32,
1481                month in 1u32..=12u32,
1482                day in 1u32..=31u32,
1483                hour in 0u32..=23u32,
1484                min in 0u32..=59u32,
1485                sec in 0u32..=59u32,
1486                micro in 0u32..=1_999_999u32,
1487            ) {
1488                Python::attach(|py| {
1489                    let date_opt = NaiveDate::from_ymd_opt(year, month, day);
1490                    let time_opt = NaiveTime::from_hms_micro_opt(hour, min, sec, micro);
1491                    if let (Some(date), Some(time)) = (date_opt, time_opt) {
1492                        let dts = match NaiveDateTime::new(date, time).and_local_timezone(Local) {
1493                            LocalResult::None => return,
1494                            LocalResult::Single(dt) => [Some((dt, false)), None],
1495                            LocalResult::Ambiguous(dt1, dt2) => [Some((dt1, false)), Some((dt2, true))],
1496                        };
1497                        for (dt, fold) in dts.iter().filter_map(|input| *input) {
1498                            // Wrap in CatchWarnings to avoid into_py firing warning for truncated leap second
1499                            let py_dt = CatchWarnings::enter(py, |_| dt.into_pyobject(py)).unwrap();
1500                            let roundtripped: DateTime<Local> = py_dt.extract().expect("Round trip");
1501                            // Leap seconds are not roundtripped
1502                            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);
1503                            let expected_roundtrip_dt: DateTime<Local> = if fold {
1504                                NaiveDateTime::new(date, expected_roundtrip_time).and_local_timezone(Local).latest()
1505                            } else {
1506                                NaiveDateTime::new(date, expected_roundtrip_time).and_local_timezone(Local).earliest()
1507                            }.unwrap();
1508                            assert_eq!(expected_roundtrip_dt, roundtripped);
1509                        }
1510                    }
1511                })
1512            }
1513        }
1514    }
1515}