1#![cfg(feature = "chrono")]
2
3#![doc = concat!("pyo3 = { version = \"", env!("CARGO_PKG_VERSION"), "\", features = [\"chrono\"] }")]
14use crate::conversion::IntoPyObject;
45use crate::exceptions::{PyTypeError, PyUserWarning, PyValueError};
46use crate::intern;
47use crate::types::any::PyAnyMethods;
48use crate::types::PyNone;
49use crate::types::{PyDate, PyDateTime, PyDelta, PyTime, PyTzInfo, PyTzInfoAccess};
50#[cfg(not(Py_LIMITED_API))]
51use crate::types::{PyDateAccess, PyDeltaAccess, PyTimeAccess};
52#[cfg(feature = "chrono-local")]
53use crate::{
54 exceptions::PyRuntimeError,
55 sync::GILOnceCell,
56 types::{PyString, PyStringMethods},
57 Py,
58};
59use crate::{ffi, Borrowed, Bound, FromPyObject, IntoPyObjectExt, PyAny, PyErr, PyResult, Python};
60use chrono::offset::{FixedOffset, Utc};
61#[cfg(feature = "chrono-local")]
62use chrono::Local;
63use chrono::{
64 DateTime, Datelike, Duration, LocalResult, NaiveDate, NaiveDateTime, NaiveTime, Offset,
65 TimeZone, Timelike,
66};
67
68impl<'py> IntoPyObject<'py> for Duration {
69 type Target = PyDelta;
70 type Output = Bound<'py, Self::Target>;
71 type Error = PyErr;
72
73 fn into_pyobject(self, py: Python<'py>) -> Result<Self::Output, Self::Error> {
74 let days = self.num_days();
76 let secs_dur = self - Duration::days(days);
78 let secs = secs_dur.num_seconds();
79 let micros = (secs_dur - Duration::seconds(secs_dur.num_seconds()))
81 .num_microseconds()
82 .unwrap();
85 PyDelta::new(
91 py,
92 days.try_into().unwrap_or(i32::MAX),
93 secs.try_into()?,
94 micros.try_into()?,
95 true,
96 )
97 }
98}
99
100impl<'py> IntoPyObject<'py> for &Duration {
101 type Target = PyDelta;
102 type Output = Bound<'py, Self::Target>;
103 type Error = PyErr;
104
105 #[inline]
106 fn into_pyobject(self, py: Python<'py>) -> Result<Self::Output, Self::Error> {
107 (*self).into_pyobject(py)
108 }
109}
110
111impl FromPyObject<'_> for Duration {
112 fn extract_bound(ob: &Bound<'_, PyAny>) -> PyResult<Duration> {
113 let delta = ob.downcast::<PyDelta>()?;
114 #[cfg(not(Py_LIMITED_API))]
119 let (days, seconds, microseconds) = {
120 (
121 delta.get_days().into(),
122 delta.get_seconds().into(),
123 delta.get_microseconds().into(),
124 )
125 };
126 #[cfg(Py_LIMITED_API)]
127 let (days, seconds, microseconds) = {
128 let py = delta.py();
129 (
130 delta.getattr(intern!(py, "days"))?.extract()?,
131 delta.getattr(intern!(py, "seconds"))?.extract()?,
132 delta.getattr(intern!(py, "microseconds"))?.extract()?,
133 )
134 };
135 Ok(
136 Duration::days(days)
137 + Duration::seconds(seconds)
138 + Duration::microseconds(microseconds),
139 )
140 }
141}
142
143impl<'py> IntoPyObject<'py> for NaiveDate {
144 type Target = PyDate;
145 type Output = Bound<'py, Self::Target>;
146 type Error = PyErr;
147
148 fn into_pyobject(self, py: Python<'py>) -> Result<Self::Output, Self::Error> {
149 let DateArgs { year, month, day } = (&self).into();
150 PyDate::new(py, year, month, day)
151 }
152}
153
154impl<'py> IntoPyObject<'py> for &NaiveDate {
155 type Target = PyDate;
156 type Output = Bound<'py, Self::Target>;
157 type Error = PyErr;
158
159 #[inline]
160 fn into_pyobject(self, py: Python<'py>) -> Result<Self::Output, Self::Error> {
161 (*self).into_pyobject(py)
162 }
163}
164
165impl FromPyObject<'_> for NaiveDate {
166 fn extract_bound(ob: &Bound<'_, PyAny>) -> PyResult<NaiveDate> {
167 let date = ob.downcast::<PyDate>()?;
168 py_date_to_naive_date(date)
169 }
170}
171
172impl<'py> IntoPyObject<'py> for NaiveTime {
173 type Target = PyTime;
174 type Output = Bound<'py, Self::Target>;
175 type Error = PyErr;
176
177 fn into_pyobject(self, py: Python<'py>) -> Result<Self::Output, Self::Error> {
178 let TimeArgs {
179 hour,
180 min,
181 sec,
182 micro,
183 truncated_leap_second,
184 } = (&self).into();
185
186 let time = PyTime::new(py, hour, min, sec, micro, None)?;
187
188 if truncated_leap_second {
189 warn_truncated_leap_second(&time);
190 }
191
192 Ok(time)
193 }
194}
195
196impl<'py> IntoPyObject<'py> for &NaiveTime {
197 type Target = PyTime;
198 type Output = Bound<'py, Self::Target>;
199 type Error = PyErr;
200
201 #[inline]
202 fn into_pyobject(self, py: Python<'py>) -> Result<Self::Output, Self::Error> {
203 (*self).into_pyobject(py)
204 }
205}
206
207impl FromPyObject<'_> for NaiveTime {
208 fn extract_bound(ob: &Bound<'_, PyAny>) -> PyResult<NaiveTime> {
209 let time = ob.downcast::<PyTime>()?;
210 py_time_to_naive_time(time)
211 }
212}
213
214impl<'py> IntoPyObject<'py> for NaiveDateTime {
215 type Target = PyDateTime;
216 type Output = Bound<'py, Self::Target>;
217 type Error = PyErr;
218
219 fn into_pyobject(self, py: Python<'py>) -> Result<Self::Output, Self::Error> {
220 let DateArgs { year, month, day } = (&self.date()).into();
221 let TimeArgs {
222 hour,
223 min,
224 sec,
225 micro,
226 truncated_leap_second,
227 } = (&self.time()).into();
228
229 let datetime = PyDateTime::new(py, year, month, day, hour, min, sec, micro, None)?;
230
231 if truncated_leap_second {
232 warn_truncated_leap_second(&datetime);
233 }
234
235 Ok(datetime)
236 }
237}
238
239impl<'py> IntoPyObject<'py> for &NaiveDateTime {
240 type Target = PyDateTime;
241 type Output = Bound<'py, Self::Target>;
242 type Error = PyErr;
243
244 #[inline]
245 fn into_pyobject(self, py: Python<'py>) -> Result<Self::Output, Self::Error> {
246 (*self).into_pyobject(py)
247 }
248}
249
250impl FromPyObject<'_> for NaiveDateTime {
251 fn extract_bound(dt: &Bound<'_, PyAny>) -> PyResult<NaiveDateTime> {
252 let dt = dt.downcast::<PyDateTime>()?;
253
254 let has_tzinfo = dt.get_tzinfo().is_some();
258 if has_tzinfo {
259 return Err(PyTypeError::new_err("expected a datetime without tzinfo"));
260 }
261
262 let dt = NaiveDateTime::new(py_date_to_naive_date(dt)?, py_time_to_naive_time(dt)?);
263 Ok(dt)
264 }
265}
266
267impl<'py, Tz: TimeZone> IntoPyObject<'py> for DateTime<Tz>
268where
269 Tz: IntoPyObject<'py>,
270{
271 type Target = PyDateTime;
272 type Output = Bound<'py, Self::Target>;
273 type Error = PyErr;
274
275 #[inline]
276 fn into_pyobject(self, py: Python<'py>) -> Result<Self::Output, Self::Error> {
277 (&self).into_pyobject(py)
278 }
279}
280
281impl<'py, Tz: TimeZone> IntoPyObject<'py> for &DateTime<Tz>
282where
283 Tz: IntoPyObject<'py>,
284{
285 type Target = PyDateTime;
286 type Output = Bound<'py, Self::Target>;
287 type Error = PyErr;
288
289 fn into_pyobject(self, py: Python<'py>) -> Result<Self::Output, Self::Error> {
290 let tz = self.timezone().into_bound_py_any(py)?.downcast_into()?;
291
292 let DateArgs { year, month, day } = (&self.naive_local().date()).into();
293 let TimeArgs {
294 hour,
295 min,
296 sec,
297 micro,
298 truncated_leap_second,
299 } = (&self.naive_local().time()).into();
300
301 let fold = matches!(
302 self.timezone().offset_from_local_datetime(&self.naive_local()),
303 LocalResult::Ambiguous(_, latest) if self.offset().fix() == latest.fix()
304 );
305
306 let datetime = PyDateTime::new_with_fold(
307 py,
308 year,
309 month,
310 day,
311 hour,
312 min,
313 sec,
314 micro,
315 Some(&tz),
316 fold,
317 )?;
318
319 if truncated_leap_second {
320 warn_truncated_leap_second(&datetime);
321 }
322
323 Ok(datetime)
324 }
325}
326
327impl<Tz: TimeZone + for<'py> FromPyObject<'py>> FromPyObject<'_> for DateTime<Tz> {
328 fn extract_bound(dt: &Bound<'_, PyAny>) -> PyResult<DateTime<Tz>> {
329 let dt = dt.downcast::<PyDateTime>()?;
330 let tzinfo = dt.get_tzinfo();
331
332 let tz = if let Some(tzinfo) = tzinfo {
333 tzinfo.extract()?
334 } else {
335 return Err(PyTypeError::new_err(
336 "expected a datetime with non-None tzinfo",
337 ));
338 };
339 let naive_dt = NaiveDateTime::new(py_date_to_naive_date(dt)?, py_time_to_naive_time(dt)?);
340 match naive_dt.and_local_timezone(tz) {
341 LocalResult::Single(value) => Ok(value),
342 LocalResult::Ambiguous(earliest, latest) => {
343 #[cfg(not(Py_LIMITED_API))]
344 let fold = dt.get_fold();
345
346 #[cfg(Py_LIMITED_API)]
347 let fold = dt.getattr(intern!(dt.py(), "fold"))?.extract::<usize>()? > 0;
348
349 if fold {
350 Ok(latest)
351 } else {
352 Ok(earliest)
353 }
354 }
355 LocalResult::None => Err(PyValueError::new_err(format!(
356 "The datetime {dt:?} contains an incompatible timezone"
357 ))),
358 }
359 }
360}
361
362impl<'py> IntoPyObject<'py> for FixedOffset {
363 type Target = PyTzInfo;
364 type Output = Bound<'py, Self::Target>;
365 type Error = PyErr;
366
367 fn into_pyobject(self, py: Python<'py>) -> Result<Self::Output, Self::Error> {
368 let seconds_offset = self.local_minus_utc();
369 let td = PyDelta::new(py, 0, seconds_offset, 0, true)?;
370 PyTzInfo::fixed_offset(py, td)
371 }
372}
373
374impl<'py> IntoPyObject<'py> for &FixedOffset {
375 type Target = PyTzInfo;
376 type Output = Bound<'py, Self::Target>;
377 type Error = PyErr;
378
379 #[inline]
380 fn into_pyobject(self, py: Python<'py>) -> Result<Self::Output, Self::Error> {
381 (*self).into_pyobject(py)
382 }
383}
384
385impl FromPyObject<'_> for FixedOffset {
386 fn extract_bound(ob: &Bound<'_, PyAny>) -> PyResult<FixedOffset> {
391 let ob = ob.downcast::<PyTzInfo>()?;
392
393 let py_timedelta =
399 ob.call_method1(intern!(ob.py(), "utcoffset"), (PyNone::get(ob.py()),))?;
400 if py_timedelta.is_none() {
401 return Err(PyTypeError::new_err(format!(
402 "{ob:?} is not a fixed offset timezone"
403 )));
404 }
405 let total_seconds: Duration = py_timedelta.extract()?;
406 let total_seconds = total_seconds.num_seconds() as i32;
408 FixedOffset::east_opt(total_seconds)
409 .ok_or_else(|| PyValueError::new_err("fixed offset out of bounds"))
410 }
411}
412
413impl<'py> IntoPyObject<'py> for Utc {
414 type Target = PyTzInfo;
415 type Output = Borrowed<'static, 'py, Self::Target>;
416 type Error = PyErr;
417
418 fn into_pyobject(self, py: Python<'py>) -> Result<Self::Output, Self::Error> {
419 PyTzInfo::utc(py)
420 }
421}
422
423impl<'py> IntoPyObject<'py> for &Utc {
424 type Target = PyTzInfo;
425 type Output = Borrowed<'static, 'py, Self::Target>;
426 type Error = PyErr;
427
428 #[inline]
429 fn into_pyobject(self, py: Python<'py>) -> Result<Self::Output, Self::Error> {
430 (*self).into_pyobject(py)
431 }
432}
433
434impl FromPyObject<'_> for Utc {
435 fn extract_bound(ob: &Bound<'_, PyAny>) -> PyResult<Utc> {
436 let py_utc = PyTzInfo::utc(ob.py())?;
437 if ob.eq(py_utc)? {
438 Ok(Utc)
439 } else {
440 Err(PyValueError::new_err("expected datetime.timezone.utc"))
441 }
442 }
443}
444
445#[cfg(feature = "chrono-local")]
446impl<'py> IntoPyObject<'py> for Local {
447 type Target = PyTzInfo;
448 type Output = Borrowed<'static, 'py, Self::Target>;
449 type Error = PyErr;
450
451 fn into_pyobject(self, py: Python<'py>) -> Result<Self::Output, Self::Error> {
452 static LOCAL_TZ: GILOnceCell<Py<PyTzInfo>> = GILOnceCell::new();
453 let tz = LOCAL_TZ
454 .get_or_try_init(py, || {
455 let iana_name = iana_time_zone::get_timezone().map_err(|e| {
456 PyRuntimeError::new_err(format!("Could not get local timezone: {e}"))
457 })?;
458 PyTzInfo::timezone(py, iana_name).map(Bound::unbind)
459 })?
460 .bind_borrowed(py);
461 Ok(tz)
462 }
463}
464
465#[cfg(feature = "chrono-local")]
466impl<'py> IntoPyObject<'py> for &Local {
467 type Target = PyTzInfo;
468 type Output = Borrowed<'static, 'py, Self::Target>;
469 type Error = PyErr;
470
471 #[inline]
472 fn into_pyobject(self, py: Python<'py>) -> Result<Self::Output, Self::Error> {
473 (*self).into_pyobject(py)
474 }
475}
476
477#[cfg(feature = "chrono-local")]
478impl FromPyObject<'_> for Local {
479 fn extract_bound(ob: &Bound<'_, PyAny>) -> PyResult<Local> {
480 let local_tz = Local.into_pyobject(ob.py())?;
481 if ob.eq(local_tz)? {
482 Ok(Local)
483 } else {
484 let name = local_tz.getattr("key")?.downcast_into::<PyString>()?;
485 Err(PyValueError::new_err(format!(
486 "expected local timezone {}",
487 name.to_cow()?
488 )))
489 }
490 }
491}
492
493struct DateArgs {
494 year: i32,
495 month: u8,
496 day: u8,
497}
498
499impl From<&NaiveDate> for DateArgs {
500 fn from(value: &NaiveDate) -> Self {
501 Self {
502 year: value.year(),
503 month: value.month() as u8,
504 day: value.day() as u8,
505 }
506 }
507}
508
509struct TimeArgs {
510 hour: u8,
511 min: u8,
512 sec: u8,
513 micro: u32,
514 truncated_leap_second: bool,
515}
516
517impl From<&NaiveTime> for TimeArgs {
518 fn from(value: &NaiveTime) -> Self {
519 let ns = value.nanosecond();
520 let checked_sub = ns.checked_sub(1_000_000_000);
521 let truncated_leap_second = checked_sub.is_some();
522 let micro = checked_sub.unwrap_or(ns) / 1000;
523 Self {
524 hour: value.hour() as u8,
525 min: value.minute() as u8,
526 sec: value.second() as u8,
527 micro,
528 truncated_leap_second,
529 }
530 }
531}
532
533fn warn_truncated_leap_second(obj: &Bound<'_, PyAny>) {
534 let py = obj.py();
535 if let Err(e) = PyErr::warn(
536 py,
537 &py.get_type::<PyUserWarning>(),
538 ffi::c_str!("ignored leap-second, `datetime` does not support leap-seconds"),
539 0,
540 ) {
541 e.write_unraisable(py, Some(obj))
542 };
543}
544
545#[cfg(not(Py_LIMITED_API))]
546fn py_date_to_naive_date(py_date: &impl PyDateAccess) -> PyResult<NaiveDate> {
547 NaiveDate::from_ymd_opt(
548 py_date.get_year(),
549 py_date.get_month().into(),
550 py_date.get_day().into(),
551 )
552 .ok_or_else(|| PyValueError::new_err("invalid or out-of-range date"))
553}
554
555#[cfg(Py_LIMITED_API)]
556fn py_date_to_naive_date(py_date: &Bound<'_, PyAny>) -> PyResult<NaiveDate> {
557 NaiveDate::from_ymd_opt(
558 py_date.getattr(intern!(py_date.py(), "year"))?.extract()?,
559 py_date.getattr(intern!(py_date.py(), "month"))?.extract()?,
560 py_date.getattr(intern!(py_date.py(), "day"))?.extract()?,
561 )
562 .ok_or_else(|| PyValueError::new_err("invalid or out-of-range date"))
563}
564
565#[cfg(not(Py_LIMITED_API))]
566fn py_time_to_naive_time(py_time: &impl PyTimeAccess) -> PyResult<NaiveTime> {
567 NaiveTime::from_hms_micro_opt(
568 py_time.get_hour().into(),
569 py_time.get_minute().into(),
570 py_time.get_second().into(),
571 py_time.get_microsecond(),
572 )
573 .ok_or_else(|| PyValueError::new_err("invalid or out-of-range time"))
574}
575
576#[cfg(Py_LIMITED_API)]
577fn py_time_to_naive_time(py_time: &Bound<'_, PyAny>) -> PyResult<NaiveTime> {
578 NaiveTime::from_hms_micro_opt(
579 py_time.getattr(intern!(py_time.py(), "hour"))?.extract()?,
580 py_time
581 .getattr(intern!(py_time.py(), "minute"))?
582 .extract()?,
583 py_time
584 .getattr(intern!(py_time.py(), "second"))?
585 .extract()?,
586 py_time
587 .getattr(intern!(py_time.py(), "microsecond"))?
588 .extract()?,
589 )
590 .ok_or_else(|| PyValueError::new_err("invalid or out-of-range time"))
591}
592
593#[cfg(test)]
594mod tests {
595 use super::*;
596 use crate::{types::PyTuple, BoundObject};
597 use std::{cmp::Ordering, panic};
598
599 #[test]
600 #[cfg(all(Py_3_9, not(target_os = "windows")))]
604 fn test_zoneinfo_is_not_fixed_offset() {
605 use crate::ffi;
606 use crate::types::any::PyAnyMethods;
607 use crate::types::dict::PyDictMethods;
608
609 Python::attach(|py| {
610 let locals = crate::types::PyDict::new(py);
611 py.run(
612 ffi::c_str!("import zoneinfo; zi = zoneinfo.ZoneInfo('Europe/London')"),
613 None,
614 Some(&locals),
615 )
616 .unwrap();
617 let result: PyResult<FixedOffset> = locals.get_item("zi").unwrap().unwrap().extract();
618 assert!(result.is_err());
619 let res = result.err().unwrap();
620 let msg = res.value(py).repr().unwrap().to_string();
622 assert_eq!(msg, "TypeError(\"zoneinfo.ZoneInfo(key='Europe/London') is not a fixed offset timezone\")");
623 });
624 }
625
626 #[test]
627 fn test_timezone_aware_to_naive_fails() {
628 Python::attach(|py| {
631 let py_datetime =
632 new_py_datetime_ob(py, "datetime", (2022, 1, 1, 1, 0, 0, 0, python_utc(py)));
633 let res: PyResult<NaiveDateTime> = py_datetime.extract();
635 assert_eq!(
636 res.unwrap_err().value(py).repr().unwrap().to_string(),
637 "TypeError('expected a datetime without tzinfo')"
638 );
639 });
640 }
641
642 #[test]
643 fn test_naive_to_timezone_aware_fails() {
644 Python::attach(|py| {
647 let py_datetime = new_py_datetime_ob(py, "datetime", (2022, 1, 1, 1, 0, 0, 0));
648 let res: PyResult<DateTime<Utc>> = py_datetime.extract();
650 assert_eq!(
651 res.unwrap_err().value(py).repr().unwrap().to_string(),
652 "TypeError('expected a datetime with non-None tzinfo')"
653 );
654
655 let res: PyResult<DateTime<FixedOffset>> = py_datetime.extract();
657 assert_eq!(
658 res.unwrap_err().value(py).repr().unwrap().to_string(),
659 "TypeError('expected a datetime with non-None tzinfo')"
660 );
661 });
662 }
663
664 #[test]
665 fn test_invalid_types_fail() {
666 Python::attach(|py| {
669 let none = py.None().into_bound(py);
670 assert_eq!(
671 none.extract::<Duration>().unwrap_err().to_string(),
672 "TypeError: 'NoneType' object cannot be converted to 'PyDelta'"
673 );
674 assert_eq!(
675 none.extract::<FixedOffset>().unwrap_err().to_string(),
676 "TypeError: 'NoneType' object cannot be converted to 'PyTzInfo'"
677 );
678 assert_eq!(
679 none.extract::<Utc>().unwrap_err().to_string(),
680 "ValueError: expected datetime.timezone.utc"
681 );
682 assert_eq!(
683 none.extract::<NaiveTime>().unwrap_err().to_string(),
684 "TypeError: 'NoneType' object cannot be converted to 'PyTime'"
685 );
686 assert_eq!(
687 none.extract::<NaiveDate>().unwrap_err().to_string(),
688 "TypeError: 'NoneType' object cannot be converted to 'PyDate'"
689 );
690 assert_eq!(
691 none.extract::<NaiveDateTime>().unwrap_err().to_string(),
692 "TypeError: 'NoneType' object cannot be converted to 'PyDateTime'"
693 );
694 assert_eq!(
695 none.extract::<DateTime<Utc>>().unwrap_err().to_string(),
696 "TypeError: 'NoneType' object cannot be converted to 'PyDateTime'"
697 );
698 assert_eq!(
699 none.extract::<DateTime<FixedOffset>>()
700 .unwrap_err()
701 .to_string(),
702 "TypeError: 'NoneType' object cannot be converted to 'PyDateTime'"
703 );
704 });
705 }
706
707 #[test]
708 fn test_pyo3_timedelta_into_pyobject() {
709 let check = |name: &'static str, delta: Duration, py_days, py_seconds, py_ms| {
712 Python::attach(|py| {
713 let delta = delta.into_pyobject(py).unwrap();
714 let py_delta = new_py_datetime_ob(py, "timedelta", (py_days, py_seconds, py_ms));
715 assert!(
716 delta.eq(&py_delta).unwrap(),
717 "{name}: {delta} != {py_delta}"
718 );
719 });
720 };
721
722 let delta = Duration::days(-1) + Duration::seconds(1) + Duration::microseconds(-10);
723 check("delta normalization", delta, -1, 1, -10);
724
725 let delta = Duration::seconds(-86399999913600); check("delta min value", delta, -999999999, 0, 0);
729
730 let delta = Duration::seconds(86399999999999) + Duration::nanoseconds(999999000); check("delta max value", delta, 999999999, 86399, 999999);
733
734 Python::attach(|py| {
736 #[allow(deprecated)]
738 {
739 assert!(Duration::min_value().into_pyobject(py).is_err());
740 assert!(Duration::max_value().into_pyobject(py).is_err());
741 }
742 });
743 }
744
745 #[test]
746 fn test_pyo3_timedelta_frompyobject() {
747 let check = |name: &'static str, delta: Duration, py_days, py_seconds, py_ms| {
750 Python::attach(|py| {
751 let py_delta = new_py_datetime_ob(py, "timedelta", (py_days, py_seconds, py_ms));
752 let py_delta: Duration = py_delta.extract().unwrap();
753 assert_eq!(py_delta, delta, "{name}: {py_delta} != {delta}");
754 })
755 };
756
757 check(
760 "min py_delta value",
761 Duration::seconds(-86399999913600),
762 -999999999,
763 0,
764 0,
765 );
766 check(
768 "max py_delta value",
769 Duration::seconds(86399999999999) + Duration::microseconds(999999),
770 999999999,
771 86399,
772 999999,
773 );
774
775 Python::attach(|py| {
778 let low_days: i32 = -1000000000;
779 assert!(panic::catch_unwind(|| Duration::days(low_days as i64)).is_ok());
781 assert!(panic::catch_unwind(|| {
783 let py_delta = new_py_datetime_ob(py, "timedelta", (low_days, 0, 0));
784 if let Ok(_duration) = py_delta.extract::<Duration>() {
785 }
787 })
788 .is_err());
789
790 let high_days: i32 = 1000000000;
791 assert!(panic::catch_unwind(|| Duration::days(high_days as i64)).is_ok());
793 assert!(panic::catch_unwind(|| {
795 let py_delta = new_py_datetime_ob(py, "timedelta", (high_days, 0, 0));
796 if let Ok(_duration) = py_delta.extract::<Duration>() {
797 }
799 })
800 .is_err());
801 });
802 }
803
804 #[test]
805 fn test_pyo3_date_into_pyobject() {
806 let eq_ymd = |name: &'static str, year, month, day| {
807 Python::attach(|py| {
808 let date = NaiveDate::from_ymd_opt(year, month, day)
809 .unwrap()
810 .into_pyobject(py)
811 .unwrap();
812 let py_date = new_py_datetime_ob(py, "date", (year, month, day));
813 assert_eq!(
814 date.compare(&py_date).unwrap(),
815 Ordering::Equal,
816 "{name}: {date} != {py_date}"
817 );
818 })
819 };
820
821 eq_ymd("past date", 2012, 2, 29);
822 eq_ymd("min date", 1, 1, 1);
823 eq_ymd("future date", 3000, 6, 5);
824 eq_ymd("max date", 9999, 12, 31);
825 }
826
827 #[test]
828 fn test_pyo3_date_frompyobject() {
829 let eq_ymd = |name: &'static str, year, month, day| {
830 Python::attach(|py| {
831 let py_date = new_py_datetime_ob(py, "date", (year, month, day));
832 let py_date: NaiveDate = py_date.extract().unwrap();
833 let date = NaiveDate::from_ymd_opt(year, month, day).unwrap();
834 assert_eq!(py_date, date, "{name}: {date} != {py_date}");
835 })
836 };
837
838 eq_ymd("past date", 2012, 2, 29);
839 eq_ymd("min date", 1, 1, 1);
840 eq_ymd("future date", 3000, 6, 5);
841 eq_ymd("max date", 9999, 12, 31);
842 }
843
844 #[test]
845 fn test_pyo3_datetime_into_pyobject_utc() {
846 Python::attach(|py| {
847 let check_utc =
848 |name: &'static str, year, month, day, hour, minute, second, ms, py_ms| {
849 let datetime = NaiveDate::from_ymd_opt(year, month, day)
850 .unwrap()
851 .and_hms_micro_opt(hour, minute, second, ms)
852 .unwrap()
853 .and_utc();
854 let datetime = datetime.into_pyobject(py).unwrap();
855 let py_datetime = new_py_datetime_ob(
856 py,
857 "datetime",
858 (
859 year,
860 month,
861 day,
862 hour,
863 minute,
864 second,
865 py_ms,
866 python_utc(py),
867 ),
868 );
869 assert_eq!(
870 datetime.compare(&py_datetime).unwrap(),
871 Ordering::Equal,
872 "{name}: {datetime} != {py_datetime}"
873 );
874 };
875
876 check_utc("regular", 2014, 5, 6, 7, 8, 9, 999_999, 999_999);
877
878 #[cfg(not(Py_GIL_DISABLED))]
879 assert_warnings!(
880 py,
881 check_utc("leap second", 2014, 5, 6, 7, 8, 59, 1_999_999, 999_999),
882 [(
883 PyUserWarning,
884 "ignored leap-second, `datetime` does not support leap-seconds"
885 )]
886 );
887 })
888 }
889
890 #[test]
891 fn test_pyo3_datetime_into_pyobject_fixed_offset() {
892 Python::attach(|py| {
893 let check_fixed_offset =
894 |name: &'static str, year, month, day, hour, minute, second, ms, py_ms| {
895 let offset = FixedOffset::east_opt(3600).unwrap();
896 let datetime = NaiveDate::from_ymd_opt(year, month, day)
897 .unwrap()
898 .and_hms_micro_opt(hour, minute, second, ms)
899 .unwrap()
900 .and_local_timezone(offset)
901 .unwrap();
902 let datetime = datetime.into_pyobject(py).unwrap();
903 let py_tz = offset.into_pyobject(py).unwrap();
904 let py_datetime = new_py_datetime_ob(
905 py,
906 "datetime",
907 (year, month, day, hour, minute, second, py_ms, py_tz),
908 );
909 assert_eq!(
910 datetime.compare(&py_datetime).unwrap(),
911 Ordering::Equal,
912 "{name}: {datetime} != {py_datetime}"
913 );
914 };
915
916 check_fixed_offset("regular", 2014, 5, 6, 7, 8, 9, 999_999, 999_999);
917
918 #[cfg(not(Py_GIL_DISABLED))]
919 assert_warnings!(
920 py,
921 check_fixed_offset("leap second", 2014, 5, 6, 7, 8, 59, 1_999_999, 999_999),
922 [(
923 PyUserWarning,
924 "ignored leap-second, `datetime` does not support leap-seconds"
925 )]
926 );
927 })
928 }
929
930 #[test]
931 #[cfg(all(Py_3_9, feature = "chrono-tz", not(windows)))]
932 fn test_pyo3_datetime_into_pyobject_tz() {
933 Python::attach(|py| {
934 let datetime = NaiveDate::from_ymd_opt(2024, 12, 11)
935 .unwrap()
936 .and_hms_opt(23, 3, 13)
937 .unwrap()
938 .and_local_timezone(chrono_tz::Tz::Europe__London)
939 .unwrap();
940 let datetime = datetime.into_pyobject(py).unwrap();
941 let py_datetime = new_py_datetime_ob(
942 py,
943 "datetime",
944 (
945 2024,
946 12,
947 11,
948 23,
949 3,
950 13,
951 0,
952 python_zoneinfo(py, "Europe/London"),
953 ),
954 );
955 assert_eq!(datetime.compare(&py_datetime).unwrap(), Ordering::Equal);
956 })
957 }
958
959 #[test]
960 fn test_pyo3_datetime_frompyobject_utc() {
961 Python::attach(|py| {
962 let year = 2014;
963 let month = 5;
964 let day = 6;
965 let hour = 7;
966 let minute = 8;
967 let second = 9;
968 let micro = 999_999;
969 let tz_utc = PyTzInfo::utc(py).unwrap();
970 let py_datetime = new_py_datetime_ob(
971 py,
972 "datetime",
973 (year, month, day, hour, minute, second, micro, tz_utc),
974 );
975 let py_datetime: DateTime<Utc> = py_datetime.extract().unwrap();
976 let datetime = NaiveDate::from_ymd_opt(year, month, day)
977 .unwrap()
978 .and_hms_micro_opt(hour, minute, second, micro)
979 .unwrap()
980 .and_utc();
981 assert_eq!(py_datetime, datetime,);
982 })
983 }
984
985 #[test]
986 fn test_pyo3_datetime_frompyobject_fixed_offset() {
987 Python::attach(|py| {
988 let year = 2014;
989 let month = 5;
990 let day = 6;
991 let hour = 7;
992 let minute = 8;
993 let second = 9;
994 let micro = 999_999;
995 let offset = FixedOffset::east_opt(3600).unwrap();
996 let py_tz = offset.into_pyobject(py).unwrap();
997 let py_datetime = new_py_datetime_ob(
998 py,
999 "datetime",
1000 (year, month, day, hour, minute, second, micro, py_tz),
1001 );
1002 let datetime_from_py: DateTime<FixedOffset> = py_datetime.extract().unwrap();
1003 let datetime = NaiveDate::from_ymd_opt(year, month, day)
1004 .unwrap()
1005 .and_hms_micro_opt(hour, minute, second, micro)
1006 .unwrap();
1007 let datetime = datetime.and_local_timezone(offset).unwrap();
1008
1009 assert_eq!(datetime_from_py, datetime);
1010 assert!(
1011 py_datetime.extract::<DateTime<Utc>>().is_err(),
1012 "Extracting Utc from nonzero FixedOffset timezone will fail"
1013 );
1014
1015 let utc = python_utc(py);
1016 let py_datetime_utc = new_py_datetime_ob(
1017 py,
1018 "datetime",
1019 (year, month, day, hour, minute, second, micro, utc),
1020 );
1021 assert!(
1022 py_datetime_utc.extract::<DateTime<FixedOffset>>().is_ok(),
1023 "Extracting FixedOffset from Utc timezone will succeed"
1024 );
1025 })
1026 }
1027
1028 #[test]
1029 fn test_pyo3_offset_fixed_into_pyobject() {
1030 Python::attach(|py| {
1031 let offset = FixedOffset::east_opt(3600)
1033 .unwrap()
1034 .into_pyobject(py)
1035 .unwrap();
1036 let td = new_py_datetime_ob(py, "timedelta", (0, 3600, 0));
1038 let py_timedelta = new_py_datetime_ob(py, "timezone", (td,));
1039 assert!(offset.eq(py_timedelta).unwrap());
1041
1042 let offset = FixedOffset::east_opt(-3600)
1044 .unwrap()
1045 .into_pyobject(py)
1046 .unwrap();
1047 let td = new_py_datetime_ob(py, "timedelta", (0, -3600, 0));
1048 let py_timedelta = new_py_datetime_ob(py, "timezone", (td,));
1049 assert!(offset.eq(py_timedelta).unwrap());
1050 })
1051 }
1052
1053 #[test]
1054 fn test_pyo3_offset_fixed_frompyobject() {
1055 Python::attach(|py| {
1056 let py_timedelta = new_py_datetime_ob(py, "timedelta", (0, 3600, 0));
1057 let py_tzinfo = new_py_datetime_ob(py, "timezone", (py_timedelta,));
1058 let offset: FixedOffset = py_tzinfo.extract().unwrap();
1059 assert_eq!(FixedOffset::east_opt(3600).unwrap(), offset);
1060 })
1061 }
1062
1063 #[test]
1064 fn test_pyo3_offset_utc_into_pyobject() {
1065 Python::attach(|py| {
1066 let utc = Utc.into_pyobject(py).unwrap();
1067 let py_utc = python_utc(py);
1068 assert!(utc.is(&py_utc));
1069 })
1070 }
1071
1072 #[test]
1073 fn test_pyo3_offset_utc_frompyobject() {
1074 Python::attach(|py| {
1075 let py_utc = python_utc(py);
1076 let py_utc: Utc = py_utc.extract().unwrap();
1077 assert_eq!(Utc, py_utc);
1078
1079 let py_timedelta = new_py_datetime_ob(py, "timedelta", (0, 0, 0));
1080 let py_timezone_utc = new_py_datetime_ob(py, "timezone", (py_timedelta,));
1081 let py_timezone_utc: Utc = py_timezone_utc.extract().unwrap();
1082 assert_eq!(Utc, py_timezone_utc);
1083
1084 let py_timedelta = new_py_datetime_ob(py, "timedelta", (0, 3600, 0));
1085 let py_timezone = new_py_datetime_ob(py, "timezone", (py_timedelta,));
1086 assert!(py_timezone.extract::<Utc>().is_err());
1087 })
1088 }
1089
1090 #[test]
1091 fn test_pyo3_time_into_pyobject() {
1092 Python::attach(|py| {
1093 let check_time = |name: &'static str, hour, minute, second, ms, py_ms| {
1094 let time = NaiveTime::from_hms_micro_opt(hour, minute, second, ms)
1095 .unwrap()
1096 .into_pyobject(py)
1097 .unwrap();
1098 let py_time = new_py_datetime_ob(py, "time", (hour, minute, second, py_ms));
1099 assert!(time.eq(&py_time).unwrap(), "{name}: {time} != {py_time}");
1100 };
1101
1102 check_time("regular", 3, 5, 7, 999_999, 999_999);
1103
1104 #[cfg(not(Py_GIL_DISABLED))]
1105 assert_warnings!(
1106 py,
1107 check_time("leap second", 3, 5, 59, 1_999_999, 999_999),
1108 [(
1109 PyUserWarning,
1110 "ignored leap-second, `datetime` does not support leap-seconds"
1111 )]
1112 );
1113 })
1114 }
1115
1116 #[test]
1117 fn test_pyo3_time_frompyobject() {
1118 let hour = 3;
1119 let minute = 5;
1120 let second = 7;
1121 let micro = 999_999;
1122 Python::attach(|py| {
1123 let py_time = new_py_datetime_ob(py, "time", (hour, minute, second, micro));
1124 let py_time: NaiveTime = py_time.extract().unwrap();
1125 let time = NaiveTime::from_hms_micro_opt(hour, minute, second, micro).unwrap();
1126 assert_eq!(py_time, time);
1127 })
1128 }
1129
1130 fn new_py_datetime_ob<'py, A>(py: Python<'py>, name: &str, args: A) -> Bound<'py, PyAny>
1131 where
1132 A: IntoPyObject<'py, Target = PyTuple>,
1133 {
1134 py.import("datetime")
1135 .unwrap()
1136 .getattr(name)
1137 .unwrap()
1138 .call1(
1139 args.into_pyobject(py)
1140 .map_err(Into::into)
1141 .unwrap()
1142 .into_bound(),
1143 )
1144 .unwrap()
1145 }
1146
1147 fn python_utc(py: Python<'_>) -> Bound<'_, PyAny> {
1148 py.import("datetime")
1149 .unwrap()
1150 .getattr("timezone")
1151 .unwrap()
1152 .getattr("utc")
1153 .unwrap()
1154 }
1155
1156 #[cfg(all(Py_3_9, feature = "chrono-tz", not(windows)))]
1157 fn python_zoneinfo<'py>(py: Python<'py>, timezone: &str) -> Bound<'py, PyAny> {
1158 py.import("zoneinfo")
1159 .unwrap()
1160 .getattr("ZoneInfo")
1161 .unwrap()
1162 .call1((timezone,))
1163 .unwrap()
1164 }
1165
1166 #[cfg(not(any(target_arch = "wasm32", Py_GIL_DISABLED)))]
1167 mod proptests {
1168 use super::*;
1169 use crate::tests::common::CatchWarnings;
1170 use crate::types::IntoPyDict;
1171 use proptest::prelude::*;
1172 use std::ffi::CString;
1173
1174 proptest! {
1175
1176 #[test]
1178 fn test_pyo3_offset_fixed_frompyobject_created_in_python(timestamp in 0..(i32::MAX as i64), timedelta in -86399i32..=86399i32) {
1179 Python::attach(|py| {
1180
1181 let globals = [("datetime", py.import("datetime").unwrap())].into_py_dict(py).unwrap();
1182 let code = format!("datetime.datetime.fromtimestamp({timestamp}).replace(tzinfo=datetime.timezone(datetime.timedelta(seconds={timedelta})))");
1183 let t = py.eval(&CString::new(code).unwrap(), Some(&globals), None).unwrap();
1184
1185 let py_iso_str = t.call_method0("isoformat").unwrap();
1187
1188 let t = t.extract::<DateTime<FixedOffset>>().unwrap();
1190 let rust_iso_str = if timedelta % 60 == 0 {
1192 t.format("%Y-%m-%dT%H:%M:%S%:z").to_string()
1193 } else {
1194 t.format("%Y-%m-%dT%H:%M:%S%::z").to_string()
1195 };
1196
1197 assert_eq!(py_iso_str.to_string(), rust_iso_str);
1199 })
1200 }
1201
1202 #[test]
1203 fn test_duration_roundtrip(days in -999999999i64..=999999999i64) {
1204 Python::attach(|py| {
1207 let dur = Duration::days(days);
1208 let py_delta = dur.into_pyobject(py).unwrap();
1209 let roundtripped: Duration = py_delta.extract().expect("Round trip");
1210 assert_eq!(dur, roundtripped);
1211 })
1212 }
1213
1214 #[test]
1215 fn test_fixed_offset_roundtrip(secs in -86399i32..=86399i32) {
1216 Python::attach(|py| {
1217 let offset = FixedOffset::east_opt(secs).unwrap();
1218 let py_offset = offset.into_pyobject(py).unwrap();
1219 let roundtripped: FixedOffset = py_offset.extract().expect("Round trip");
1220 assert_eq!(offset, roundtripped);
1221 })
1222 }
1223
1224 #[test]
1225 fn test_naive_date_roundtrip(
1226 year in 1i32..=9999i32,
1227 month in 1u32..=12u32,
1228 day in 1u32..=31u32
1229 ) {
1230 Python::attach(|py| {
1233 if let Some(date) = NaiveDate::from_ymd_opt(year, month, day) {
1236 let py_date = date.into_pyobject(py).unwrap();
1237 let roundtripped: NaiveDate = py_date.extract().expect("Round trip");
1238 assert_eq!(date, roundtripped);
1239 }
1240 })
1241 }
1242
1243 #[test]
1244 fn test_naive_time_roundtrip(
1245 hour in 0u32..=23u32,
1246 min in 0u32..=59u32,
1247 sec in 0u32..=59u32,
1248 micro in 0u32..=1_999_999u32
1249 ) {
1250 Python::attach(|py| {
1255 if let Some(time) = NaiveTime::from_hms_micro_opt(hour, min, sec, micro) {
1256 let py_time = CatchWarnings::enter(py, |_| time.into_pyobject(py)).unwrap();
1258 let roundtripped: NaiveTime = py_time.extract().expect("Round trip");
1259 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);
1261 assert_eq!(expected_roundtrip_time, roundtripped);
1262 }
1263 })
1264 }
1265
1266 #[test]
1267 fn test_naive_datetime_roundtrip(
1268 year in 1i32..=9999i32,
1269 month in 1u32..=12u32,
1270 day in 1u32..=31u32,
1271 hour in 0u32..=24u32,
1272 min in 0u32..=60u32,
1273 sec in 0u32..=60u32,
1274 micro in 0u32..=999_999u32
1275 ) {
1276 Python::attach(|py| {
1277 let date_opt = NaiveDate::from_ymd_opt(year, month, day);
1278 let time_opt = NaiveTime::from_hms_micro_opt(hour, min, sec, micro);
1279 if let (Some(date), Some(time)) = (date_opt, time_opt) {
1280 let dt = NaiveDateTime::new(date, time);
1281 let pydt = dt.into_pyobject(py).unwrap();
1282 let roundtripped: NaiveDateTime = pydt.extract().expect("Round trip");
1283 assert_eq!(dt, roundtripped);
1284 }
1285 })
1286 }
1287
1288 #[test]
1289 fn test_utc_datetime_roundtrip(
1290 year in 1i32..=9999i32,
1291 month in 1u32..=12u32,
1292 day in 1u32..=31u32,
1293 hour in 0u32..=23u32,
1294 min in 0u32..=59u32,
1295 sec in 0u32..=59u32,
1296 micro in 0u32..=1_999_999u32
1297 ) {
1298 Python::attach(|py| {
1299 let date_opt = NaiveDate::from_ymd_opt(year, month, day);
1300 let time_opt = NaiveTime::from_hms_micro_opt(hour, min, sec, micro);
1301 if let (Some(date), Some(time)) = (date_opt, time_opt) {
1302 let dt: DateTime<Utc> = NaiveDateTime::new(date, time).and_utc();
1303 let py_dt = CatchWarnings::enter(py, |_| dt.into_pyobject(py)).unwrap();
1305 let roundtripped: DateTime<Utc> = py_dt.extract().expect("Round trip");
1306 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);
1308 let expected_roundtrip_dt: DateTime<Utc> = NaiveDateTime::new(date, expected_roundtrip_time).and_utc();
1309 assert_eq!(expected_roundtrip_dt, roundtripped);
1310 }
1311 })
1312 }
1313
1314 #[test]
1315 fn test_fixed_offset_datetime_roundtrip(
1316 year in 1i32..=9999i32,
1317 month in 1u32..=12u32,
1318 day in 1u32..=31u32,
1319 hour in 0u32..=23u32,
1320 min in 0u32..=59u32,
1321 sec in 0u32..=59u32,
1322 micro in 0u32..=1_999_999u32,
1323 offset_secs in -86399i32..=86399i32
1324 ) {
1325 Python::attach(|py| {
1326 let date_opt = NaiveDate::from_ymd_opt(year, month, day);
1327 let time_opt = NaiveTime::from_hms_micro_opt(hour, min, sec, micro);
1328 let offset = FixedOffset::east_opt(offset_secs).unwrap();
1329 if let (Some(date), Some(time)) = (date_opt, time_opt) {
1330 let dt: DateTime<FixedOffset> = NaiveDateTime::new(date, time).and_local_timezone(offset).unwrap();
1331 let py_dt = CatchWarnings::enter(py, |_| dt.into_pyobject(py)).unwrap();
1333 let roundtripped: DateTime<FixedOffset> = py_dt.extract().expect("Round trip");
1334 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);
1336 let expected_roundtrip_dt: DateTime<FixedOffset> = NaiveDateTime::new(date, expected_roundtrip_time).and_local_timezone(offset).unwrap();
1337 assert_eq!(expected_roundtrip_dt, roundtripped);
1338 }
1339 })
1340 }
1341
1342 #[test]
1343 #[cfg(all(feature = "chrono-local", not(target_os = "windows")))]
1344 fn test_local_datetime_roundtrip(
1345 year in 1i32..=9999i32,
1346 month in 1u32..=12u32,
1347 day in 1u32..=31u32,
1348 hour in 0u32..=23u32,
1349 min in 0u32..=59u32,
1350 sec in 0u32..=59u32,
1351 micro in 0u32..=1_999_999u32,
1352 ) {
1353 Python::attach(|py| {
1354 let date_opt = NaiveDate::from_ymd_opt(year, month, day);
1355 let time_opt = NaiveTime::from_hms_micro_opt(hour, min, sec, micro);
1356 if let (Some(date), Some(time)) = (date_opt, time_opt) {
1357 let dts = match NaiveDateTime::new(date, time).and_local_timezone(Local) {
1358 LocalResult::None => return,
1359 LocalResult::Single(dt) => [Some((dt, false)), None],
1360 LocalResult::Ambiguous(dt1, dt2) => [Some((dt1, false)), Some((dt2, true))],
1361 };
1362 for (dt, fold) in dts.iter().filter_map(|input| *input) {
1363 let py_dt = CatchWarnings::enter(py, |_| dt.into_pyobject(py)).unwrap();
1365 let roundtripped: DateTime<Local> = py_dt.extract().expect("Round trip");
1366 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);
1368 let expected_roundtrip_dt: DateTime<Local> = if fold {
1369 NaiveDateTime::new(date, expected_roundtrip_time).and_local_timezone(Local).latest()
1370 } else {
1371 NaiveDateTime::new(date, expected_roundtrip_time).and_local_timezone(Local).earliest()
1372 }.unwrap();
1373 assert_eq!(expected_roundtrip_dt, roundtripped);
1374 }
1375 }
1376 })
1377 }
1378 }
1379 }
1380}