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