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