Skip to main content

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;
39#[cfg(feature = "experimental-inspect")]
40use crate::inspect::PyStaticExpr;
41#[cfg(feature = "experimental-inspect")]
42use crate::type_hint_identifier;
43use crate::types::{any::PyAnyMethods, PyTzInfo};
44#[cfg(all(feature = "experimental-inspect", not(Py_3_9)))]
45use crate::PyTypeInfo;
46use crate::{intern, Borrowed, Bound, FromPyObject, PyAny, PyErr, Python};
47use chrono_tz::Tz;
48use std::borrow::Cow;
49use std::str::FromStr;
50
51impl<'py> IntoPyObject<'py> for Tz {
52    type Target = PyTzInfo;
53    type Output = Bound<'py, Self::Target>;
54    type Error = PyErr;
55
56    #[cfg(all(feature = "experimental-inspect", Py_3_9))]
57    const OUTPUT_TYPE: PyStaticExpr = type_hint_identifier!("zoneinfo", "ZoneInfo");
58
59    #[cfg(all(feature = "experimental-inspect", not(Py_3_9)))]
60    const OUTPUT_TYPE: PyStaticExpr = PyTzInfo::TYPE_HINT;
61
62    fn into_pyobject(self, py: Python<'py>) -> Result<Self::Output, Self::Error> {
63        PyTzInfo::timezone(py, self.name())
64    }
65}
66
67impl<'py> IntoPyObject<'py> for &Tz {
68    type Target = PyTzInfo;
69    type Output = Bound<'py, Self::Target>;
70    type Error = PyErr;
71
72    #[cfg(feature = "experimental-inspect")]
73    const OUTPUT_TYPE: PyStaticExpr = Tz::OUTPUT_TYPE;
74
75    #[inline]
76    fn into_pyobject(self, py: Python<'py>) -> Result<Self::Output, Self::Error> {
77        (*self).into_pyobject(py)
78    }
79}
80
81impl FromPyObject<'_, '_> for Tz {
82    type Error = PyErr;
83
84    #[cfg(all(feature = "experimental-inspect", Py_3_9))]
85    const INPUT_TYPE: PyStaticExpr = type_hint_identifier!("zoneinfo", "ZoneInfo");
86
87    #[cfg(all(feature = "experimental-inspect", not(Py_3_9)))]
88    const INPUT_TYPE: PyStaticExpr = PyTzInfo::TYPE_HINT;
89
90    fn extract(ob: Borrowed<'_, '_, PyAny>) -> Result<Self, Self::Error> {
91        Tz::from_str(
92            &ob.getattr(intern!(ob.py(), "key"))?
93                .extract::<Cow<'_, str>>()?,
94        )
95        .map_err(|e| PyValueError::new_err(e.to_string()))
96    }
97}
98
99#[cfg(all(test, not(windows)))] // Troubles loading timezones on Windows
100mod tests {
101    use super::*;
102    use crate::prelude::PyAnyMethods;
103    use crate::types::IntoPyDict;
104    use crate::types::PyTzInfo;
105    use crate::Bound;
106    use crate::Python;
107    use chrono::offset::LocalResult;
108    use chrono::NaiveDate;
109    use chrono::{DateTime, Utc};
110    use chrono_tz::Tz;
111
112    #[test]
113    fn test_frompyobject() {
114        Python::attach(|py| {
115            assert_eq!(
116                new_zoneinfo(py, "Europe/Paris").extract::<Tz>().unwrap(),
117                Tz::Europe__Paris
118            );
119            assert_eq!(new_zoneinfo(py, "UTC").extract::<Tz>().unwrap(), Tz::UTC);
120            assert_eq!(
121                new_zoneinfo(py, "Etc/GMT-5").extract::<Tz>().unwrap(),
122                Tz::Etc__GMTMinus5
123            );
124        });
125    }
126
127    #[test]
128    fn test_ambiguous_datetime_to_pyobject() {
129        let dates = [
130            DateTime::<Utc>::from_str("2020-10-24 23:00:00 UTC").unwrap(),
131            DateTime::<Utc>::from_str("2020-10-25 00:00:00 UTC").unwrap(),
132            DateTime::<Utc>::from_str("2020-10-25 01:00:00 UTC").unwrap(),
133        ];
134
135        let dates = dates.map(|dt| dt.with_timezone(&Tz::Europe__London));
136
137        assert_eq!(
138            dates.map(|dt| dt.to_string()),
139            [
140                "2020-10-25 00:00:00 BST",
141                "2020-10-25 01:00:00 BST",
142                "2020-10-25 01:00:00 GMT"
143            ]
144        );
145
146        let dates = Python::attach(|py| {
147            let pydates = dates.map(|dt| dt.into_pyobject(py).unwrap());
148            assert_eq!(
149                pydates
150                    .clone()
151                    .map(|dt| dt.getattr("hour").unwrap().extract::<usize>().unwrap()),
152                [0, 1, 1]
153            );
154
155            assert_eq!(
156                pydates
157                    .clone()
158                    .map(|dt| dt.getattr("fold").unwrap().extract::<usize>().unwrap() > 0),
159                [false, false, true]
160            );
161
162            pydates.map(|dt| dt.extract::<DateTime<Tz>>().unwrap())
163        });
164
165        assert_eq!(
166            dates.map(|dt| dt.to_string()),
167            [
168                "2020-10-25 00:00:00 BST",
169                "2020-10-25 01:00:00 BST",
170                "2020-10-25 01:00:00 GMT"
171            ]
172        );
173    }
174
175    #[test]
176    fn test_nonexistent_datetime_from_pyobject() {
177        // Pacific_Apia skipped the 30th of December 2011 entirely
178
179        let naive_dt = NaiveDate::from_ymd_opt(2011, 12, 30)
180            .unwrap()
181            .and_hms_opt(2, 0, 0)
182            .unwrap();
183        let tz = Tz::Pacific__Apia;
184
185        // sanity check
186        assert_eq!(naive_dt.and_local_timezone(tz), LocalResult::None);
187
188        Python::attach(|py| {
189            // create as a Python object manually
190            let py_tz = tz.into_pyobject(py).unwrap();
191            let py_dt_naive = naive_dt.into_pyobject(py).unwrap();
192            let py_dt = py_dt_naive
193                .call_method(
194                    "replace",
195                    (),
196                    Some(&[("tzinfo", py_tz)].into_py_dict(py).unwrap()),
197                )
198                .unwrap();
199
200            // now try to extract
201            let err = py_dt.extract::<DateTime<Tz>>().unwrap_err();
202            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");
203        });
204    }
205
206    #[test]
207    #[cfg(not(Py_GIL_DISABLED))] // https://github.com/python/cpython/issues/116738#issuecomment-2404360445
208    fn test_into_pyobject() {
209        Python::attach(|py| {
210            let assert_eq = |l: Bound<'_, PyTzInfo>, r: Bound<'_, PyTzInfo>| {
211                assert!(l.eq(&r).unwrap(), "{l:?} != {r:?}");
212            };
213
214            assert_eq(
215                Tz::Europe__Paris.into_pyobject(py).unwrap(),
216                new_zoneinfo(py, "Europe/Paris"),
217            );
218            assert_eq(Tz::UTC.into_pyobject(py).unwrap(), new_zoneinfo(py, "UTC"));
219            assert_eq(
220                Tz::Etc__GMTMinus5.into_pyobject(py).unwrap(),
221                new_zoneinfo(py, "Etc/GMT-5"),
222            );
223        });
224    }
225
226    fn new_zoneinfo<'py>(py: Python<'py>, name: &str) -> Bound<'py, PyTzInfo> {
227        PyTzInfo::timezone(py, name).unwrap()
228    }
229}
⚠️ Internal Docs ⚠️ Not Public API 👉 Official Docs Here