pyo3/conversions/
chrono_tz.rs1#![cfg(all(Py_3_9, 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;
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)))] mod 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 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 assert_eq!(naive_dt.and_local_timezone(tz), LocalResult::None);
187
188 Python::attach(|py| {
189 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 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))] 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}