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