pyo3/conversions/
chrono_tz.rs

1#![cfg(all(Py_3_9, feature = "chrono-tz"))]
2
3//! Conversions to and from [chrono-tz](https://docs.rs/chrono-tz/)’s `Tz`.
4//!
5//! This feature requires at least Python 3.9.
6//!
7//! # Setup
8//!
9//! To use this feature, add this to your **`Cargo.toml`**:
10//!
11//! ```toml
12//! [dependencies]
13//! chrono-tz = "0.8"
14#![doc = concat!("pyo3 = { version = \"", env!("CARGO_PKG_VERSION"),  "\", features = [\"chrono-tz\"] }")]
15//! ```
16//!
17//! Note that you must use compatible versions of chrono, chrono-tz and PyO3.
18//! The required chrono version may vary based on the version of PyO3.
19//!
20//! # Example: Convert a `zoneinfo.ZoneInfo` to chrono-tz's `Tz`
21//!
22//! ```rust,no_run
23//! use chrono_tz::Tz;
24//! use pyo3::{Python, PyResult, IntoPyObject, types::PyAnyMethods};
25//!
26//! fn main() -> PyResult<()> {
27//!     Python::initialize();
28//!     Python::attach(|py| {
29//!         // Convert to Python
30//!         let py_tzinfo = Tz::Europe__Paris.into_pyobject(py)?;
31//!         // Convert back to Rust
32//!         assert_eq!(py_tzinfo.extract::<Tz>()?, Tz::Europe__Paris);
33//!         Ok(())
34//!     })
35//! }
36//! ```
37use crate::conversion::IntoPyObject;
38use crate::exceptions::PyValueError;
39use crate::pybacked::PyBackedStr;
40use crate::types::{any::PyAnyMethods, PyTzInfo};
41use crate::{intern, Borrowed, Bound, FromPyObject, PyAny, PyErr, Python};
42use chrono_tz::Tz;
43use std::str::FromStr;
44
45impl<'py> IntoPyObject<'py> for Tz {
46    type Target = PyTzInfo;
47    type Output = Bound<'py, Self::Target>;
48    type Error = PyErr;
49
50    fn into_pyobject(self, py: Python<'py>) -> Result<Self::Output, Self::Error> {
51        PyTzInfo::timezone(py, self.name())
52    }
53}
54
55impl<'py> IntoPyObject<'py> for &Tz {
56    type Target = PyTzInfo;
57    type Output = Bound<'py, Self::Target>;
58    type Error = PyErr;
59
60    #[inline]
61    fn into_pyobject(self, py: Python<'py>) -> Result<Self::Output, Self::Error> {
62        (*self).into_pyobject(py)
63    }
64}
65
66impl FromPyObject<'_, '_> for Tz {
67    type Error = PyErr;
68
69    fn extract(ob: Borrowed<'_, '_, PyAny>) -> Result<Self, Self::Error> {
70        Tz::from_str(
71            &ob.getattr(intern!(ob.py(), "key"))?
72                .extract::<PyBackedStr>()?,
73        )
74        .map_err(|e| PyValueError::new_err(e.to_string()))
75    }
76}
77
78#[cfg(all(test, not(windows)))] // Troubles loading timezones on Windows
79mod tests {
80    use super::*;
81    use crate::prelude::PyAnyMethods;
82    use crate::types::IntoPyDict;
83    use crate::types::PyTzInfo;
84    use crate::Bound;
85    use crate::Python;
86    use chrono::offset::LocalResult;
87    use chrono::NaiveDate;
88    use chrono::{DateTime, Utc};
89    use chrono_tz::Tz;
90
91    #[test]
92    fn test_frompyobject() {
93        Python::attach(|py| {
94            assert_eq!(
95                new_zoneinfo(py, "Europe/Paris").extract::<Tz>().unwrap(),
96                Tz::Europe__Paris
97            );
98            assert_eq!(new_zoneinfo(py, "UTC").extract::<Tz>().unwrap(), Tz::UTC);
99            assert_eq!(
100                new_zoneinfo(py, "Etc/GMT-5").extract::<Tz>().unwrap(),
101                Tz::Etc__GMTMinus5
102            );
103        });
104    }
105
106    #[test]
107    fn test_ambiguous_datetime_to_pyobject() {
108        let dates = [
109            DateTime::<Utc>::from_str("2020-10-24 23:00:00 UTC").unwrap(),
110            DateTime::<Utc>::from_str("2020-10-25 00:00:00 UTC").unwrap(),
111            DateTime::<Utc>::from_str("2020-10-25 01:00:00 UTC").unwrap(),
112        ];
113
114        let dates = dates.map(|dt| dt.with_timezone(&Tz::Europe__London));
115
116        assert_eq!(
117            dates.map(|dt| dt.to_string()),
118            [
119                "2020-10-25 00:00:00 BST",
120                "2020-10-25 01:00:00 BST",
121                "2020-10-25 01:00:00 GMT"
122            ]
123        );
124
125        let dates = Python::attach(|py| {
126            let pydates = dates.map(|dt| dt.into_pyobject(py).unwrap());
127            assert_eq!(
128                pydates
129                    .clone()
130                    .map(|dt| dt.getattr("hour").unwrap().extract::<usize>().unwrap()),
131                [0, 1, 1]
132            );
133
134            assert_eq!(
135                pydates
136                    .clone()
137                    .map(|dt| dt.getattr("fold").unwrap().extract::<usize>().unwrap() > 0),
138                [false, false, true]
139            );
140
141            pydates.map(|dt| dt.extract::<DateTime<Tz>>().unwrap())
142        });
143
144        assert_eq!(
145            dates.map(|dt| dt.to_string()),
146            [
147                "2020-10-25 00:00:00 BST",
148                "2020-10-25 01:00:00 BST",
149                "2020-10-25 01:00:00 GMT"
150            ]
151        );
152    }
153
154    #[test]
155    fn test_nonexistent_datetime_from_pyobject() {
156        // Pacific_Apia skipped the 30th of December 2011 entirely
157
158        let naive_dt = NaiveDate::from_ymd_opt(2011, 12, 30)
159            .unwrap()
160            .and_hms_opt(2, 0, 0)
161            .unwrap();
162        let tz = Tz::Pacific__Apia;
163
164        // sanity check
165        assert_eq!(naive_dt.and_local_timezone(tz), LocalResult::None);
166
167        Python::attach(|py| {
168            // create as a Python object manually
169            let py_tz = tz.into_pyobject(py).unwrap();
170            let py_dt_naive = naive_dt.into_pyobject(py).unwrap();
171            let py_dt = py_dt_naive
172                .call_method(
173                    "replace",
174                    (),
175                    Some(&[("tzinfo", py_tz)].into_py_dict(py).unwrap()),
176                )
177                .unwrap();
178
179            // now try to extract
180            let err = py_dt.extract::<DateTime<Tz>>().unwrap_err();
181            assert_eq!(err.to_string(), "ValueError: The datetime datetime.datetime(2011, 12, 30, 2, 0, tzinfo=zoneinfo.ZoneInfo(key='Pacific/Apia')) contains an incompatible timezone");
182        });
183    }
184
185    #[test]
186    #[cfg(not(Py_GIL_DISABLED))] // https://github.com/python/cpython/issues/116738#issuecomment-2404360445
187    fn test_into_pyobject() {
188        Python::attach(|py| {
189            let assert_eq = |l: Bound<'_, PyTzInfo>, r: Bound<'_, PyTzInfo>| {
190                assert!(l.eq(&r).unwrap(), "{l:?} != {r:?}");
191            };
192
193            assert_eq(
194                Tz::Europe__Paris.into_pyobject(py).unwrap(),
195                new_zoneinfo(py, "Europe/Paris"),
196            );
197            assert_eq(Tz::UTC.into_pyobject(py).unwrap(), new_zoneinfo(py, "UTC"));
198            assert_eq(
199                Tz::Etc__GMTMinus5.into_pyobject(py).unwrap(),
200                new_zoneinfo(py, "Etc/GMT-5"),
201            );
202        });
203    }
204
205    fn new_zoneinfo<'py>(py: Python<'py>, name: &str) -> Bound<'py, PyTzInfo> {
206        PyTzInfo::timezone(py, name).unwrap()
207    }
208}