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