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