pyo3/conversions/
jiff.rs

1#![cfg(feature = "jiff-02")]
2
3//! Conversions to and from [jiff](https://docs.rs/jiff/)’s `Span`, `SignedDuration`, `TimeZone`,
4//! `Offset`, `Date`, `Time`, `DateTime`, `Zoned`, and `Timestamp`.
5//!
6//! # Setup
7//!
8//! To use this feature, add this to your **`Cargo.toml`**:
9//!
10//! ```toml
11//! [dependencies]
12//! jiff = "0.2"
13#![doc = concat!("pyo3 = { version = \"", env!("CARGO_PKG_VERSION"),  "\", features = [\"jiff-02\"] }")]
14//! ```
15//!
16//! Note that you must use compatible versions of jiff and PyO3.
17//! The required jiff version may vary based on the version of PyO3. Jiff also requires a MSRV
18//! of 1.70.
19//!
20//! # Example: Convert a `datetime.datetime` to jiff `Zoned`
21//!
22//! ```rust
23//! # #![cfg_attr(windows, allow(unused_imports))]
24//! # use jiff_02 as jiff;
25//! use jiff::{Zoned, SignedDuration, ToSpan};
26//! use pyo3::{Python, PyResult, IntoPyObject, types::PyAnyMethods};
27//!
28//! # #[cfg(windows)]
29//! # fn main() -> () {}
30//! # #[cfg(not(windows))]
31//! fn main() -> PyResult<()> {
32//!     pyo3::prepare_freethreaded_python();
33//!     Python::with_gil(|py| {
34//!         // Build some jiff values
35//!         let jiff_zoned = Zoned::now();
36//!         let jiff_span = 1.second();
37//!         // Convert them to Python
38//!         let py_datetime = jiff_zoned.into_pyobject(py)?;
39//!         let py_timedelta = SignedDuration::try_from(jiff_span)?.into_pyobject(py)?;
40//!         // Do an operation in Python
41//!         let py_sum = py_datetime.call_method1("__add__", (py_timedelta,))?;
42//!         // Convert back to Rust
43//!         let jiff_sum: Zoned = py_sum.extract()?;
44//!         println!("Zoned: {}", jiff_sum);
45//!         Ok(())
46//!     })
47//! }
48//! ```
49use crate::exceptions::{PyTypeError, PyValueError};
50use crate::pybacked::PyBackedStr;
51use crate::sync::GILOnceCell;
52#[cfg(not(Py_LIMITED_API))]
53use crate::types::datetime::timezone_from_offset;
54#[cfg(Py_LIMITED_API)]
55use crate::types::datetime_abi3::{check_type, timezone_utc, DatetimeTypes};
56#[cfg(Py_LIMITED_API)]
57use crate::types::IntoPyDict;
58#[cfg(not(Py_LIMITED_API))]
59use crate::types::{
60    timezone_utc, PyDate, PyDateAccess, PyDateTime, PyDelta, PyDeltaAccess, PyTime, PyTimeAccess,
61    PyTzInfo, PyTzInfoAccess,
62};
63use crate::types::{PyAnyMethods, PyNone, PyType};
64use crate::{intern, Bound, FromPyObject, IntoPyObject, Py, PyAny, PyErr, PyResult, Python};
65use jiff::civil::{Date, DateTime, Time};
66use jiff::tz::{Offset, TimeZone};
67use jiff::{SignedDuration, Span, Timestamp, Zoned};
68#[cfg(feature = "jiff-02")]
69use jiff_02 as jiff;
70
71#[cfg(not(Py_LIMITED_API))]
72fn datetime_to_pydatetime<'py>(
73    py: Python<'py>,
74    datetime: &DateTime,
75    fold: bool,
76    timezone: Option<&TimeZone>,
77) -> PyResult<Bound<'py, PyDateTime>> {
78    PyDateTime::new_with_fold(
79        py,
80        datetime.year().into(),
81        datetime.month().try_into()?,
82        datetime.day().try_into()?,
83        datetime.hour().try_into()?,
84        datetime.minute().try_into()?,
85        datetime.second().try_into()?,
86        (datetime.subsec_nanosecond() / 1000).try_into()?,
87        timezone
88            .map(|tz| tz.into_pyobject(py))
89            .transpose()?
90            .as_ref(),
91        fold,
92    )
93}
94
95#[cfg(Py_LIMITED_API)]
96fn datetime_to_pydatetime<'py>(
97    py: Python<'py>,
98    datetime: &DateTime,
99    fold: bool,
100    timezone: Option<&TimeZone>,
101) -> PyResult<Bound<'py, PyAny>> {
102    DatetimeTypes::try_get(py)?.datetime.bind(py).call(
103        (
104            datetime.year(),
105            datetime.month(),
106            datetime.day(),
107            datetime.hour(),
108            datetime.minute(),
109            datetime.second(),
110            datetime.subsec_nanosecond() / 1000,
111            timezone,
112        ),
113        Some(&[("fold", fold as u8)].into_py_dict(py)?),
114    )
115}
116
117#[cfg(not(Py_LIMITED_API))]
118fn pytime_to_time(time: &impl PyTimeAccess) -> PyResult<Time> {
119    Ok(Time::new(
120        time.get_hour().try_into()?,
121        time.get_minute().try_into()?,
122        time.get_second().try_into()?,
123        (time.get_microsecond() * 1000).try_into()?,
124    )?)
125}
126
127#[cfg(Py_LIMITED_API)]
128fn pytime_to_time(time: &Bound<'_, PyAny>) -> PyResult<Time> {
129    let py = time.py();
130    Ok(Time::new(
131        time.getattr(intern!(py, "hour"))?.extract()?,
132        time.getattr(intern!(py, "minute"))?.extract()?,
133        time.getattr(intern!(py, "second"))?.extract()?,
134        time.getattr(intern!(py, "microsecond"))?.extract::<i32>()? * 1000,
135    )?)
136}
137
138impl<'py> IntoPyObject<'py> for Timestamp {
139    #[cfg(not(Py_LIMITED_API))]
140    type Target = PyDateTime;
141    #[cfg(Py_LIMITED_API)]
142    type Target = PyAny;
143    type Output = Bound<'py, Self::Target>;
144    type Error = PyErr;
145
146    fn into_pyobject(self, py: Python<'py>) -> Result<Self::Output, Self::Error> {
147        (&self).into_pyobject(py)
148    }
149}
150
151impl<'py> IntoPyObject<'py> for &Timestamp {
152    #[cfg(not(Py_LIMITED_API))]
153    type Target = PyDateTime;
154    #[cfg(Py_LIMITED_API)]
155    type Target = PyAny;
156    type Output = Bound<'py, Self::Target>;
157    type Error = PyErr;
158
159    fn into_pyobject(self, py: Python<'py>) -> Result<Self::Output, Self::Error> {
160        self.to_zoned(TimeZone::UTC).into_pyobject(py)
161    }
162}
163
164impl<'py> FromPyObject<'py> for Timestamp {
165    fn extract_bound(ob: &Bound<'py, PyAny>) -> PyResult<Self> {
166        let zoned = ob.extract::<Zoned>()?;
167        Ok(zoned.timestamp())
168    }
169}
170
171impl<'py> IntoPyObject<'py> for Date {
172    #[cfg(not(Py_LIMITED_API))]
173    type Target = PyDate;
174    #[cfg(Py_LIMITED_API)]
175    type Target = PyAny;
176    type Output = Bound<'py, Self::Target>;
177    type Error = PyErr;
178
179    fn into_pyobject(self, py: Python<'py>) -> Result<Self::Output, Self::Error> {
180        (&self).into_pyobject(py)
181    }
182}
183
184impl<'py> IntoPyObject<'py> for &Date {
185    #[cfg(not(Py_LIMITED_API))]
186    type Target = PyDate;
187    #[cfg(Py_LIMITED_API)]
188    type Target = PyAny;
189    type Output = Bound<'py, Self::Target>;
190    type Error = PyErr;
191
192    fn into_pyobject(self, py: Python<'py>) -> Result<Self::Output, Self::Error> {
193        #[cfg(not(Py_LIMITED_API))]
194        {
195            PyDate::new(
196                py,
197                self.year().into(),
198                self.month().try_into()?,
199                self.day().try_into()?,
200            )
201        }
202
203        #[cfg(Py_LIMITED_API)]
204        {
205            DatetimeTypes::try_get(py)?
206                .date
207                .bind(py)
208                .call1((self.year(), self.month(), self.day()))
209        }
210    }
211}
212
213impl<'py> FromPyObject<'py> for Date {
214    fn extract_bound(ob: &Bound<'py, PyAny>) -> PyResult<Self> {
215        #[cfg(not(Py_LIMITED_API))]
216        {
217            let date = ob.downcast::<PyDate>()?;
218            Ok(Date::new(
219                date.get_year().try_into()?,
220                date.get_month().try_into()?,
221                date.get_day().try_into()?,
222            )?)
223        }
224
225        #[cfg(Py_LIMITED_API)]
226        {
227            check_type(ob, &DatetimeTypes::get(ob.py()).date, "PyDate")?;
228            Ok(Date::new(
229                ob.getattr(intern!(ob.py(), "year"))?.extract()?,
230                ob.getattr(intern!(ob.py(), "month"))?.extract()?,
231                ob.getattr(intern!(ob.py(), "day"))?.extract()?,
232            )?)
233        }
234    }
235}
236
237impl<'py> IntoPyObject<'py> for Time {
238    #[cfg(not(Py_LIMITED_API))]
239    type Target = PyTime;
240    #[cfg(Py_LIMITED_API)]
241    type Target = PyAny;
242    type Output = Bound<'py, Self::Target>;
243    type Error = PyErr;
244
245    fn into_pyobject(self, py: Python<'py>) -> Result<Self::Output, Self::Error> {
246        (&self).into_pyobject(py)
247    }
248}
249
250impl<'py> IntoPyObject<'py> for &Time {
251    #[cfg(not(Py_LIMITED_API))]
252    type Target = PyTime;
253    #[cfg(Py_LIMITED_API)]
254    type Target = PyAny;
255    type Output = Bound<'py, Self::Target>;
256    type Error = PyErr;
257
258    fn into_pyobject(self, py: Python<'py>) -> Result<Self::Output, Self::Error> {
259        #[cfg(not(Py_LIMITED_API))]
260        {
261            PyTime::new(
262                py,
263                self.hour().try_into()?,
264                self.minute().try_into()?,
265                self.second().try_into()?,
266                (self.subsec_nanosecond() / 1000).try_into()?,
267                None,
268            )
269        }
270
271        #[cfg(Py_LIMITED_API)]
272        {
273            DatetimeTypes::try_get(py)?.time.bind(py).call1((
274                self.hour(),
275                self.minute(),
276                self.second(),
277                self.subsec_nanosecond() / 1000,
278            ))
279        }
280    }
281}
282
283impl<'py> FromPyObject<'py> for Time {
284    fn extract_bound(ob: &Bound<'py, PyAny>) -> PyResult<Self> {
285        #[cfg(not(Py_LIMITED_API))]
286        let ob = ob.downcast::<PyTime>()?;
287
288        #[cfg(Py_LIMITED_API)]
289        check_type(ob, &DatetimeTypes::get(ob.py()).time, "PyTime")?;
290
291        pytime_to_time(ob)
292    }
293}
294
295impl<'py> IntoPyObject<'py> for DateTime {
296    #[cfg(not(Py_LIMITED_API))]
297    type Target = PyDateTime;
298    #[cfg(Py_LIMITED_API)]
299    type Target = PyAny;
300    type Output = Bound<'py, Self::Target>;
301    type Error = PyErr;
302
303    fn into_pyobject(self, py: Python<'py>) -> Result<Self::Output, Self::Error> {
304        (&self).into_pyobject(py)
305    }
306}
307
308impl<'py> IntoPyObject<'py> for &DateTime {
309    #[cfg(not(Py_LIMITED_API))]
310    type Target = PyDateTime;
311    #[cfg(Py_LIMITED_API)]
312    type Target = PyAny;
313    type Output = Bound<'py, Self::Target>;
314    type Error = PyErr;
315
316    fn into_pyobject(self, py: Python<'py>) -> Result<Self::Output, Self::Error> {
317        datetime_to_pydatetime(py, self, false, None)
318    }
319}
320
321impl<'py> FromPyObject<'py> for DateTime {
322    fn extract_bound(dt: &Bound<'py, PyAny>) -> PyResult<Self> {
323        #[cfg(not(Py_LIMITED_API))]
324        let dt = dt.downcast::<PyDateTime>()?;
325
326        #[cfg(Py_LIMITED_API)]
327        check_type(dt, &DatetimeTypes::get(dt.py()).datetime, "PyDateTime")?;
328
329        #[cfg(not(Py_LIMITED_API))]
330        let has_tzinfo = dt.get_tzinfo().is_some();
331
332        #[cfg(Py_LIMITED_API)]
333        let has_tzinfo = !dt.getattr(intern!(dt.py(), "tzinfo"))?.is_none();
334
335        if has_tzinfo {
336            return Err(PyTypeError::new_err("expected a datetime without tzinfo"));
337        }
338
339        Ok(DateTime::from_parts(dt.extract()?, pytime_to_time(dt)?))
340    }
341}
342
343impl<'py> IntoPyObject<'py> for Zoned {
344    #[cfg(not(Py_LIMITED_API))]
345    type Target = PyDateTime;
346    #[cfg(Py_LIMITED_API)]
347    type Target = PyAny;
348    type Output = Bound<'py, Self::Target>;
349    type Error = PyErr;
350
351    fn into_pyobject(self, py: Python<'py>) -> Result<Self::Output, Self::Error> {
352        (&self).into_pyobject(py)
353    }
354}
355
356impl<'py> IntoPyObject<'py> for &Zoned {
357    #[cfg(not(Py_LIMITED_API))]
358    type Target = PyDateTime;
359    #[cfg(Py_LIMITED_API)]
360    type Target = PyAny;
361    type Output = Bound<'py, Self::Target>;
362    type Error = PyErr;
363
364    fn into_pyobject(self, py: Python<'py>) -> Result<Self::Output, Self::Error> {
365        fn fold(zoned: &Zoned) -> Option<bool> {
366            let prev = zoned.time_zone().preceding(zoned.timestamp()).next()?;
367            let next = zoned.time_zone().following(prev.timestamp()).next()?;
368            let start_of_current_offset = if next.timestamp() == zoned.timestamp() {
369                next.timestamp()
370            } else {
371                prev.timestamp()
372            };
373            Some(zoned.timestamp() + (zoned.offset() - prev.offset()) <= start_of_current_offset)
374        }
375        datetime_to_pydatetime(
376            py,
377            &self.datetime(),
378            fold(self).unwrap_or(false),
379            Some(self.time_zone()),
380        )
381    }
382}
383
384impl<'py> FromPyObject<'py> for Zoned {
385    fn extract_bound(dt: &Bound<'py, PyAny>) -> PyResult<Self> {
386        #[cfg(not(Py_LIMITED_API))]
387        let dt = dt.downcast::<PyDateTime>()?;
388
389        #[cfg(Py_LIMITED_API)]
390        check_type(dt, &DatetimeTypes::get(dt.py()).datetime, "PyDateTime")?;
391
392        let tz = {
393            #[cfg(not(Py_LIMITED_API))]
394            let tzinfo: Option<_> = dt.get_tzinfo();
395
396            #[cfg(Py_LIMITED_API)]
397            let tzinfo: Option<Bound<'_, PyAny>> =
398                dt.getattr(intern!(dt.py(), "tzinfo"))?.extract()?;
399
400            tzinfo
401                .map(|tz| tz.extract::<TimeZone>())
402                .unwrap_or_else(|| {
403                    Err(PyTypeError::new_err(
404                        "expected a datetime with non-None tzinfo",
405                    ))
406                })?
407        };
408        let datetime = DateTime::from_parts(dt.extract()?, pytime_to_time(dt)?);
409        let zoned = tz.into_ambiguous_zoned(datetime);
410
411        #[cfg(not(Py_LIMITED_API))]
412        let fold = dt.get_fold();
413
414        #[cfg(Py_LIMITED_API)]
415        let fold = dt.getattr(intern!(dt.py(), "fold"))?.extract::<usize>()? > 0;
416
417        if fold {
418            Ok(zoned.later()?)
419        } else {
420            Ok(zoned.earlier()?)
421        }
422    }
423}
424
425impl<'py> IntoPyObject<'py> for TimeZone {
426    #[cfg(not(Py_LIMITED_API))]
427    type Target = PyTzInfo;
428    #[cfg(Py_LIMITED_API)]
429    type Target = PyAny;
430    type Output = Bound<'py, Self::Target>;
431    type Error = PyErr;
432
433    fn into_pyobject(self, py: Python<'py>) -> Result<Self::Output, Self::Error> {
434        (&self).into_pyobject(py)
435    }
436}
437
438impl<'py> IntoPyObject<'py> for &TimeZone {
439    #[cfg(not(Py_LIMITED_API))]
440    type Target = PyTzInfo;
441    #[cfg(Py_LIMITED_API)]
442    type Target = PyAny;
443    type Output = Bound<'py, Self::Target>;
444    type Error = PyErr;
445
446    fn into_pyobject(self, py: Python<'py>) -> Result<Self::Output, Self::Error> {
447        if self == &TimeZone::UTC {
448            Ok(timezone_utc(py))
449        } else if let Some(iana_name) = self.iana_name() {
450            static ZONE_INFO: GILOnceCell<Py<PyType>> = GILOnceCell::new();
451            let tz = ZONE_INFO
452                .import(py, "zoneinfo", "ZoneInfo")
453                .and_then(|obj| obj.call1((iana_name,)))?;
454
455            #[cfg(not(Py_LIMITED_API))]
456            let tz = tz.downcast_into()?;
457
458            Ok(tz)
459        } else {
460            self.to_fixed_offset()?.into_pyobject(py)
461        }
462    }
463}
464
465impl<'py> FromPyObject<'py> for TimeZone {
466    fn extract_bound(ob: &Bound<'py, PyAny>) -> PyResult<Self> {
467        #[cfg(not(Py_LIMITED_API))]
468        let ob = ob.downcast::<PyTzInfo>()?;
469
470        #[cfg(Py_LIMITED_API)]
471        check_type(ob, &DatetimeTypes::get(ob.py()).tzinfo, "PyTzInfo")?;
472
473        let attr = intern!(ob.py(), "key");
474        if ob.hasattr(attr)? {
475            Ok(TimeZone::get(&ob.getattr(attr)?.extract::<PyBackedStr>()?)?)
476        } else {
477            Ok(ob.extract::<Offset>()?.to_time_zone())
478        }
479    }
480}
481
482impl<'py> IntoPyObject<'py> for &Offset {
483    #[cfg(not(Py_LIMITED_API))]
484    type Target = PyTzInfo;
485    #[cfg(Py_LIMITED_API)]
486    type Target = PyAny;
487    type Output = Bound<'py, Self::Target>;
488    type Error = PyErr;
489
490    fn into_pyobject(self, py: Python<'py>) -> Result<Self::Output, Self::Error> {
491        if self == &Offset::UTC {
492            return Ok(timezone_utc(py));
493        }
494
495        let delta = self.duration_since(Offset::UTC).into_pyobject(py)?;
496
497        #[cfg(not(Py_LIMITED_API))]
498        {
499            timezone_from_offset(&delta)
500        }
501
502        #[cfg(Py_LIMITED_API)]
503        {
504            DatetimeTypes::try_get(py)?
505                .timezone
506                .bind(py)
507                .call1((delta,))
508        }
509    }
510}
511
512impl<'py> IntoPyObject<'py> for Offset {
513    #[cfg(not(Py_LIMITED_API))]
514    type Target = PyTzInfo;
515    #[cfg(Py_LIMITED_API)]
516    type Target = PyAny;
517    type Output = Bound<'py, Self::Target>;
518    type Error = PyErr;
519
520    fn into_pyobject(self, py: Python<'py>) -> Result<Self::Output, Self::Error> {
521        (&self).into_pyobject(py)
522    }
523}
524
525impl<'py> FromPyObject<'py> for Offset {
526    fn extract_bound(ob: &Bound<'py, PyAny>) -> PyResult<Self> {
527        let py = ob.py();
528
529        #[cfg(not(Py_LIMITED_API))]
530        let ob = ob.downcast::<PyTzInfo>()?;
531
532        #[cfg(Py_LIMITED_API)]
533        check_type(ob, &DatetimeTypes::get(ob.py()).tzinfo, "PyTzInfo")?;
534
535        let py_timedelta = ob.call_method1(intern!(py, "utcoffset"), (PyNone::get(py),))?;
536        if py_timedelta.is_none() {
537            return Err(PyTypeError::new_err(format!(
538                "{:?} is not a fixed offset timezone",
539                ob
540            )));
541        }
542
543        let total_seconds = py_timedelta.extract::<SignedDuration>()?.as_secs();
544        debug_assert!(
545            (total_seconds / 3600).abs() <= 24,
546            "Offset must be between -24 hours and 24 hours but was {}h",
547            total_seconds / 3600
548        );
549        // This cast is safe since the timedelta is limited to -24 hours and 24 hours.
550        Ok(Offset::from_seconds(total_seconds as i32)?)
551    }
552}
553
554impl<'py> IntoPyObject<'py> for &SignedDuration {
555    #[cfg(not(Py_LIMITED_API))]
556    type Target = PyDelta;
557    #[cfg(Py_LIMITED_API)]
558    type Target = PyAny;
559    type Output = Bound<'py, Self::Target>;
560    type Error = PyErr;
561
562    fn into_pyobject(self, py: Python<'py>) -> Result<Self::Output, Self::Error> {
563        let total_seconds = self.as_secs();
564        let days: i32 = (total_seconds / (24 * 60 * 60)).try_into()?;
565        let seconds: i32 = (total_seconds % (24 * 60 * 60)).try_into()?;
566        let microseconds = self.subsec_micros();
567
568        #[cfg(not(Py_LIMITED_API))]
569        {
570            PyDelta::new(py, days, seconds, microseconds, true)
571        }
572
573        #[cfg(Py_LIMITED_API)]
574        {
575            DatetimeTypes::try_get(py)?
576                .timedelta
577                .bind(py)
578                .call1((days, seconds, microseconds))
579        }
580    }
581}
582
583impl<'py> IntoPyObject<'py> for SignedDuration {
584    #[cfg(not(Py_LIMITED_API))]
585    type Target = PyDelta;
586    #[cfg(Py_LIMITED_API)]
587    type Target = PyAny;
588    type Output = Bound<'py, Self::Target>;
589    type Error = PyErr;
590
591    fn into_pyobject(self, py: Python<'py>) -> Result<Self::Output, Self::Error> {
592        (&self).into_pyobject(py)
593    }
594}
595
596impl<'py> FromPyObject<'py> for SignedDuration {
597    fn extract_bound(ob: &Bound<'py, PyAny>) -> PyResult<Self> {
598        #[cfg(not(Py_LIMITED_API))]
599        let (seconds, microseconds) = {
600            let delta = ob.downcast::<PyDelta>()?;
601            let days = delta.get_days() as i64;
602            let seconds = delta.get_seconds() as i64;
603            let microseconds = delta.get_microseconds();
604            (days * 24 * 60 * 60 + seconds, microseconds)
605        };
606
607        #[cfg(Py_LIMITED_API)]
608        let (seconds, microseconds) = {
609            check_type(ob, &DatetimeTypes::get(ob.py()).timedelta, "PyDelta")?;
610            let days = ob.getattr(intern!(ob.py(), "days"))?.extract::<i64>()?;
611            let seconds = ob.getattr(intern!(ob.py(), "seconds"))?.extract::<i64>()?;
612            let microseconds = ob
613                .getattr(intern!(ob.py(), "microseconds"))?
614                .extract::<i32>()?;
615            (days * 24 * 60 * 60 + seconds, microseconds)
616        };
617
618        Ok(SignedDuration::new(seconds, microseconds * 1000))
619    }
620}
621
622impl<'py> FromPyObject<'py> for Span {
623    fn extract_bound(ob: &Bound<'py, PyAny>) -> PyResult<Self> {
624        let duration = ob.extract::<SignedDuration>()?;
625        Ok(duration.try_into()?)
626    }
627}
628
629impl From<jiff::Error> for PyErr {
630    fn from(e: jiff::Error) -> Self {
631        PyValueError::new_err(e.to_string())
632    }
633}
634
635#[cfg(test)]
636mod tests {
637    use super::*;
638    #[cfg(not(Py_LIMITED_API))]
639    use crate::types::timezone_utc;
640    use crate::{types::PyTuple, BoundObject};
641    use jiff::tz::Offset;
642    use std::cmp::Ordering;
643
644    #[test]
645    // Only Python>=3.9 has the zoneinfo package
646    // We skip the test on windows too since we'd need to install
647    // tzdata there to make this work.
648    #[cfg(all(Py_3_9, not(target_os = "windows")))]
649    fn test_zoneinfo_is_not_fixed_offset() {
650        use crate::ffi;
651        use crate::types::any::PyAnyMethods;
652        use crate::types::dict::PyDictMethods;
653
654        Python::with_gil(|py| {
655            let locals = crate::types::PyDict::new(py);
656            py.run(
657                ffi::c_str!("import zoneinfo; zi = zoneinfo.ZoneInfo('Europe/London')"),
658                None,
659                Some(&locals),
660            )
661            .unwrap();
662            let result: PyResult<Offset> = locals.get_item("zi").unwrap().unwrap().extract();
663            assert!(result.is_err());
664            let res = result.err().unwrap();
665            // Also check the error message is what we expect
666            let msg = res.value(py).repr().unwrap().to_string();
667            assert_eq!(msg, "TypeError(\"zoneinfo.ZoneInfo(key='Europe/London') is not a fixed offset timezone\")");
668        });
669    }
670
671    #[test]
672    fn test_timezone_aware_to_naive_fails() {
673        // Test that if a user tries to convert a python's timezone aware datetime into a naive
674        // one, the conversion fails.
675        Python::with_gil(|py| {
676            let py_datetime =
677                new_py_datetime_ob(py, "datetime", (2022, 1, 1, 1, 0, 0, 0, python_utc(py)));
678            // Now test that converting a PyDateTime with tzinfo to a NaiveDateTime fails
679            let res: PyResult<DateTime> = py_datetime.extract();
680            assert_eq!(
681                res.unwrap_err().value(py).repr().unwrap().to_string(),
682                "TypeError('expected a datetime without tzinfo')"
683            );
684        });
685    }
686
687    #[test]
688    fn test_naive_to_timezone_aware_fails() {
689        // Test that if a user tries to convert a python's naive datetime into a timezone aware
690        // one, the conversion fails.
691        Python::with_gil(|py| {
692            let py_datetime = new_py_datetime_ob(py, "datetime", (2022, 1, 1, 1, 0, 0, 0));
693            let res: PyResult<Zoned> = py_datetime.extract();
694            assert_eq!(
695                res.unwrap_err().value(py).repr().unwrap().to_string(),
696                "TypeError('expected a datetime with non-None tzinfo')"
697            );
698        });
699    }
700
701    #[test]
702    fn test_invalid_types_fail() {
703        Python::with_gil(|py| {
704            let none = py.None().into_bound(py);
705            assert_eq!(
706                none.extract::<Span>().unwrap_err().to_string(),
707                "TypeError: 'NoneType' object cannot be converted to 'PyDelta'"
708            );
709            assert_eq!(
710                none.extract::<Offset>().unwrap_err().to_string(),
711                "TypeError: 'NoneType' object cannot be converted to 'PyTzInfo'"
712            );
713            assert_eq!(
714                none.extract::<TimeZone>().unwrap_err().to_string(),
715                "TypeError: 'NoneType' object cannot be converted to 'PyTzInfo'"
716            );
717            assert_eq!(
718                none.extract::<Time>().unwrap_err().to_string(),
719                "TypeError: 'NoneType' object cannot be converted to 'PyTime'"
720            );
721            assert_eq!(
722                none.extract::<Date>().unwrap_err().to_string(),
723                "TypeError: 'NoneType' object cannot be converted to 'PyDate'"
724            );
725            assert_eq!(
726                none.extract::<DateTime>().unwrap_err().to_string(),
727                "TypeError: 'NoneType' object cannot be converted to 'PyDateTime'"
728            );
729            assert_eq!(
730                none.extract::<Zoned>().unwrap_err().to_string(),
731                "TypeError: 'NoneType' object cannot be converted to 'PyDateTime'"
732            );
733        });
734    }
735
736    #[test]
737    fn test_pyo3_date_into_pyobject() {
738        let eq_ymd = |name: &'static str, year, month, day| {
739            Python::with_gil(|py| {
740                let date = Date::new(year, month, day)
741                    .unwrap()
742                    .into_pyobject(py)
743                    .unwrap();
744                let py_date = new_py_datetime_ob(py, "date", (year, month, day));
745                assert_eq!(
746                    date.compare(&py_date).unwrap(),
747                    Ordering::Equal,
748                    "{}: {} != {}",
749                    name,
750                    date,
751                    py_date
752                );
753            })
754        };
755
756        eq_ymd("past date", 2012, 2, 29);
757        eq_ymd("min date", 1, 1, 1);
758        eq_ymd("future date", 3000, 6, 5);
759        eq_ymd("max date", 9999, 12, 31);
760    }
761
762    #[test]
763    fn test_pyo3_date_frompyobject() {
764        let eq_ymd = |name: &'static str, year, month, day| {
765            Python::with_gil(|py| {
766                let py_date = new_py_datetime_ob(py, "date", (year, month, day));
767                let py_date: Date = py_date.extract().unwrap();
768                let date = Date::new(year, month, day).unwrap();
769                assert_eq!(py_date, date, "{}: {} != {}", name, date, py_date);
770            })
771        };
772
773        eq_ymd("past date", 2012, 2, 29);
774        eq_ymd("min date", 1, 1, 1);
775        eq_ymd("future date", 3000, 6, 5);
776        eq_ymd("max date", 9999, 12, 31);
777    }
778
779    #[test]
780    fn test_pyo3_datetime_into_pyobject_utc() {
781        Python::with_gil(|py| {
782            let check_utc =
783                |name: &'static str, year, month, day, hour, minute, second, ms, py_ms| {
784                    let datetime = DateTime::new(year, month, day, hour, minute, second, ms * 1000)
785                        .unwrap()
786                        .to_zoned(TimeZone::UTC)
787                        .unwrap();
788                    let datetime = datetime.into_pyobject(py).unwrap();
789                    let py_datetime = new_py_datetime_ob(
790                        py,
791                        "datetime",
792                        (
793                            year,
794                            month,
795                            day,
796                            hour,
797                            minute,
798                            second,
799                            py_ms,
800                            python_utc(py),
801                        ),
802                    );
803                    assert_eq!(
804                        datetime.compare(&py_datetime).unwrap(),
805                        Ordering::Equal,
806                        "{}: {} != {}",
807                        name,
808                        datetime,
809                        py_datetime
810                    );
811                };
812
813            check_utc("regular", 2014, 5, 6, 7, 8, 9, 999_999, 999_999);
814        })
815    }
816
817    #[test]
818    fn test_pyo3_datetime_into_pyobject_fixed_offset() {
819        Python::with_gil(|py| {
820            let check_fixed_offset =
821                |name: &'static str, year, month, day, hour, minute, second, ms, py_ms| {
822                    let offset = Offset::from_seconds(3600).unwrap();
823                    let datetime = DateTime::new(year, month, day, hour, minute, second, ms * 1000)
824                        .map_err(|e| {
825                            eprintln!("{}: {}", name, e);
826                            e
827                        })
828                        .unwrap()
829                        .to_zoned(offset.to_time_zone())
830                        .unwrap();
831                    let datetime = datetime.into_pyobject(py).unwrap();
832                    let py_tz = offset.into_pyobject(py).unwrap();
833                    let py_datetime = new_py_datetime_ob(
834                        py,
835                        "datetime",
836                        (year, month, day, hour, minute, second, py_ms, py_tz),
837                    );
838                    assert_eq!(
839                        datetime.compare(&py_datetime).unwrap(),
840                        Ordering::Equal,
841                        "{}: {} != {}",
842                        name,
843                        datetime,
844                        py_datetime
845                    );
846                };
847
848            check_fixed_offset("regular", 2014, 5, 6, 7, 8, 9, 999_999, 999_999);
849        })
850    }
851
852    #[test]
853    #[cfg(all(Py_3_9, not(windows)))]
854    fn test_pyo3_datetime_into_pyobject_tz() {
855        Python::with_gil(|py| {
856            let datetime = DateTime::new(2024, 12, 11, 23, 3, 13, 0)
857                .unwrap()
858                .to_zoned(TimeZone::get("Europe/London").unwrap())
859                .unwrap();
860            let datetime = datetime.into_pyobject(py).unwrap();
861            let py_datetime = new_py_datetime_ob(
862                py,
863                "datetime",
864                (
865                    2024,
866                    12,
867                    11,
868                    23,
869                    3,
870                    13,
871                    0,
872                    python_zoneinfo(py, "Europe/London"),
873                ),
874            );
875            assert_eq!(datetime.compare(&py_datetime).unwrap(), Ordering::Equal);
876        })
877    }
878
879    #[test]
880    fn test_pyo3_datetime_frompyobject_utc() {
881        Python::with_gil(|py| {
882            let year = 2014;
883            let month = 5;
884            let day = 6;
885            let hour = 7;
886            let minute = 8;
887            let second = 9;
888            let micro = 999_999;
889            let tz_utc = timezone_utc(py);
890            let py_datetime = new_py_datetime_ob(
891                py,
892                "datetime",
893                (year, month, day, hour, minute, second, micro, tz_utc),
894            );
895            let py_datetime: Zoned = py_datetime.extract().unwrap();
896            let datetime = DateTime::new(year, month, day, hour, minute, second, micro * 1000)
897                .unwrap()
898                .to_zoned(TimeZone::UTC)
899                .unwrap();
900            assert_eq!(py_datetime, datetime,);
901        })
902    }
903
904    #[test]
905    #[cfg(all(Py_3_9, not(windows)))]
906    fn test_ambiguous_datetime_to_pyobject() {
907        use std::str::FromStr;
908        let dates = [
909            Zoned::from_str("2020-10-24 23:00:00[UTC]").unwrap(),
910            Zoned::from_str("2020-10-25 00:00:00[UTC]").unwrap(),
911            Zoned::from_str("2020-10-25 01:00:00[UTC]").unwrap(),
912            Zoned::from_str("2020-10-25 02:00:00[UTC]").unwrap(),
913        ];
914
915        let tz = TimeZone::get("Europe/London").unwrap();
916        let dates = dates.map(|dt| dt.with_time_zone(tz.clone()));
917
918        assert_eq!(
919            dates.clone().map(|ref dt| dt.to_string()),
920            [
921                "2020-10-25T00:00:00+01:00[Europe/London]",
922                "2020-10-25T01:00:00+01:00[Europe/London]",
923                "2020-10-25T01:00:00+00:00[Europe/London]",
924                "2020-10-25T02:00:00+00:00[Europe/London]",
925            ]
926        );
927
928        let dates = Python::with_gil(|py| {
929            let pydates = dates.map(|dt| dt.into_pyobject(py).unwrap());
930            assert_eq!(
931                pydates
932                    .clone()
933                    .map(|dt| dt.getattr("hour").unwrap().extract::<usize>().unwrap()),
934                [0, 1, 1, 2]
935            );
936
937            assert_eq!(
938                pydates
939                    .clone()
940                    .map(|dt| dt.getattr("fold").unwrap().extract::<usize>().unwrap() > 0),
941                [false, false, true, false]
942            );
943
944            pydates.map(|dt| dt.extract::<Zoned>().unwrap())
945        });
946
947        assert_eq!(
948            dates.map(|dt| dt.to_string()),
949            [
950                "2020-10-25T00:00:00+01:00[Europe/London]",
951                "2020-10-25T01:00:00+01:00[Europe/London]",
952                "2020-10-25T01:00:00+00:00[Europe/London]",
953                "2020-10-25T02:00:00+00:00[Europe/London]",
954            ]
955        );
956    }
957
958    #[test]
959    fn test_pyo3_datetime_frompyobject_fixed_offset() {
960        Python::with_gil(|py| {
961            let year = 2014;
962            let month = 5;
963            let day = 6;
964            let hour = 7;
965            let minute = 8;
966            let second = 9;
967            let micro = 999_999;
968            let offset = Offset::from_seconds(3600).unwrap();
969            let py_tz = offset.into_pyobject(py).unwrap();
970            let py_datetime = new_py_datetime_ob(
971                py,
972                "datetime",
973                (year, month, day, hour, minute, second, micro, py_tz),
974            );
975            let datetime_from_py: Zoned = py_datetime.extract().unwrap();
976            let datetime =
977                DateTime::new(year, month, day, hour, minute, second, micro * 1000).unwrap();
978            let datetime = datetime.to_zoned(offset.to_time_zone()).unwrap();
979
980            assert_eq!(datetime_from_py, datetime);
981        })
982    }
983
984    #[test]
985    fn test_pyo3_offset_fixed_into_pyobject() {
986        Python::with_gil(|py| {
987            // jiff offset
988            let offset = Offset::from_seconds(3600)
989                .unwrap()
990                .into_pyobject(py)
991                .unwrap();
992            // Python timezone from timedelta
993            let td = new_py_datetime_ob(py, "timedelta", (0, 3600, 0));
994            let py_timedelta = new_py_datetime_ob(py, "timezone", (td,));
995            // Should be equal
996            assert!(offset.eq(py_timedelta).unwrap());
997
998            // Same but with negative values
999            let offset = Offset::from_seconds(-3600)
1000                .unwrap()
1001                .into_pyobject(py)
1002                .unwrap();
1003            let td = new_py_datetime_ob(py, "timedelta", (0, -3600, 0));
1004            let py_timedelta = new_py_datetime_ob(py, "timezone", (td,));
1005            assert!(offset.eq(py_timedelta).unwrap());
1006        })
1007    }
1008
1009    #[test]
1010    fn test_pyo3_offset_fixed_frompyobject() {
1011        Python::with_gil(|py| {
1012            let py_timedelta = new_py_datetime_ob(py, "timedelta", (0, 3600, 0));
1013            let py_tzinfo = new_py_datetime_ob(py, "timezone", (py_timedelta,));
1014            let offset: Offset = py_tzinfo.extract().unwrap();
1015            assert_eq!(Offset::from_seconds(3600).unwrap(), offset);
1016        })
1017    }
1018
1019    #[test]
1020    fn test_pyo3_offset_utc_into_pyobject() {
1021        Python::with_gil(|py| {
1022            let utc = Offset::UTC.into_pyobject(py).unwrap();
1023            let py_utc = python_utc(py);
1024            assert!(utc.is(&py_utc));
1025        })
1026    }
1027
1028    #[test]
1029    fn test_pyo3_offset_utc_frompyobject() {
1030        Python::with_gil(|py| {
1031            let py_utc = python_utc(py);
1032            let py_utc: Offset = py_utc.extract().unwrap();
1033            assert_eq!(Offset::UTC, py_utc);
1034
1035            let py_timedelta = new_py_datetime_ob(py, "timedelta", (0, 0, 0));
1036            let py_timezone_utc = new_py_datetime_ob(py, "timezone", (py_timedelta,));
1037            let py_timezone_utc: Offset = py_timezone_utc.extract().unwrap();
1038            assert_eq!(Offset::UTC, py_timezone_utc);
1039
1040            let py_timedelta = new_py_datetime_ob(py, "timedelta", (0, 3600, 0));
1041            let py_timezone = new_py_datetime_ob(py, "timezone", (py_timedelta,));
1042            assert_ne!(Offset::UTC, py_timezone.extract::<Offset>().unwrap());
1043        })
1044    }
1045
1046    #[test]
1047    fn test_pyo3_time_into_pyobject() {
1048        Python::with_gil(|py| {
1049            let check_time = |name: &'static str, hour, minute, second, ms, py_ms| {
1050                let time = Time::new(hour, minute, second, ms * 1000)
1051                    .unwrap()
1052                    .into_pyobject(py)
1053                    .unwrap();
1054                let py_time = new_py_datetime_ob(py, "time", (hour, minute, second, py_ms));
1055                assert!(
1056                    time.eq(&py_time).unwrap(),
1057                    "{}: {} != {}",
1058                    name,
1059                    time,
1060                    py_time
1061                );
1062            };
1063
1064            check_time("regular", 3, 5, 7, 999_999, 999_999);
1065        })
1066    }
1067
1068    #[test]
1069    fn test_pyo3_time_frompyobject() {
1070        let hour = 3;
1071        let minute = 5;
1072        let second = 7;
1073        let micro = 999_999;
1074        Python::with_gil(|py| {
1075            let py_time = new_py_datetime_ob(py, "time", (hour, minute, second, micro));
1076            let py_time: Time = py_time.extract().unwrap();
1077            let time = Time::new(hour, minute, second, micro * 1000).unwrap();
1078            assert_eq!(py_time, time);
1079        })
1080    }
1081
1082    fn new_py_datetime_ob<'py, A>(py: Python<'py>, name: &str, args: A) -> Bound<'py, PyAny>
1083    where
1084        A: IntoPyObject<'py, Target = PyTuple>,
1085    {
1086        py.import("datetime")
1087            .unwrap()
1088            .getattr(name)
1089            .unwrap()
1090            .call1(
1091                args.into_pyobject(py)
1092                    .map_err(Into::into)
1093                    .unwrap()
1094                    .into_bound(),
1095            )
1096            .unwrap()
1097    }
1098
1099    fn python_utc(py: Python<'_>) -> Bound<'_, PyAny> {
1100        py.import("datetime")
1101            .unwrap()
1102            .getattr("timezone")
1103            .unwrap()
1104            .getattr("utc")
1105            .unwrap()
1106    }
1107
1108    #[cfg(all(Py_3_9, not(windows)))]
1109    fn python_zoneinfo<'py>(py: Python<'py>, timezone: &str) -> Bound<'py, PyAny> {
1110        py.import("zoneinfo")
1111            .unwrap()
1112            .getattr("ZoneInfo")
1113            .unwrap()
1114            .call1((timezone,))
1115            .unwrap()
1116    }
1117
1118    #[cfg(not(any(target_arch = "wasm32", Py_GIL_DISABLED)))]
1119    mod proptests {
1120        use super::*;
1121        use crate::types::IntoPyDict;
1122        use jiff::tz::TimeZoneTransition;
1123        use jiff::SpanRelativeTo;
1124        use proptest::prelude::*;
1125        use std::ffi::CString;
1126
1127        // This is to skip the test if we are creating an invalid date, like February 31.
1128        fn try_date(year: i32, month: u32, day: u32) -> PyResult<Date> {
1129            Ok(Date::new(
1130                year.try_into()?,
1131                month.try_into()?,
1132                day.try_into()?,
1133            )?)
1134        }
1135
1136        fn try_time(hour: u32, min: u32, sec: u32, micro: u32) -> PyResult<Time> {
1137            Ok(Time::new(
1138                hour.try_into()?,
1139                min.try_into()?,
1140                sec.try_into()?,
1141                (micro * 1000).try_into()?,
1142            )?)
1143        }
1144
1145        prop_compose! {
1146            fn timezone_transitions(timezone: &TimeZone)
1147                            (year in 1900i16..=2100i16, month in 1i8..=12i8)
1148                            -> TimeZoneTransition<'_> {
1149                let datetime = DateTime::new(year, month, 1, 0, 0, 0, 0).unwrap();
1150                let timestamp= timezone.to_zoned(datetime).unwrap().timestamp();
1151                timezone.following(timestamp).next().unwrap()
1152            }
1153        }
1154
1155        proptest! {
1156
1157            // Range is limited to 1970 to 2038 due to windows limitations
1158            #[test]
1159            fn test_pyo3_offset_fixed_frompyobject_created_in_python(timestamp in 0..(i32::MAX as i64), timedelta in -86399i32..=86399i32) {
1160                Python::with_gil(|py| {
1161
1162                    let globals = [("datetime", py.import("datetime").unwrap())].into_py_dict(py).unwrap();
1163                    let code = format!("datetime.datetime.fromtimestamp({}).replace(tzinfo=datetime.timezone(datetime.timedelta(seconds={})))", timestamp, timedelta);
1164                    let t = py.eval(&CString::new(code).unwrap(), Some(&globals), None).unwrap();
1165
1166                    // Get ISO 8601 string from python
1167                    let py_iso_str = t.call_method0("isoformat").unwrap();
1168
1169                    // Get ISO 8601 string from rust
1170                    let rust_iso_str = t.extract::<Zoned>().unwrap().strftime("%Y-%m-%dT%H:%M:%S%:z").to_string();
1171
1172                    // They should be equal
1173                    assert_eq!(py_iso_str.to_string(), rust_iso_str);
1174                })
1175            }
1176
1177            #[test]
1178            fn test_duration_roundtrip(days in -999999999i64..=999999999i64) {
1179                // Test roundtrip conversion rust->python->rust for all allowed
1180                // python values of durations (from -999999999 to 999999999 days),
1181                Python::with_gil(|py| {
1182                    let dur = SignedDuration::new(days * 24 * 60 * 60, 0);
1183                    let py_delta = dur.into_pyobject(py).unwrap();
1184                    let roundtripped: SignedDuration = py_delta.extract().expect("Round trip");
1185                    assert_eq!(dur, roundtripped);
1186                })
1187            }
1188
1189            #[test]
1190            fn test_span_roundtrip(days in -999999999i64..=999999999i64) {
1191                // Test roundtrip conversion rust->python->rust for all allowed
1192                // python values of durations (from -999999999 to 999999999 days),
1193                Python::with_gil(|py| {
1194                    if let Ok(span) = Span::new().try_days(days) {
1195                        let relative_to = SpanRelativeTo::days_are_24_hours();
1196                        let jiff_duration = span.to_duration(relative_to).unwrap();
1197                        let py_delta = jiff_duration.into_pyobject(py).unwrap();
1198                        let roundtripped: Span = py_delta.extract().expect("Round trip");
1199                        assert_eq!(span.compare((roundtripped, relative_to)).unwrap(), Ordering::Equal);
1200                    }
1201                })
1202            }
1203
1204            #[test]
1205            fn test_fixed_offset_roundtrip(secs in -86399i32..=86399i32) {
1206                Python::with_gil(|py| {
1207                    let offset = Offset::from_seconds(secs).unwrap();
1208                    let py_offset = offset.into_pyobject(py).unwrap();
1209                    let roundtripped: Offset = py_offset.extract().expect("Round trip");
1210                    assert_eq!(offset, roundtripped);
1211                })
1212            }
1213
1214            #[test]
1215            fn test_naive_date_roundtrip(
1216                year in 1i32..=9999i32,
1217                month in 1u32..=12u32,
1218                day in 1u32..=31u32
1219            ) {
1220                // Test roundtrip conversion rust->python->rust for all allowed
1221                // python dates (from year 1 to year 9999)
1222                Python::with_gil(|py| {
1223                    if let Ok(date) = try_date(year, month, day) {
1224                        let py_date = date.into_pyobject(py).unwrap();
1225                        let roundtripped: Date = py_date.extract().expect("Round trip");
1226                        assert_eq!(date, roundtripped);
1227                    }
1228                })
1229            }
1230
1231            #[test]
1232            fn test_naive_time_roundtrip(
1233                hour in 0u32..=23u32,
1234                min in 0u32..=59u32,
1235                sec in 0u32..=59u32,
1236                micro in 0u32..=1_999_999u32
1237            ) {
1238                Python::with_gil(|py| {
1239                    if let Ok(time) = try_time(hour, min, sec, micro) {
1240                        let py_time = time.into_pyobject(py).unwrap();
1241                        let roundtripped: Time = py_time.extract().expect("Round trip");
1242                        assert_eq!(time, roundtripped);
1243                    }
1244                })
1245            }
1246
1247            #[test]
1248            fn test_naive_datetime_roundtrip(
1249                year in 1i32..=9999i32,
1250                month in 1u32..=12u32,
1251                day in 1u32..=31u32,
1252                hour in 0u32..=24u32,
1253                min in 0u32..=60u32,
1254                sec in 0u32..=60u32,
1255                micro in 0u32..=999_999u32
1256            ) {
1257                Python::with_gil(|py| {
1258                    let date_opt = try_date(year, month, day);
1259                    let time_opt = try_time(hour, min, sec, micro);
1260                    if let (Ok(date), Ok(time)) = (date_opt, time_opt) {
1261                        let dt = DateTime::from_parts(date, time);
1262                        let pydt = dt.into_pyobject(py).unwrap();
1263                        let roundtripped: DateTime = pydt.extract().expect("Round trip");
1264                        assert_eq!(dt, roundtripped);
1265                    }
1266                })
1267            }
1268
1269            #[test]
1270            fn test_utc_datetime_roundtrip(
1271                year in 1i32..=9999i32,
1272                month in 1u32..=12u32,
1273                day in 1u32..=31u32,
1274                hour in 0u32..=23u32,
1275                min in 0u32..=59u32,
1276                sec in 0u32..=59u32,
1277                micro in 0u32..=1_999_999u32
1278            ) {
1279                Python::with_gil(|py| {
1280                    let date_opt = try_date(year, month, day);
1281                    let time_opt = try_time(hour, min, sec, micro);
1282                    if let (Ok(date), Ok(time)) = (date_opt, time_opt) {
1283                        let dt: Zoned = DateTime::from_parts(date, time).to_zoned(TimeZone::UTC).unwrap();
1284                        let py_dt = (&dt).into_pyobject(py).unwrap();
1285                        let roundtripped: Zoned = py_dt.extract().expect("Round trip");
1286                        assert_eq!(dt, roundtripped);
1287                    }
1288                })
1289            }
1290
1291            #[test]
1292            fn test_fixed_offset_datetime_roundtrip(
1293                year in 1i32..=9999i32,
1294                month in 1u32..=12u32,
1295                day in 1u32..=31u32,
1296                hour in 0u32..=23u32,
1297                min in 0u32..=59u32,
1298                sec in 0u32..=59u32,
1299                micro in 0u32..=1_999_999u32,
1300                offset_secs in -86399i32..=86399i32
1301            ) {
1302                Python::with_gil(|py| {
1303                    let date_opt = try_date(year, month, day);
1304                    let time_opt = try_time(hour, min, sec, micro);
1305                    let offset = Offset::from_seconds(offset_secs).unwrap();
1306                    if let (Ok(date), Ok(time)) = (date_opt, time_opt) {
1307                        let dt: Zoned = DateTime::from_parts(date, time).to_zoned(offset.to_time_zone()).unwrap();
1308                        let py_dt = (&dt).into_pyobject(py).unwrap();
1309                        let roundtripped: Zoned = py_dt.extract().expect("Round trip");
1310                        assert_eq!(dt, roundtripped);
1311                    }
1312                })
1313            }
1314
1315            #[test]
1316            #[cfg(all(Py_3_9, not(windows)))]
1317            fn test_zoned_datetime_roundtrip_around_timezone_transition(
1318                (timezone, transition) in prop_oneof![
1319                                Just(&TimeZone::get("Europe/London").unwrap()),
1320                                Just(&TimeZone::get("America/New_York").unwrap()),
1321                                Just(&TimeZone::get("Australia/Sydney").unwrap()),
1322                            ].prop_flat_map(|tz| (Just(tz), timezone_transitions(tz))),
1323                hour in -2i32..=2i32,
1324                min in 0u32..=59u32,
1325            ) {
1326
1327                Python::with_gil(|py| {
1328                    let transition_moment = transition.timestamp();
1329                    let zoned = (transition_moment - Span::new().hours(hour).minutes(min))
1330                        .to_zoned(timezone.clone());
1331
1332                    let py_dt = (&zoned).into_pyobject(py).unwrap();
1333                    let roundtripped: Zoned = py_dt.extract().expect("Round trip");
1334                    assert_eq!(zoned, roundtripped);
1335                })
1336
1337            }
1338        }
1339    }
1340}
⚠️ Internal Docs ⚠️ Not Public API 👉 Official Docs Here