1 | #![cfg (feature = "chrono" )] |
2 | |
3 | //! Conversions to and from [chrono](https://docs.rs/chrono/)’s `Duration`, |
4 | //! `NaiveDate`, `NaiveTime`, `DateTime<Tz>`, `FixedOffset`, and `Utc`. |
5 | //! |
6 | //! # Setup |
7 | //! |
8 | //! To use this feature, add this to your **`Cargo.toml`**: |
9 | //! |
10 | //! ```toml |
11 | //! [dependencies] |
12 | //! chrono = "0.4" |
13 | #![doc = concat!("pyo3 = { version = \"" , env!("CARGO_PKG_VERSION" ), " \", features = [ \"chrono \"] }" )] |
14 | //! ``` |
15 | //! |
16 | //! Note that you must use compatible versions of chrono and PyO3. |
17 | //! The required chrono version may vary based on the version of PyO3. |
18 | //! |
19 | //! # Example: Convert a `datetime.datetime` to chrono's `DateTime<Utc>` |
20 | //! |
21 | //! ```rust |
22 | //! use chrono::{DateTime, Duration, TimeZone, Utc}; |
23 | //! use pyo3::{Python, PyResult, IntoPyObject, types::PyAnyMethods}; |
24 | //! |
25 | //! fn main() -> PyResult<()> { |
26 | //! pyo3::prepare_freethreaded_python(); |
27 | //! Python::with_gil(|py| { |
28 | //! // Build some chrono values |
29 | //! let chrono_datetime = Utc.with_ymd_and_hms(2022, 1, 1, 12, 0, 0).unwrap(); |
30 | //! let chrono_duration = Duration::seconds(1); |
31 | //! // Convert them to Python |
32 | //! let py_datetime = chrono_datetime.into_pyobject(py)?; |
33 | //! let py_timedelta = chrono_duration.into_pyobject(py)?; |
34 | //! // Do an operation in Python |
35 | //! let py_sum = py_datetime.call_method1("__add__" , (py_timedelta,))?; |
36 | //! // Convert back to Rust |
37 | //! let chrono_sum: DateTime<Utc> = py_sum.extract()?; |
38 | //! println!("DateTime<Utc>: {}" , chrono_datetime); |
39 | //! Ok(()) |
40 | //! }) |
41 | //! } |
42 | //! ``` |
43 | |
44 | use crate::conversion::IntoPyObject; |
45 | use crate::exceptions::{PyTypeError, PyUserWarning, PyValueError}; |
46 | #[cfg (Py_LIMITED_API)] |
47 | use crate::intern; |
48 | use crate::types::any::PyAnyMethods; |
49 | #[cfg (not(Py_LIMITED_API))] |
50 | use crate::types::datetime::timezone_from_offset; |
51 | #[cfg (Py_LIMITED_API)] |
52 | use crate::types::datetime_abi3::{check_type, timezone_utc, DatetimeTypes}; |
53 | #[cfg (Py_LIMITED_API)] |
54 | use crate::types::IntoPyDict; |
55 | use crate::types::PyNone; |
56 | #[cfg (not(Py_LIMITED_API))] |
57 | use crate::types::{ |
58 | timezone_utc, PyDate, PyDateAccess, PyDateTime, PyDelta, PyDeltaAccess, PyTime, PyTimeAccess, |
59 | PyTzInfo, PyTzInfoAccess, |
60 | }; |
61 | use crate::{ffi, Bound, FromPyObject, IntoPyObjectExt, PyAny, PyErr, PyObject, PyResult, Python}; |
62 | #[allow (deprecated)] |
63 | use crate::{IntoPy, ToPyObject}; |
64 | use chrono::offset::{FixedOffset, Utc}; |
65 | use chrono::{ |
66 | DateTime, Datelike, Duration, LocalResult, NaiveDate, NaiveDateTime, NaiveTime, Offset, |
67 | TimeZone, Timelike, |
68 | }; |
69 | |
70 | #[allow (deprecated)] |
71 | impl ToPyObject for Duration { |
72 | #[inline ] |
73 | fn to_object(&self, py: Python<'_>) -> PyObject { |
74 | self.into_pyobject(py).unwrap().into_any().unbind() |
75 | } |
76 | } |
77 | |
78 | #[allow (deprecated)] |
79 | impl IntoPy<PyObject> for Duration { |
80 | #[inline ] |
81 | fn into_py(self, py: Python<'_>) -> PyObject { |
82 | self.into_pyobject(py).unwrap().into_any().unbind() |
83 | } |
84 | } |
85 | |
86 | impl<'py> IntoPyObject<'py> for Duration { |
87 | #[cfg (Py_LIMITED_API)] |
88 | type Target = PyAny; |
89 | #[cfg (not(Py_LIMITED_API))] |
90 | type Target = PyDelta; |
91 | type Output = Bound<'py, Self::Target>; |
92 | type Error = PyErr; |
93 | |
94 | fn into_pyobject(self, py: Python<'py>) -> Result<Self::Output, Self::Error> { |
95 | // Total number of days |
96 | let days = self.num_days(); |
97 | // Remainder of seconds |
98 | let secs_dur = self - Duration::days(days); |
99 | let secs = secs_dur.num_seconds(); |
100 | // Fractional part of the microseconds |
101 | let micros = (secs_dur - Duration::seconds(secs_dur.num_seconds())) |
102 | .num_microseconds() |
103 | // This should never panic since we are just getting the fractional |
104 | // part of the total microseconds, which should never overflow. |
105 | .unwrap(); |
106 | |
107 | #[cfg (not(Py_LIMITED_API))] |
108 | { |
109 | // We do not need to check the days i64 to i32 cast from rust because |
110 | // python will panic with OverflowError. |
111 | // We pass true as the `normalize` parameter since we'd need to do several checks here to |
112 | // avoid that, and it shouldn't have a big performance impact. |
113 | // The seconds and microseconds cast should never overflow since it's at most the number of seconds per day |
114 | PyDelta::new( |
115 | py, |
116 | days.try_into().unwrap_or(i32::MAX), |
117 | secs.try_into()?, |
118 | micros.try_into()?, |
119 | true, |
120 | ) |
121 | } |
122 | |
123 | #[cfg (Py_LIMITED_API)] |
124 | { |
125 | DatetimeTypes::try_get(py) |
126 | .and_then(|dt| dt.timedelta.bind(py).call1((days, secs, micros))) |
127 | } |
128 | } |
129 | } |
130 | |
131 | impl<'py> IntoPyObject<'py> for &Duration { |
132 | #[cfg (Py_LIMITED_API)] |
133 | type Target = PyAny; |
134 | #[cfg (not(Py_LIMITED_API))] |
135 | type Target = PyDelta; |
136 | type Output = Bound<'py, Self::Target>; |
137 | type Error = PyErr; |
138 | |
139 | #[inline ] |
140 | fn into_pyobject(self, py: Python<'py>) -> Result<Self::Output, Self::Error> { |
141 | (*self).into_pyobject(py) |
142 | } |
143 | } |
144 | |
145 | impl FromPyObject<'_> for Duration { |
146 | fn extract_bound(ob: &Bound<'_, PyAny>) -> PyResult<Duration> { |
147 | // Python size are much lower than rust size so we do not need bound checks. |
148 | // 0 <= microseconds < 1000000 |
149 | // 0 <= seconds < 3600*24 |
150 | // -999999999 <= days <= 999999999 |
151 | #[cfg (not(Py_LIMITED_API))] |
152 | let (days, seconds, microseconds) = { |
153 | let delta = ob.downcast::<PyDelta>()?; |
154 | ( |
155 | delta.get_days().into(), |
156 | delta.get_seconds().into(), |
157 | delta.get_microseconds().into(), |
158 | ) |
159 | }; |
160 | #[cfg (Py_LIMITED_API)] |
161 | let (days, seconds, microseconds) = { |
162 | check_type(ob, &DatetimeTypes::get(ob.py()).timedelta, "PyDelta" )?; |
163 | ( |
164 | ob.getattr(intern!(ob.py(), "days" ))?.extract()?, |
165 | ob.getattr(intern!(ob.py(), "seconds" ))?.extract()?, |
166 | ob.getattr(intern!(ob.py(), "microseconds" ))?.extract()?, |
167 | ) |
168 | }; |
169 | Ok( |
170 | Duration::days(days) |
171 | + Duration::seconds(seconds) |
172 | + Duration::microseconds(microseconds), |
173 | ) |
174 | } |
175 | } |
176 | |
177 | #[allow (deprecated)] |
178 | impl ToPyObject for NaiveDate { |
179 | #[inline ] |
180 | fn to_object(&self, py: Python<'_>) -> PyObject { |
181 | self.into_pyobject(py).unwrap().into_any().unbind() |
182 | } |
183 | } |
184 | |
185 | #[allow (deprecated)] |
186 | impl IntoPy<PyObject> for NaiveDate { |
187 | #[inline ] |
188 | fn into_py(self, py: Python<'_>) -> PyObject { |
189 | self.into_pyobject(py).unwrap().into_any().unbind() |
190 | } |
191 | } |
192 | |
193 | impl<'py> IntoPyObject<'py> for NaiveDate { |
194 | #[cfg (Py_LIMITED_API)] |
195 | type Target = PyAny; |
196 | #[cfg (not(Py_LIMITED_API))] |
197 | type Target = PyDate; |
198 | type Output = Bound<'py, Self::Target>; |
199 | type Error = PyErr; |
200 | |
201 | fn into_pyobject(self, py: Python<'py>) -> Result<Self::Output, Self::Error> { |
202 | let DateArgs { year: i32, month: u8, day: u8 } = (&self).into(); |
203 | #[cfg (not(Py_LIMITED_API))] |
204 | { |
205 | PyDate::new(py, year, month, day) |
206 | } |
207 | |
208 | #[cfg (Py_LIMITED_API)] |
209 | { |
210 | DatetimeTypes::try_get(py).and_then(|dt: &DatetimeTypes| dt.date.bind(py).call1((year, month, day))) |
211 | } |
212 | } |
213 | } |
214 | |
215 | impl<'py> IntoPyObject<'py> for &NaiveDate { |
216 | #[cfg (Py_LIMITED_API)] |
217 | type Target = PyAny; |
218 | #[cfg (not(Py_LIMITED_API))] |
219 | type Target = PyDate; |
220 | type Output = Bound<'py, Self::Target>; |
221 | type Error = PyErr; |
222 | |
223 | #[inline ] |
224 | fn into_pyobject(self, py: Python<'py>) -> Result<Self::Output, Self::Error> { |
225 | (*self).into_pyobject(py) |
226 | } |
227 | } |
228 | |
229 | impl FromPyObject<'_> for NaiveDate { |
230 | fn extract_bound(ob: &Bound<'_, PyAny>) -> PyResult<NaiveDate> { |
231 | #[cfg (not(Py_LIMITED_API))] |
232 | { |
233 | let date = ob.downcast::<PyDate>()?; |
234 | py_date_to_naive_date(date) |
235 | } |
236 | #[cfg (Py_LIMITED_API)] |
237 | { |
238 | check_type(value:ob, &DatetimeTypes::get(ob.py()).date, type_name:"PyDate" )?; |
239 | py_date_to_naive_date(py_date:ob) |
240 | } |
241 | } |
242 | } |
243 | |
244 | #[allow (deprecated)] |
245 | impl ToPyObject for NaiveTime { |
246 | #[inline ] |
247 | fn to_object(&self, py: Python<'_>) -> PyObject { |
248 | self.into_pyobject(py).unwrap().into_any().unbind() |
249 | } |
250 | } |
251 | |
252 | #[allow (deprecated)] |
253 | impl IntoPy<PyObject> for NaiveTime { |
254 | #[inline ] |
255 | fn into_py(self, py: Python<'_>) -> PyObject { |
256 | self.into_pyobject(py).unwrap().into_any().unbind() |
257 | } |
258 | } |
259 | |
260 | impl<'py> IntoPyObject<'py> for NaiveTime { |
261 | #[cfg (Py_LIMITED_API)] |
262 | type Target = PyAny; |
263 | #[cfg (not(Py_LIMITED_API))] |
264 | type Target = PyTime; |
265 | type Output = Bound<'py, Self::Target>; |
266 | type Error = PyErr; |
267 | |
268 | fn into_pyobject(self, py: Python<'py>) -> Result<Self::Output, Self::Error> { |
269 | let TimeArgs { |
270 | hour, |
271 | min, |
272 | sec, |
273 | micro, |
274 | truncated_leap_second, |
275 | } = (&self).into(); |
276 | |
277 | #[cfg (not(Py_LIMITED_API))] |
278 | let time = PyTime::new(py, hour, min, sec, micro, None)?; |
279 | |
280 | #[cfg (Py_LIMITED_API)] |
281 | let time = DatetimeTypes::try_get(py) |
282 | .and_then(|dt| dt.time.bind(py).call1((hour, min, sec, micro)))?; |
283 | |
284 | if truncated_leap_second { |
285 | warn_truncated_leap_second(&time); |
286 | } |
287 | |
288 | Ok(time) |
289 | } |
290 | } |
291 | |
292 | impl<'py> IntoPyObject<'py> for &NaiveTime { |
293 | #[cfg (Py_LIMITED_API)] |
294 | type Target = PyAny; |
295 | #[cfg (not(Py_LIMITED_API))] |
296 | type Target = PyTime; |
297 | type Output = Bound<'py, Self::Target>; |
298 | type Error = PyErr; |
299 | |
300 | #[inline ] |
301 | fn into_pyobject(self, py: Python<'py>) -> Result<Self::Output, Self::Error> { |
302 | (*self).into_pyobject(py) |
303 | } |
304 | } |
305 | |
306 | impl FromPyObject<'_> for NaiveTime { |
307 | fn extract_bound(ob: &Bound<'_, PyAny>) -> PyResult<NaiveTime> { |
308 | #[cfg (not(Py_LIMITED_API))] |
309 | { |
310 | let time = ob.downcast::<PyTime>()?; |
311 | py_time_to_naive_time(time) |
312 | } |
313 | #[cfg (Py_LIMITED_API)] |
314 | { |
315 | check_type(value:ob, &DatetimeTypes::get(ob.py()).time, type_name:"PyTime" )?; |
316 | py_time_to_naive_time(py_time:ob) |
317 | } |
318 | } |
319 | } |
320 | |
321 | #[allow (deprecated)] |
322 | impl ToPyObject for NaiveDateTime { |
323 | #[inline ] |
324 | fn to_object(&self, py: Python<'_>) -> PyObject { |
325 | self.into_pyobject(py).unwrap().into_any().unbind() |
326 | } |
327 | } |
328 | |
329 | #[allow (deprecated)] |
330 | impl IntoPy<PyObject> for NaiveDateTime { |
331 | #[inline ] |
332 | fn into_py(self, py: Python<'_>) -> PyObject { |
333 | self.into_pyobject(py).unwrap().into_any().unbind() |
334 | } |
335 | } |
336 | |
337 | impl<'py> IntoPyObject<'py> for NaiveDateTime { |
338 | #[cfg (Py_LIMITED_API)] |
339 | type Target = PyAny; |
340 | #[cfg (not(Py_LIMITED_API))] |
341 | type Target = PyDateTime; |
342 | type Output = Bound<'py, Self::Target>; |
343 | type Error = PyErr; |
344 | |
345 | fn into_pyobject(self, py: Python<'py>) -> Result<Self::Output, Self::Error> { |
346 | let DateArgs { year, month, day } = (&self.date()).into(); |
347 | let TimeArgs { |
348 | hour, |
349 | min, |
350 | sec, |
351 | micro, |
352 | truncated_leap_second, |
353 | } = (&self.time()).into(); |
354 | |
355 | #[cfg (not(Py_LIMITED_API))] |
356 | let datetime = PyDateTime::new(py, year, month, day, hour, min, sec, micro, None)?; |
357 | |
358 | #[cfg (Py_LIMITED_API)] |
359 | let datetime = DatetimeTypes::try_get(py).and_then(|dt| { |
360 | dt.datetime |
361 | .bind(py) |
362 | .call1((year, month, day, hour, min, sec, micro)) |
363 | })?; |
364 | |
365 | if truncated_leap_second { |
366 | warn_truncated_leap_second(&datetime); |
367 | } |
368 | |
369 | Ok(datetime) |
370 | } |
371 | } |
372 | |
373 | impl<'py> IntoPyObject<'py> for &NaiveDateTime { |
374 | #[cfg (Py_LIMITED_API)] |
375 | type Target = PyAny; |
376 | #[cfg (not(Py_LIMITED_API))] |
377 | type Target = PyDateTime; |
378 | type Output = Bound<'py, Self::Target>; |
379 | type Error = PyErr; |
380 | |
381 | #[inline ] |
382 | fn into_pyobject(self, py: Python<'py>) -> Result<Self::Output, Self::Error> { |
383 | (*self).into_pyobject(py) |
384 | } |
385 | } |
386 | |
387 | impl FromPyObject<'_> for NaiveDateTime { |
388 | fn extract_bound(dt: &Bound<'_, PyAny>) -> PyResult<NaiveDateTime> { |
389 | #[cfg (not(Py_LIMITED_API))] |
390 | let dt = dt.downcast::<PyDateTime>()?; |
391 | #[cfg (Py_LIMITED_API)] |
392 | check_type(value:dt, &DatetimeTypes::get(dt.py()).datetime, type_name:"PyDateTime" )?; |
393 | |
394 | // If the user tries to convert a timezone aware datetime into a naive one, |
395 | // we return a hard error. We could silently remove tzinfo, or assume local timezone |
396 | // and do a conversion, but better leave this decision to the user of the library. |
397 | #[cfg (not(Py_LIMITED_API))] |
398 | let has_tzinfo = dt.get_tzinfo().is_some(); |
399 | #[cfg (Py_LIMITED_API)] |
400 | let has_tzinfo: bool = !dt.getattr(attr_name:intern!(dt.py(), "tzinfo" ))?.is_none(); |
401 | if has_tzinfo { |
402 | return Err(PyTypeError::new_err(args:"expected a datetime without tzinfo" )); |
403 | } |
404 | |
405 | let dt: NaiveDateTime = NaiveDateTime::new(py_date_to_naive_date(py_date:dt)?, py_time_to_naive_time(py_time:dt)?); |
406 | Ok(dt) |
407 | } |
408 | } |
409 | |
410 | #[allow (deprecated)] |
411 | impl<Tz: TimeZone> ToPyObject for DateTime<Tz> { |
412 | fn to_object(&self, py: Python<'_>) -> PyObject { |
413 | // FIXME: convert to better timezone representation here than just convert to fixed offset |
414 | // See https://github.com/PyO3/pyo3/issues/3266 |
415 | let tz: Py = self.offset().fix().to_object(py); |
416 | let tz: &Bound<'_, PyAny> = tz.bind(py).downcast().unwrap(); |
417 | naive_datetime_to_py_datetime(py, &self.naive_local(), tzinfo:Some(tz)) |
418 | } |
419 | } |
420 | |
421 | #[allow (deprecated)] |
422 | impl<Tz: TimeZone> IntoPy<PyObject> for DateTime<Tz> { |
423 | fn into_py(self, py: Python<'_>) -> PyObject { |
424 | self.to_object(py) |
425 | } |
426 | } |
427 | |
428 | impl<'py, Tz: TimeZone> IntoPyObject<'py> for DateTime<Tz> |
429 | where |
430 | Tz: IntoPyObject<'py>, |
431 | { |
432 | #[cfg (Py_LIMITED_API)] |
433 | type Target = PyAny; |
434 | #[cfg (not(Py_LIMITED_API))] |
435 | type Target = PyDateTime; |
436 | type Output = Bound<'py, Self::Target>; |
437 | type Error = PyErr; |
438 | |
439 | #[inline ] |
440 | fn into_pyobject(self, py: Python<'py>) -> Result<Self::Output, Self::Error> { |
441 | (&self).into_pyobject(py) |
442 | } |
443 | } |
444 | |
445 | impl<'py, Tz: TimeZone> IntoPyObject<'py> for &DateTime<Tz> |
446 | where |
447 | Tz: IntoPyObject<'py>, |
448 | { |
449 | #[cfg (Py_LIMITED_API)] |
450 | type Target = PyAny; |
451 | #[cfg (not(Py_LIMITED_API))] |
452 | type Target = PyDateTime; |
453 | type Output = Bound<'py, Self::Target>; |
454 | type Error = PyErr; |
455 | |
456 | fn into_pyobject(self, py: Python<'py>) -> Result<Self::Output, Self::Error> { |
457 | let tz = self.timezone().into_bound_py_any(py)?; |
458 | |
459 | #[cfg (not(Py_LIMITED_API))] |
460 | let tz = tz.downcast()?; |
461 | |
462 | let DateArgs { year, month, day } = (&self.naive_local().date()).into(); |
463 | let TimeArgs { |
464 | hour, |
465 | min, |
466 | sec, |
467 | micro, |
468 | truncated_leap_second, |
469 | } = (&self.naive_local().time()).into(); |
470 | |
471 | let fold = matches!( |
472 | self.timezone().offset_from_local_datetime(&self.naive_local()), |
473 | LocalResult::Ambiguous(_, latest) if self.offset().fix() == latest.fix() |
474 | ); |
475 | |
476 | #[cfg (not(Py_LIMITED_API))] |
477 | let datetime = |
478 | PyDateTime::new_with_fold(py, year, month, day, hour, min, sec, micro, Some(tz), fold)?; |
479 | |
480 | #[cfg (Py_LIMITED_API)] |
481 | let datetime = DatetimeTypes::try_get(py).and_then(|dt| { |
482 | dt.datetime.bind(py).call( |
483 | (year, month, day, hour, min, sec, micro, tz), |
484 | Some(&[("fold" , fold as u8)].into_py_dict(py)?), |
485 | ) |
486 | })?; |
487 | |
488 | if truncated_leap_second { |
489 | warn_truncated_leap_second(&datetime); |
490 | } |
491 | |
492 | Ok(datetime) |
493 | } |
494 | } |
495 | |
496 | impl<Tz: TimeZone + for<'py> dynFromPyObject<'py>> FromPyObject<'_> for DateTime<Tz> { |
497 | fn extract_bound(dt: &Bound<'_, PyAny>) -> PyResult<DateTime<Tz>> { |
498 | #[cfg (not(Py_LIMITED_API))] |
499 | let dt = dt.downcast::<PyDateTime>()?; |
500 | #[cfg (Py_LIMITED_API)] |
501 | check_type(dt, &DatetimeTypes::get(dt.py()).datetime, "PyDateTime" )?; |
502 | |
503 | #[cfg (not(Py_LIMITED_API))] |
504 | let tzinfo = dt.get_tzinfo(); |
505 | #[cfg (Py_LIMITED_API)] |
506 | let tzinfo: Option<Bound<'_, PyAny>> = dt.getattr(intern!(dt.py(), "tzinfo" ))?.extract()?; |
507 | |
508 | let tz = if let Some(tzinfo) = tzinfo { |
509 | tzinfo.extract()? |
510 | } else { |
511 | return Err(PyTypeError::new_err( |
512 | "expected a datetime with non-None tzinfo" , |
513 | )); |
514 | }; |
515 | let naive_dt = NaiveDateTime::new(py_date_to_naive_date(dt)?, py_time_to_naive_time(dt)?); |
516 | match naive_dt.and_local_timezone(tz) { |
517 | LocalResult::Single(value) => Ok(value), |
518 | LocalResult::Ambiguous(earliest, latest) => { |
519 | #[cfg (not(Py_LIMITED_API))] |
520 | let fold = dt.get_fold(); |
521 | |
522 | #[cfg (Py_LIMITED_API)] |
523 | let fold = dt.getattr(intern!(dt.py(), "fold" ))?.extract::<usize>()? > 0; |
524 | |
525 | if fold { |
526 | Ok(latest) |
527 | } else { |
528 | Ok(earliest) |
529 | } |
530 | } |
531 | LocalResult::None => Err(PyValueError::new_err(format!( |
532 | "The datetime {:?} contains an incompatible timezone" , |
533 | dt |
534 | ))), |
535 | } |
536 | } |
537 | } |
538 | |
539 | #[allow (deprecated)] |
540 | impl ToPyObject for FixedOffset { |
541 | #[inline ] |
542 | fn to_object(&self, py: Python<'_>) -> PyObject { |
543 | self.into_pyobject(py).unwrap().into_any().unbind() |
544 | } |
545 | } |
546 | |
547 | #[allow (deprecated)] |
548 | impl IntoPy<PyObject> for FixedOffset { |
549 | #[inline ] |
550 | fn into_py(self, py: Python<'_>) -> PyObject { |
551 | self.into_pyobject(py).unwrap().into_any().unbind() |
552 | } |
553 | } |
554 | |
555 | impl<'py> IntoPyObject<'py> for FixedOffset { |
556 | #[cfg (Py_LIMITED_API)] |
557 | type Target = PyAny; |
558 | #[cfg (not(Py_LIMITED_API))] |
559 | type Target = PyTzInfo; |
560 | type Output = Bound<'py, Self::Target>; |
561 | type Error = PyErr; |
562 | |
563 | fn into_pyobject(self, py: Python<'py>) -> Result<Self::Output, Self::Error> { |
564 | let seconds_offset: i32 = self.local_minus_utc(); |
565 | #[cfg (not(Py_LIMITED_API))] |
566 | { |
567 | let td = PyDelta::new(py, 0, seconds_offset, 0, true)?; |
568 | timezone_from_offset(&td) |
569 | } |
570 | |
571 | #[cfg (Py_LIMITED_API)] |
572 | { |
573 | let td: Bound<'_, PyAny> = Duration::seconds(seconds_offset.into()).into_pyobject(py)?; |
574 | DatetimeTypes::try_get(py).and_then(|dt: &DatetimeTypes| dt.timezone.bind(py).call1((td,))) |
575 | } |
576 | } |
577 | } |
578 | |
579 | impl<'py> IntoPyObject<'py> for &FixedOffset { |
580 | #[cfg (Py_LIMITED_API)] |
581 | type Target = PyAny; |
582 | #[cfg (not(Py_LIMITED_API))] |
583 | type Target = PyTzInfo; |
584 | type Output = Bound<'py, Self::Target>; |
585 | type Error = PyErr; |
586 | |
587 | #[inline ] |
588 | fn into_pyobject(self, py: Python<'py>) -> Result<Self::Output, Self::Error> { |
589 | (*self).into_pyobject(py) |
590 | } |
591 | } |
592 | |
593 | impl FromPyObject<'_> for FixedOffset { |
594 | /// Convert python tzinfo to rust [`FixedOffset`]. |
595 | /// |
596 | /// Note that the conversion will result in precision lost in microseconds as chrono offset |
597 | /// does not supports microseconds. |
598 | fn extract_bound(ob: &Bound<'_, PyAny>) -> PyResult<FixedOffset> { |
599 | #[cfg (not(Py_LIMITED_API))] |
600 | let ob = ob.downcast::<PyTzInfo>()?; |
601 | #[cfg (Py_LIMITED_API)] |
602 | check_type(ob, &DatetimeTypes::get(ob.py()).tzinfo, "PyTzInfo" )?; |
603 | |
604 | // Passing Python's None to the `utcoffset` function will only |
605 | // work for timezones defined as fixed offsets in Python. |
606 | // Any other timezone would require a datetime as the parameter, and return |
607 | // None if the datetime is not provided. |
608 | // Trying to convert None to a PyDelta in the next line will then fail. |
609 | let py_timedelta = ob.call_method1("utcoffset" , (PyNone::get(ob.py()),))?; |
610 | if py_timedelta.is_none() { |
611 | return Err(PyTypeError::new_err(format!( |
612 | " {:?} is not a fixed offset timezone" , |
613 | ob |
614 | ))); |
615 | } |
616 | let total_seconds: Duration = py_timedelta.extract()?; |
617 | // This cast is safe since the timedelta is limited to -24 hours and 24 hours. |
618 | let total_seconds = total_seconds.num_seconds() as i32; |
619 | FixedOffset::east_opt(total_seconds) |
620 | .ok_or_else(|| PyValueError::new_err("fixed offset out of bounds" )) |
621 | } |
622 | } |
623 | |
624 | #[allow (deprecated)] |
625 | impl ToPyObject for Utc { |
626 | #[inline ] |
627 | fn to_object(&self, py: Python<'_>) -> PyObject { |
628 | self.into_pyobject(py).unwrap().into_any().unbind() |
629 | } |
630 | } |
631 | |
632 | #[allow (deprecated)] |
633 | impl IntoPy<PyObject> for Utc { |
634 | #[inline ] |
635 | fn into_py(self, py: Python<'_>) -> PyObject { |
636 | self.into_pyobject(py).unwrap().into_any().unbind() |
637 | } |
638 | } |
639 | |
640 | impl<'py> IntoPyObject<'py> for Utc { |
641 | #[cfg (Py_LIMITED_API)] |
642 | type Target = PyAny; |
643 | #[cfg (not(Py_LIMITED_API))] |
644 | type Target = PyTzInfo; |
645 | type Output = Bound<'py, Self::Target>; |
646 | type Error = PyErr; |
647 | |
648 | fn into_pyobject(self, py: Python<'py>) -> Result<Self::Output, Self::Error> { |
649 | #[cfg (Py_LIMITED_API)] |
650 | { |
651 | Ok(timezone_utc(py).into_any()) |
652 | } |
653 | #[cfg (not(Py_LIMITED_API))] |
654 | { |
655 | Ok(timezone_utc(py)) |
656 | } |
657 | } |
658 | } |
659 | |
660 | impl<'py> IntoPyObject<'py> for &Utc { |
661 | #[cfg (Py_LIMITED_API)] |
662 | type Target = PyAny; |
663 | #[cfg (not(Py_LIMITED_API))] |
664 | type Target = PyTzInfo; |
665 | type Output = Bound<'py, Self::Target>; |
666 | type Error = PyErr; |
667 | |
668 | #[inline ] |
669 | fn into_pyobject(self, py: Python<'py>) -> Result<Self::Output, Self::Error> { |
670 | (*self).into_pyobject(py) |
671 | } |
672 | } |
673 | |
674 | impl FromPyObject<'_> for Utc { |
675 | fn extract_bound(ob: &Bound<'_, PyAny>) -> PyResult<Utc> { |
676 | let py_utc: Bound<'_, PyAny> = timezone_utc(ob.py()); |
677 | if ob.eq(py_utc)? { |
678 | Ok(Utc) |
679 | } else { |
680 | Err(PyValueError::new_err(args:"expected datetime.timezone.utc" )) |
681 | } |
682 | } |
683 | } |
684 | |
685 | struct DateArgs { |
686 | year: i32, |
687 | month: u8, |
688 | day: u8, |
689 | } |
690 | |
691 | impl From<&NaiveDate> for DateArgs { |
692 | fn from(value: &NaiveDate) -> Self { |
693 | Self { |
694 | year: value.year(), |
695 | month: value.month() as u8, |
696 | day: value.day() as u8, |
697 | } |
698 | } |
699 | } |
700 | |
701 | struct TimeArgs { |
702 | hour: u8, |
703 | min: u8, |
704 | sec: u8, |
705 | micro: u32, |
706 | truncated_leap_second: bool, |
707 | } |
708 | |
709 | impl From<&NaiveTime> for TimeArgs { |
710 | fn from(value: &NaiveTime) -> Self { |
711 | let ns: u32 = value.nanosecond(); |
712 | let checked_sub: Option = ns.checked_sub(1_000_000_000); |
713 | let truncated_leap_second: bool = checked_sub.is_some(); |
714 | let micro: u32 = checked_sub.unwrap_or(default:ns) / 1000; |
715 | Self { |
716 | hour: value.hour() as u8, |
717 | min: value.minute() as u8, |
718 | sec: value.second() as u8, |
719 | micro, |
720 | truncated_leap_second, |
721 | } |
722 | } |
723 | } |
724 | |
725 | fn naive_datetime_to_py_datetime( |
726 | py: Python<'_>, |
727 | naive_datetime: &NaiveDateTime, |
728 | #[cfg (not(Py_LIMITED_API))] tzinfo: Option<&Bound<'_, PyTzInfo>>, |
729 | #[cfg (Py_LIMITED_API)] tzinfo: Option<&Bound<'_, PyAny>>, |
730 | ) -> PyObject { |
731 | let DateArgs { year: i32, month: u8, day: u8 } = (&naive_datetime.date()).into(); |
732 | let TimeArgs { |
733 | hour: u8, |
734 | min: u8, |
735 | sec: u8, |
736 | micro: u32, |
737 | truncated_leap_second: bool, |
738 | } = (&naive_datetime.time()).into(); |
739 | #[cfg (not(Py_LIMITED_API))] |
740 | let datetime = PyDateTime::new(py, year, month, day, hour, min, sec, micro, tzinfo) |
741 | .expect("failed to construct datetime" ); |
742 | #[cfg (Py_LIMITED_API)] |
743 | let datetime: Bound<'_, PyAny> = DatetimeTypes::get(py) |
744 | .datetime |
745 | .bind(py) |
746 | .call1((year, month, day, hour, min, sec, micro, tzinfo)) |
747 | .expect(msg:"failed to construct datetime.datetime" ); |
748 | if truncated_leap_second { |
749 | warn_truncated_leap_second(&datetime); |
750 | } |
751 | datetime.into() |
752 | } |
753 | |
754 | fn warn_truncated_leap_second(obj: &Bound<'_, PyAny>) { |
755 | let py: Python<'_> = obj.py(); |
756 | if let Err(e: PyErr) = PyErr::warn( |
757 | py, |
758 | &py.get_type::<PyUserWarning>(), |
759 | message:ffi::c_str!("ignored leap-second, `datetime` does not support leap-seconds" ), |
760 | stacklevel:0, |
761 | ) { |
762 | e.write_unraisable(py, obj:Some(obj)) |
763 | }; |
764 | } |
765 | |
766 | #[cfg (not(Py_LIMITED_API))] |
767 | fn py_date_to_naive_date(py_date: &impl PyDateAccess) -> PyResult<NaiveDate> { |
768 | NaiveDate::from_ymd_opt( |
769 | py_date.get_year(), |
770 | py_date.get_month().into(), |
771 | py_date.get_day().into(), |
772 | ) |
773 | .ok_or_else(|| PyValueError::new_err("invalid or out-of-range date" )) |
774 | } |
775 | |
776 | #[cfg (Py_LIMITED_API)] |
777 | fn py_date_to_naive_date(py_date: &Bound<'_, PyAny>) -> PyResult<NaiveDate> { |
778 | NaiveDate::from_ymd_opt( |
779 | py_date.getattr(intern!(py_date.py(), "year" ))?.extract()?, |
780 | py_date.getattr(intern!(py_date.py(), "month" ))?.extract()?, |
781 | py_date.getattr(intern!(py_date.py(), "day" ))?.extract()?, |
782 | ) |
783 | .ok_or_else(|| PyValueError::new_err(args:"invalid or out-of-range date" )) |
784 | } |
785 | |
786 | #[cfg (not(Py_LIMITED_API))] |
787 | fn py_time_to_naive_time(py_time: &impl PyTimeAccess) -> PyResult<NaiveTime> { |
788 | NaiveTime::from_hms_micro_opt( |
789 | py_time.get_hour().into(), |
790 | py_time.get_minute().into(), |
791 | py_time.get_second().into(), |
792 | py_time.get_microsecond(), |
793 | ) |
794 | .ok_or_else(|| PyValueError::new_err("invalid or out-of-range time" )) |
795 | } |
796 | |
797 | #[cfg (Py_LIMITED_API)] |
798 | fn py_time_to_naive_time(py_time: &Bound<'_, PyAny>) -> PyResult<NaiveTime> { |
799 | NaiveTime::from_hms_micro_opt( |
800 | py_time.getattr(intern!(py_time.py(), "hour" ))?.extract()?, |
801 | py_time |
802 | .getattr(intern!(py_time.py(), "minute" ))? |
803 | .extract()?, |
804 | py_time |
805 | .getattr(intern!(py_time.py(), "second" ))? |
806 | .extract()?, |
807 | py_time |
808 | .getattr(intern!(py_time.py(), "microsecond" ))? |
809 | .extract()?, |
810 | ) |
811 | .ok_or_else(|| PyValueError::new_err(args:"invalid or out-of-range time" )) |
812 | } |
813 | |
814 | #[cfg (test)] |
815 | mod tests { |
816 | use super::*; |
817 | use crate::{types::PyTuple, BoundObject}; |
818 | use std::{cmp::Ordering, panic}; |
819 | |
820 | #[test ] |
821 | // Only Python>=3.9 has the zoneinfo package |
822 | // We skip the test on windows too since we'd need to install |
823 | // tzdata there to make this work. |
824 | #[cfg (all(Py_3_9, not(target_os = "windows" )))] |
825 | fn test_zoneinfo_is_not_fixed_offset() { |
826 | use crate::ffi; |
827 | use crate::types::any::PyAnyMethods; |
828 | use crate::types::dict::PyDictMethods; |
829 | |
830 | Python::with_gil(|py| { |
831 | let locals = crate::types::PyDict::new(py); |
832 | py.run( |
833 | ffi::c_str!("import zoneinfo; zi = zoneinfo.ZoneInfo('Europe/London')" ), |
834 | None, |
835 | Some(&locals), |
836 | ) |
837 | .unwrap(); |
838 | let result: PyResult<FixedOffset> = locals.get_item("zi" ).unwrap().unwrap().extract(); |
839 | assert!(result.is_err()); |
840 | let res = result.err().unwrap(); |
841 | // Also check the error message is what we expect |
842 | let msg = res.value(py).repr().unwrap().to_string(); |
843 | assert_eq!(msg, "TypeError( \"zoneinfo.ZoneInfo(key='Europe/London') is not a fixed offset timezone \")" ); |
844 | }); |
845 | } |
846 | |
847 | #[test ] |
848 | fn test_timezone_aware_to_naive_fails() { |
849 | // Test that if a user tries to convert a python's timezone aware datetime into a naive |
850 | // one, the conversion fails. |
851 | Python::with_gil(|py| { |
852 | let py_datetime = |
853 | new_py_datetime_ob(py, "datetime" , (2022, 1, 1, 1, 0, 0, 0, python_utc(py))); |
854 | // Now test that converting a PyDateTime with tzinfo to a NaiveDateTime fails |
855 | let res: PyResult<NaiveDateTime> = py_datetime.extract(); |
856 | assert_eq!( |
857 | res.unwrap_err().value(py).repr().unwrap().to_string(), |
858 | "TypeError('expected a datetime without tzinfo')" |
859 | ); |
860 | }); |
861 | } |
862 | |
863 | #[test ] |
864 | fn test_naive_to_timezone_aware_fails() { |
865 | // Test that if a user tries to convert a python's timezone aware datetime into a naive |
866 | // one, the conversion fails. |
867 | Python::with_gil(|py| { |
868 | let py_datetime = new_py_datetime_ob(py, "datetime" , (2022, 1, 1, 1, 0, 0, 0)); |
869 | // Now test that converting a PyDateTime with tzinfo to a NaiveDateTime fails |
870 | let res: PyResult<DateTime<Utc>> = py_datetime.extract(); |
871 | assert_eq!( |
872 | res.unwrap_err().value(py).repr().unwrap().to_string(), |
873 | "TypeError('expected a datetime with non-None tzinfo')" |
874 | ); |
875 | |
876 | // Now test that converting a PyDateTime with tzinfo to a NaiveDateTime fails |
877 | let res: PyResult<DateTime<FixedOffset>> = py_datetime.extract(); |
878 | assert_eq!( |
879 | res.unwrap_err().value(py).repr().unwrap().to_string(), |
880 | "TypeError('expected a datetime with non-None tzinfo')" |
881 | ); |
882 | }); |
883 | } |
884 | |
885 | #[test ] |
886 | fn test_invalid_types_fail() { |
887 | // Test that if a user tries to convert a python's timezone aware datetime into a naive |
888 | // one, the conversion fails. |
889 | Python::with_gil(|py| { |
890 | let none = py.None().into_bound(py); |
891 | assert_eq!( |
892 | none.extract::<Duration>().unwrap_err().to_string(), |
893 | "TypeError: 'NoneType' object cannot be converted to 'PyDelta'" |
894 | ); |
895 | assert_eq!( |
896 | none.extract::<FixedOffset>().unwrap_err().to_string(), |
897 | "TypeError: 'NoneType' object cannot be converted to 'PyTzInfo'" |
898 | ); |
899 | assert_eq!( |
900 | none.extract::<Utc>().unwrap_err().to_string(), |
901 | "ValueError: expected datetime.timezone.utc" |
902 | ); |
903 | assert_eq!( |
904 | none.extract::<NaiveTime>().unwrap_err().to_string(), |
905 | "TypeError: 'NoneType' object cannot be converted to 'PyTime'" |
906 | ); |
907 | assert_eq!( |
908 | none.extract::<NaiveDate>().unwrap_err().to_string(), |
909 | "TypeError: 'NoneType' object cannot be converted to 'PyDate'" |
910 | ); |
911 | assert_eq!( |
912 | none.extract::<NaiveDateTime>().unwrap_err().to_string(), |
913 | "TypeError: 'NoneType' object cannot be converted to 'PyDateTime'" |
914 | ); |
915 | assert_eq!( |
916 | none.extract::<DateTime<Utc>>().unwrap_err().to_string(), |
917 | "TypeError: 'NoneType' object cannot be converted to 'PyDateTime'" |
918 | ); |
919 | assert_eq!( |
920 | none.extract::<DateTime<FixedOffset>>() |
921 | .unwrap_err() |
922 | .to_string(), |
923 | "TypeError: 'NoneType' object cannot be converted to 'PyDateTime'" |
924 | ); |
925 | }); |
926 | } |
927 | |
928 | #[test ] |
929 | fn test_pyo3_timedelta_into_pyobject() { |
930 | // Utility function used to check different durations. |
931 | // The `name` parameter is used to identify the check in case of a failure. |
932 | let check = |name: &'static str, delta: Duration, py_days, py_seconds, py_ms| { |
933 | Python::with_gil(|py| { |
934 | let delta = delta.into_pyobject(py).unwrap(); |
935 | let py_delta = new_py_datetime_ob(py, "timedelta" , (py_days, py_seconds, py_ms)); |
936 | assert!( |
937 | delta.eq(&py_delta).unwrap(), |
938 | "{}: {} != {}" , |
939 | name, |
940 | delta, |
941 | py_delta |
942 | ); |
943 | }); |
944 | }; |
945 | |
946 | let delta = Duration::days(-1) + Duration::seconds(1) + Duration::microseconds(-10); |
947 | check("delta normalization" , delta, -1, 1, -10); |
948 | |
949 | // Check the minimum value allowed by PyDelta, which is different |
950 | // from the minimum value allowed in Duration. This should pass. |
951 | let delta = Duration::seconds(-86399999913600); // min |
952 | check("delta min value" , delta, -999999999, 0, 0); |
953 | |
954 | // Same, for max value |
955 | let delta = Duration::seconds(86399999999999) + Duration::nanoseconds(999999000); // max |
956 | check("delta max value" , delta, 999999999, 86399, 999999); |
957 | |
958 | // Also check that trying to convert an out of bound value errors. |
959 | Python::with_gil(|py| { |
960 | // min_value and max_value were deprecated in chrono 0.4.39 |
961 | #[allow (deprecated)] |
962 | { |
963 | assert!(Duration::min_value().into_pyobject(py).is_err()); |
964 | assert!(Duration::max_value().into_pyobject(py).is_err()); |
965 | } |
966 | }); |
967 | } |
968 | |
969 | #[test ] |
970 | fn test_pyo3_timedelta_frompyobject() { |
971 | // Utility function used to check different durations. |
972 | // The `name` parameter is used to identify the check in case of a failure. |
973 | let check = |name: &'static str, delta: Duration, py_days, py_seconds, py_ms| { |
974 | Python::with_gil(|py| { |
975 | let py_delta = new_py_datetime_ob(py, "timedelta" , (py_days, py_seconds, py_ms)); |
976 | let py_delta: Duration = py_delta.extract().unwrap(); |
977 | assert_eq!(py_delta, delta, "{}: {} != {}" , name, py_delta, delta); |
978 | }) |
979 | }; |
980 | |
981 | // Check the minimum value allowed by PyDelta, which is different |
982 | // from the minimum value allowed in Duration. This should pass. |
983 | check( |
984 | "min py_delta value" , |
985 | Duration::seconds(-86399999913600), |
986 | -999999999, |
987 | 0, |
988 | 0, |
989 | ); |
990 | // Same, for max value |
991 | check( |
992 | "max py_delta value" , |
993 | Duration::seconds(86399999999999) + Duration::microseconds(999999), |
994 | 999999999, |
995 | 86399, |
996 | 999999, |
997 | ); |
998 | |
999 | // This check is to assert that we can't construct every possible Duration from a PyDelta |
1000 | // since they have different bounds. |
1001 | Python::with_gil(|py| { |
1002 | let low_days: i32 = -1000000000; |
1003 | // This is possible |
1004 | assert!(panic::catch_unwind(|| Duration::days(low_days as i64)).is_ok()); |
1005 | // This panics on PyDelta::new |
1006 | assert!(panic::catch_unwind(|| { |
1007 | let py_delta = new_py_datetime_ob(py, "timedelta" , (low_days, 0, 0)); |
1008 | if let Ok(_duration) = py_delta.extract::<Duration>() { |
1009 | // So we should never get here |
1010 | } |
1011 | }) |
1012 | .is_err()); |
1013 | |
1014 | let high_days: i32 = 1000000000; |
1015 | // This is possible |
1016 | assert!(panic::catch_unwind(|| Duration::days(high_days as i64)).is_ok()); |
1017 | // This panics on PyDelta::new |
1018 | assert!(panic::catch_unwind(|| { |
1019 | let py_delta = new_py_datetime_ob(py, "timedelta" , (high_days, 0, 0)); |
1020 | if let Ok(_duration) = py_delta.extract::<Duration>() { |
1021 | // So we should never get here |
1022 | } |
1023 | }) |
1024 | .is_err()); |
1025 | }); |
1026 | } |
1027 | |
1028 | #[test ] |
1029 | fn test_pyo3_date_into_pyobject() { |
1030 | let eq_ymd = |name: &'static str, year, month, day| { |
1031 | Python::with_gil(|py| { |
1032 | let date = NaiveDate::from_ymd_opt(year, month, day) |
1033 | .unwrap() |
1034 | .into_pyobject(py) |
1035 | .unwrap(); |
1036 | let py_date = new_py_datetime_ob(py, "date" , (year, month, day)); |
1037 | assert_eq!( |
1038 | date.compare(&py_date).unwrap(), |
1039 | Ordering::Equal, |
1040 | "{}: {} != {}" , |
1041 | name, |
1042 | date, |
1043 | py_date |
1044 | ); |
1045 | }) |
1046 | }; |
1047 | |
1048 | eq_ymd("past date" , 2012, 2, 29); |
1049 | eq_ymd("min date" , 1, 1, 1); |
1050 | eq_ymd("future date" , 3000, 6, 5); |
1051 | eq_ymd("max date" , 9999, 12, 31); |
1052 | } |
1053 | |
1054 | #[test ] |
1055 | fn test_pyo3_date_frompyobject() { |
1056 | let eq_ymd = |name: &'static str, year, month, day| { |
1057 | Python::with_gil(|py| { |
1058 | let py_date = new_py_datetime_ob(py, "date" , (year, month, day)); |
1059 | let py_date: NaiveDate = py_date.extract().unwrap(); |
1060 | let date = NaiveDate::from_ymd_opt(year, month, day).unwrap(); |
1061 | assert_eq!(py_date, date, "{}: {} != {}" , name, date, py_date); |
1062 | }) |
1063 | }; |
1064 | |
1065 | eq_ymd("past date" , 2012, 2, 29); |
1066 | eq_ymd("min date" , 1, 1, 1); |
1067 | eq_ymd("future date" , 3000, 6, 5); |
1068 | eq_ymd("max date" , 9999, 12, 31); |
1069 | } |
1070 | |
1071 | #[test ] |
1072 | fn test_pyo3_datetime_into_pyobject_utc() { |
1073 | Python::with_gil(|py| { |
1074 | let check_utc = |
1075 | |name: &'static str, year, month, day, hour, minute, second, ms, py_ms| { |
1076 | let datetime = NaiveDate::from_ymd_opt(year, month, day) |
1077 | .unwrap() |
1078 | .and_hms_micro_opt(hour, minute, second, ms) |
1079 | .unwrap() |
1080 | .and_utc(); |
1081 | let datetime = datetime.into_pyobject(py).unwrap(); |
1082 | let py_datetime = new_py_datetime_ob( |
1083 | py, |
1084 | "datetime" , |
1085 | ( |
1086 | year, |
1087 | month, |
1088 | day, |
1089 | hour, |
1090 | minute, |
1091 | second, |
1092 | py_ms, |
1093 | python_utc(py), |
1094 | ), |
1095 | ); |
1096 | assert_eq!( |
1097 | datetime.compare(&py_datetime).unwrap(), |
1098 | Ordering::Equal, |
1099 | "{}: {} != {}" , |
1100 | name, |
1101 | datetime, |
1102 | py_datetime |
1103 | ); |
1104 | }; |
1105 | |
1106 | check_utc("regular" , 2014, 5, 6, 7, 8, 9, 999_999, 999_999); |
1107 | |
1108 | #[cfg (not(Py_GIL_DISABLED))] |
1109 | assert_warnings!( |
1110 | py, |
1111 | check_utc("leap second" , 2014, 5, 6, 7, 8, 59, 1_999_999, 999_999), |
1112 | [( |
1113 | PyUserWarning, |
1114 | "ignored leap-second, `datetime` does not support leap-seconds" |
1115 | )] |
1116 | ); |
1117 | }) |
1118 | } |
1119 | |
1120 | #[test ] |
1121 | fn test_pyo3_datetime_into_pyobject_fixed_offset() { |
1122 | Python::with_gil(|py| { |
1123 | let check_fixed_offset = |
1124 | |name: &'static str, year, month, day, hour, minute, second, ms, py_ms| { |
1125 | let offset = FixedOffset::east_opt(3600).unwrap(); |
1126 | let datetime = NaiveDate::from_ymd_opt(year, month, day) |
1127 | .unwrap() |
1128 | .and_hms_micro_opt(hour, minute, second, ms) |
1129 | .unwrap() |
1130 | .and_local_timezone(offset) |
1131 | .unwrap(); |
1132 | let datetime = datetime.into_pyobject(py).unwrap(); |
1133 | let py_tz = offset.into_pyobject(py).unwrap(); |
1134 | let py_datetime = new_py_datetime_ob( |
1135 | py, |
1136 | "datetime" , |
1137 | (year, month, day, hour, minute, second, py_ms, py_tz), |
1138 | ); |
1139 | assert_eq!( |
1140 | datetime.compare(&py_datetime).unwrap(), |
1141 | Ordering::Equal, |
1142 | "{}: {} != {}" , |
1143 | name, |
1144 | datetime, |
1145 | py_datetime |
1146 | ); |
1147 | }; |
1148 | |
1149 | check_fixed_offset("regular" , 2014, 5, 6, 7, 8, 9, 999_999, 999_999); |
1150 | |
1151 | #[cfg (not(Py_GIL_DISABLED))] |
1152 | assert_warnings!( |
1153 | py, |
1154 | check_fixed_offset("leap second" , 2014, 5, 6, 7, 8, 59, 1_999_999, 999_999), |
1155 | [( |
1156 | PyUserWarning, |
1157 | "ignored leap-second, `datetime` does not support leap-seconds" |
1158 | )] |
1159 | ); |
1160 | }) |
1161 | } |
1162 | |
1163 | #[test ] |
1164 | #[cfg (all(Py_3_9, feature = "chrono-tz" , not(windows)))] |
1165 | fn test_pyo3_datetime_into_pyobject_tz() { |
1166 | Python::with_gil(|py| { |
1167 | let datetime = NaiveDate::from_ymd_opt(2024, 12, 11) |
1168 | .unwrap() |
1169 | .and_hms_opt(23, 3, 13) |
1170 | .unwrap() |
1171 | .and_local_timezone(chrono_tz::Tz::Europe__London) |
1172 | .unwrap(); |
1173 | let datetime = datetime.into_pyobject(py).unwrap(); |
1174 | let py_datetime = new_py_datetime_ob( |
1175 | py, |
1176 | "datetime" , |
1177 | ( |
1178 | 2024, |
1179 | 12, |
1180 | 11, |
1181 | 23, |
1182 | 3, |
1183 | 13, |
1184 | 0, |
1185 | python_zoneinfo(py, "Europe/London" ), |
1186 | ), |
1187 | ); |
1188 | assert_eq!(datetime.compare(&py_datetime).unwrap(), Ordering::Equal); |
1189 | }) |
1190 | } |
1191 | |
1192 | #[test ] |
1193 | fn test_pyo3_datetime_frompyobject_utc() { |
1194 | Python::with_gil(|py| { |
1195 | let year = 2014; |
1196 | let month = 5; |
1197 | let day = 6; |
1198 | let hour = 7; |
1199 | let minute = 8; |
1200 | let second = 9; |
1201 | let micro = 999_999; |
1202 | let tz_utc = timezone_utc(py); |
1203 | let py_datetime = new_py_datetime_ob( |
1204 | py, |
1205 | "datetime" , |
1206 | (year, month, day, hour, minute, second, micro, tz_utc), |
1207 | ); |
1208 | let py_datetime: DateTime<Utc> = py_datetime.extract().unwrap(); |
1209 | let datetime = NaiveDate::from_ymd_opt(year, month, day) |
1210 | .unwrap() |
1211 | .and_hms_micro_opt(hour, minute, second, micro) |
1212 | .unwrap() |
1213 | .and_utc(); |
1214 | assert_eq!(py_datetime, datetime,); |
1215 | }) |
1216 | } |
1217 | |
1218 | #[test ] |
1219 | fn test_pyo3_datetime_frompyobject_fixed_offset() { |
1220 | Python::with_gil(|py| { |
1221 | let year = 2014; |
1222 | let month = 5; |
1223 | let day = 6; |
1224 | let hour = 7; |
1225 | let minute = 8; |
1226 | let second = 9; |
1227 | let micro = 999_999; |
1228 | let offset = FixedOffset::east_opt(3600).unwrap(); |
1229 | let py_tz = offset.into_pyobject(py).unwrap(); |
1230 | let py_datetime = new_py_datetime_ob( |
1231 | py, |
1232 | "datetime" , |
1233 | (year, month, day, hour, minute, second, micro, py_tz), |
1234 | ); |
1235 | let datetime_from_py: DateTime<FixedOffset> = py_datetime.extract().unwrap(); |
1236 | let datetime = NaiveDate::from_ymd_opt(year, month, day) |
1237 | .unwrap() |
1238 | .and_hms_micro_opt(hour, minute, second, micro) |
1239 | .unwrap(); |
1240 | let datetime = datetime.and_local_timezone(offset).unwrap(); |
1241 | |
1242 | assert_eq!(datetime_from_py, datetime); |
1243 | assert!( |
1244 | py_datetime.extract::<DateTime<Utc>>().is_err(), |
1245 | "Extracting Utc from nonzero FixedOffset timezone will fail" |
1246 | ); |
1247 | |
1248 | let utc = python_utc(py); |
1249 | let py_datetime_utc = new_py_datetime_ob( |
1250 | py, |
1251 | "datetime" , |
1252 | (year, month, day, hour, minute, second, micro, utc), |
1253 | ); |
1254 | assert!( |
1255 | py_datetime_utc.extract::<DateTime<FixedOffset>>().is_ok(), |
1256 | "Extracting FixedOffset from Utc timezone will succeed" |
1257 | ); |
1258 | }) |
1259 | } |
1260 | |
1261 | #[test ] |
1262 | fn test_pyo3_offset_fixed_into_pyobject() { |
1263 | Python::with_gil(|py| { |
1264 | // Chrono offset |
1265 | let offset = FixedOffset::east_opt(3600) |
1266 | .unwrap() |
1267 | .into_pyobject(py) |
1268 | .unwrap(); |
1269 | // Python timezone from timedelta |
1270 | let td = new_py_datetime_ob(py, "timedelta" , (0, 3600, 0)); |
1271 | let py_timedelta = new_py_datetime_ob(py, "timezone" , (td,)); |
1272 | // Should be equal |
1273 | assert!(offset.eq(py_timedelta).unwrap()); |
1274 | |
1275 | // Same but with negative values |
1276 | let offset = FixedOffset::east_opt(-3600) |
1277 | .unwrap() |
1278 | .into_pyobject(py) |
1279 | .unwrap(); |
1280 | let td = new_py_datetime_ob(py, "timedelta" , (0, -3600, 0)); |
1281 | let py_timedelta = new_py_datetime_ob(py, "timezone" , (td,)); |
1282 | assert!(offset.eq(py_timedelta).unwrap()); |
1283 | }) |
1284 | } |
1285 | |
1286 | #[test ] |
1287 | fn test_pyo3_offset_fixed_frompyobject() { |
1288 | Python::with_gil(|py| { |
1289 | let py_timedelta = new_py_datetime_ob(py, "timedelta" , (0, 3600, 0)); |
1290 | let py_tzinfo = new_py_datetime_ob(py, "timezone" , (py_timedelta,)); |
1291 | let offset: FixedOffset = py_tzinfo.extract().unwrap(); |
1292 | assert_eq!(FixedOffset::east_opt(3600).unwrap(), offset); |
1293 | }) |
1294 | } |
1295 | |
1296 | #[test ] |
1297 | fn test_pyo3_offset_utc_into_pyobject() { |
1298 | Python::with_gil(|py| { |
1299 | let utc = Utc.into_pyobject(py).unwrap(); |
1300 | let py_utc = python_utc(py); |
1301 | assert!(utc.is(&py_utc)); |
1302 | }) |
1303 | } |
1304 | |
1305 | #[test ] |
1306 | fn test_pyo3_offset_utc_frompyobject() { |
1307 | Python::with_gil(|py| { |
1308 | let py_utc = python_utc(py); |
1309 | let py_utc: Utc = py_utc.extract().unwrap(); |
1310 | assert_eq!(Utc, py_utc); |
1311 | |
1312 | let py_timedelta = new_py_datetime_ob(py, "timedelta" , (0, 0, 0)); |
1313 | let py_timezone_utc = new_py_datetime_ob(py, "timezone" , (py_timedelta,)); |
1314 | let py_timezone_utc: Utc = py_timezone_utc.extract().unwrap(); |
1315 | assert_eq!(Utc, py_timezone_utc); |
1316 | |
1317 | let py_timedelta = new_py_datetime_ob(py, "timedelta" , (0, 3600, 0)); |
1318 | let py_timezone = new_py_datetime_ob(py, "timezone" , (py_timedelta,)); |
1319 | assert!(py_timezone.extract::<Utc>().is_err()); |
1320 | }) |
1321 | } |
1322 | |
1323 | #[test ] |
1324 | fn test_pyo3_time_into_pyobject() { |
1325 | Python::with_gil(|py| { |
1326 | let check_time = |name: &'static str, hour, minute, second, ms, py_ms| { |
1327 | let time = NaiveTime::from_hms_micro_opt(hour, minute, second, ms) |
1328 | .unwrap() |
1329 | .into_pyobject(py) |
1330 | .unwrap(); |
1331 | let py_time = new_py_datetime_ob(py, "time" , (hour, minute, second, py_ms)); |
1332 | assert!( |
1333 | time.eq(&py_time).unwrap(), |
1334 | "{}: {} != {}" , |
1335 | name, |
1336 | time, |
1337 | py_time |
1338 | ); |
1339 | }; |
1340 | |
1341 | check_time("regular" , 3, 5, 7, 999_999, 999_999); |
1342 | |
1343 | #[cfg (not(Py_GIL_DISABLED))] |
1344 | assert_warnings!( |
1345 | py, |
1346 | check_time("leap second" , 3, 5, 59, 1_999_999, 999_999), |
1347 | [( |
1348 | PyUserWarning, |
1349 | "ignored leap-second, `datetime` does not support leap-seconds" |
1350 | )] |
1351 | ); |
1352 | }) |
1353 | } |
1354 | |
1355 | #[test ] |
1356 | fn test_pyo3_time_frompyobject() { |
1357 | let hour = 3; |
1358 | let minute = 5; |
1359 | let second = 7; |
1360 | let micro = 999_999; |
1361 | Python::with_gil(|py| { |
1362 | let py_time = new_py_datetime_ob(py, "time" , (hour, minute, second, micro)); |
1363 | let py_time: NaiveTime = py_time.extract().unwrap(); |
1364 | let time = NaiveTime::from_hms_micro_opt(hour, minute, second, micro).unwrap(); |
1365 | assert_eq!(py_time, time); |
1366 | }) |
1367 | } |
1368 | |
1369 | fn new_py_datetime_ob<'py, A>(py: Python<'py>, name: &str, args: A) -> Bound<'py, PyAny> |
1370 | where |
1371 | A: IntoPyObject<'py, Target = PyTuple>, |
1372 | { |
1373 | py.import("datetime" ) |
1374 | .unwrap() |
1375 | .getattr(name) |
1376 | .unwrap() |
1377 | .call1( |
1378 | args.into_pyobject(py) |
1379 | .map_err(Into::into) |
1380 | .unwrap() |
1381 | .into_bound(), |
1382 | ) |
1383 | .unwrap() |
1384 | } |
1385 | |
1386 | fn python_utc(py: Python<'_>) -> Bound<'_, PyAny> { |
1387 | py.import("datetime" ) |
1388 | .unwrap() |
1389 | .getattr("timezone" ) |
1390 | .unwrap() |
1391 | .getattr("utc" ) |
1392 | .unwrap() |
1393 | } |
1394 | |
1395 | #[cfg (all(Py_3_9, feature = "chrono-tz" , not(windows)))] |
1396 | fn python_zoneinfo<'py>(py: Python<'py>, timezone: &str) -> Bound<'py, PyAny> { |
1397 | py.import("zoneinfo" ) |
1398 | .unwrap() |
1399 | .getattr("ZoneInfo" ) |
1400 | .unwrap() |
1401 | .call1((timezone,)) |
1402 | .unwrap() |
1403 | } |
1404 | |
1405 | #[cfg (not(any(target_arch = "wasm32" , Py_GIL_DISABLED)))] |
1406 | mod proptests { |
1407 | use super::*; |
1408 | use crate::tests::common::CatchWarnings; |
1409 | use crate::types::IntoPyDict; |
1410 | use proptest::prelude::*; |
1411 | use std::ffi::CString; |
1412 | |
1413 | proptest! { |
1414 | |
1415 | // Range is limited to 1970 to 2038 due to windows limitations |
1416 | #[test] |
1417 | fn test_pyo3_offset_fixed_frompyobject_created_in_python(timestamp in 0..(i32::MAX as i64), timedelta in -86399i32..=86399i32) { |
1418 | Python::with_gil(|py| { |
1419 | |
1420 | let globals = [("datetime" , py.import("datetime" ).unwrap())].into_py_dict(py).unwrap(); |
1421 | let code = format!("datetime.datetime.fromtimestamp({}).replace(tzinfo=datetime.timezone(datetime.timedelta(seconds={})))" , timestamp, timedelta); |
1422 | let t = py.eval(&CString::new(code).unwrap(), Some(&globals), None).unwrap(); |
1423 | |
1424 | // Get ISO 8601 string from python |
1425 | let py_iso_str = t.call_method0("isoformat" ).unwrap(); |
1426 | |
1427 | // Get ISO 8601 string from rust |
1428 | let t = t.extract::<DateTime<FixedOffset>>().unwrap(); |
1429 | // Python doesn't print the seconds of the offset if they are 0 |
1430 | let rust_iso_str = if timedelta % 60 == 0 { |
1431 | t.format("%Y-%m-%dT%H:%M:%S%:z" ).to_string() |
1432 | } else { |
1433 | t.format("%Y-%m-%dT%H:%M:%S%::z" ).to_string() |
1434 | }; |
1435 | |
1436 | // They should be equal |
1437 | assert_eq!(py_iso_str.to_string(), rust_iso_str); |
1438 | }) |
1439 | } |
1440 | |
1441 | #[test] |
1442 | fn test_duration_roundtrip(days in -999999999i64..=999999999i64) { |
1443 | // Test roundtrip conversion rust->python->rust for all allowed |
1444 | // python values of durations (from -999999999 to 999999999 days), |
1445 | Python::with_gil(|py| { |
1446 | let dur = Duration::days(days); |
1447 | let py_delta = dur.into_pyobject(py).unwrap(); |
1448 | let roundtripped: Duration = py_delta.extract().expect("Round trip" ); |
1449 | assert_eq!(dur, roundtripped); |
1450 | }) |
1451 | } |
1452 | |
1453 | #[test] |
1454 | fn test_fixed_offset_roundtrip(secs in -86399i32..=86399i32) { |
1455 | Python::with_gil(|py| { |
1456 | let offset = FixedOffset::east_opt(secs).unwrap(); |
1457 | let py_offset = offset.into_pyobject(py).unwrap(); |
1458 | let roundtripped: FixedOffset = py_offset.extract().expect("Round trip" ); |
1459 | assert_eq!(offset, roundtripped); |
1460 | }) |
1461 | } |
1462 | |
1463 | #[test] |
1464 | fn test_naive_date_roundtrip( |
1465 | year in 1i32..=9999i32, |
1466 | month in 1u32..=12u32, |
1467 | day in 1u32..=31u32 |
1468 | ) { |
1469 | // Test roundtrip conversion rust->python->rust for all allowed |
1470 | // python dates (from year 1 to year 9999) |
1471 | Python::with_gil(|py| { |
1472 | // We use to `from_ymd_opt` constructor so that we only test valid `NaiveDate`s. |
1473 | // This is to skip the test if we are creating an invalid date, like February 31. |
1474 | if let Some(date) = NaiveDate::from_ymd_opt(year, month, day) { |
1475 | let py_date = date.into_pyobject(py).unwrap(); |
1476 | let roundtripped: NaiveDate = py_date.extract().expect("Round trip" ); |
1477 | assert_eq!(date, roundtripped); |
1478 | } |
1479 | }) |
1480 | } |
1481 | |
1482 | #[test] |
1483 | fn test_naive_time_roundtrip( |
1484 | hour in 0u32..=23u32, |
1485 | min in 0u32..=59u32, |
1486 | sec in 0u32..=59u32, |
1487 | micro in 0u32..=1_999_999u32 |
1488 | ) { |
1489 | // Test roundtrip conversion rust->python->rust for naive times. |
1490 | // Python time has a resolution of microseconds, so we only test |
1491 | // NaiveTimes with microseconds resolution, even if NaiveTime has nanosecond |
1492 | // resolution. |
1493 | Python::with_gil(|py| { |
1494 | if let Some(time) = NaiveTime::from_hms_micro_opt(hour, min, sec, micro) { |
1495 | // Wrap in CatchWarnings to avoid to_object firing warning for truncated leap second |
1496 | let py_time = CatchWarnings::enter(py, |_| time.into_pyobject(py)).unwrap(); |
1497 | let roundtripped: NaiveTime = py_time.extract().expect("Round trip" ); |
1498 | // Leap seconds are not roundtripped |
1499 | let expected_roundtrip_time = micro.checked_sub(1_000_000).map(|micro| NaiveTime::from_hms_micro_opt(hour, min, sec, micro).unwrap()).unwrap_or(time); |
1500 | assert_eq!(expected_roundtrip_time, roundtripped); |
1501 | } |
1502 | }) |
1503 | } |
1504 | |
1505 | #[test] |
1506 | fn test_naive_datetime_roundtrip( |
1507 | year in 1i32..=9999i32, |
1508 | month in 1u32..=12u32, |
1509 | day in 1u32..=31u32, |
1510 | hour in 0u32..=24u32, |
1511 | min in 0u32..=60u32, |
1512 | sec in 0u32..=60u32, |
1513 | micro in 0u32..=999_999u32 |
1514 | ) { |
1515 | Python::with_gil(|py| { |
1516 | let date_opt = NaiveDate::from_ymd_opt(year, month, day); |
1517 | let time_opt = NaiveTime::from_hms_micro_opt(hour, min, sec, micro); |
1518 | if let (Some(date), Some(time)) = (date_opt, time_opt) { |
1519 | let dt = NaiveDateTime::new(date, time); |
1520 | let pydt = dt.into_pyobject(py).unwrap(); |
1521 | let roundtripped: NaiveDateTime = pydt.extract().expect("Round trip" ); |
1522 | assert_eq!(dt, roundtripped); |
1523 | } |
1524 | }) |
1525 | } |
1526 | |
1527 | #[test] |
1528 | fn test_utc_datetime_roundtrip( |
1529 | year in 1i32..=9999i32, |
1530 | month in 1u32..=12u32, |
1531 | day in 1u32..=31u32, |
1532 | hour in 0u32..=23u32, |
1533 | min in 0u32..=59u32, |
1534 | sec in 0u32..=59u32, |
1535 | micro in 0u32..=1_999_999u32 |
1536 | ) { |
1537 | Python::with_gil(|py| { |
1538 | let date_opt = NaiveDate::from_ymd_opt(year, month, day); |
1539 | let time_opt = NaiveTime::from_hms_micro_opt(hour, min, sec, micro); |
1540 | if let (Some(date), Some(time)) = (date_opt, time_opt) { |
1541 | let dt: DateTime<Utc> = NaiveDateTime::new(date, time).and_utc(); |
1542 | // Wrap in CatchWarnings to avoid into_py firing warning for truncated leap second |
1543 | let py_dt = CatchWarnings::enter(py, |_| dt.into_pyobject(py)).unwrap(); |
1544 | let roundtripped: DateTime<Utc> = py_dt.extract().expect("Round trip" ); |
1545 | // Leap seconds are not roundtripped |
1546 | let expected_roundtrip_time = micro.checked_sub(1_000_000).map(|micro| NaiveTime::from_hms_micro_opt(hour, min, sec, micro).unwrap()).unwrap_or(time); |
1547 | let expected_roundtrip_dt: DateTime<Utc> = NaiveDateTime::new(date, expected_roundtrip_time).and_utc(); |
1548 | assert_eq!(expected_roundtrip_dt, roundtripped); |
1549 | } |
1550 | }) |
1551 | } |
1552 | |
1553 | #[test] |
1554 | fn test_fixed_offset_datetime_roundtrip( |
1555 | year in 1i32..=9999i32, |
1556 | month in 1u32..=12u32, |
1557 | day in 1u32..=31u32, |
1558 | hour in 0u32..=23u32, |
1559 | min in 0u32..=59u32, |
1560 | sec in 0u32..=59u32, |
1561 | micro in 0u32..=1_999_999u32, |
1562 | offset_secs in -86399i32..=86399i32 |
1563 | ) { |
1564 | Python::with_gil(|py| { |
1565 | let date_opt = NaiveDate::from_ymd_opt(year, month, day); |
1566 | let time_opt = NaiveTime::from_hms_micro_opt(hour, min, sec, micro); |
1567 | let offset = FixedOffset::east_opt(offset_secs).unwrap(); |
1568 | if let (Some(date), Some(time)) = (date_opt, time_opt) { |
1569 | let dt: DateTime<FixedOffset> = NaiveDateTime::new(date, time).and_local_timezone(offset).unwrap(); |
1570 | // Wrap in CatchWarnings to avoid into_py firing warning for truncated leap second |
1571 | let py_dt = CatchWarnings::enter(py, |_| dt.into_pyobject(py)).unwrap(); |
1572 | let roundtripped: DateTime<FixedOffset> = py_dt.extract().expect("Round trip" ); |
1573 | // Leap seconds are not roundtripped |
1574 | let expected_roundtrip_time = micro.checked_sub(1_000_000).map(|micro| NaiveTime::from_hms_micro_opt(hour, min, sec, micro).unwrap()).unwrap_or(time); |
1575 | let expected_roundtrip_dt: DateTime<FixedOffset> = NaiveDateTime::new(date, expected_roundtrip_time).and_local_timezone(offset).unwrap(); |
1576 | assert_eq!(expected_roundtrip_dt, roundtripped); |
1577 | } |
1578 | }) |
1579 | } |
1580 | } |
1581 | } |
1582 | } |
1583 | |