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;
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)))] mod 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 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 assert_eq!(naive_dt.and_local_timezone(tz), LocalResult::None);
166
167 Python::attach(|py| {
168 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 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))] 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}