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