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::any::PyAnyMethods;
50use crate::types::PyNone;
51use crate::types::{PyDate, PyDateTime, PyDelta, PyTime, PyTzInfo, PyTzInfoAccess};
52#[cfg(not(Py_LIMITED_API))]
53use crate::types::{PyDateAccess, PyDeltaAccess, PyTimeAccess};
54#[cfg(feature = "chrono-local")]
55use crate::{
56 exceptions::PyRuntimeError,
57 sync::PyOnceLock,
58 types::{PyString, PyStringMethods},
59 Py,
60};
61#[cfg(feature = "experimental-inspect")]
62use crate::{type_hint_identifier, PyTypeInfo};
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: PyStaticExpr = 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: PyStaticExpr = 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: PyStaticExpr = 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: PyStaticExpr = 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: PyStaticExpr = 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: PyStaticExpr = 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: PyStaticExpr = 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: PyStaticExpr = 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: PyStaticExpr = 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: PyStaticExpr = 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: PyStaticExpr = 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: PyStaticExpr = 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: PyStaticExpr = <&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: PyStaticExpr = 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: PyStaticExpr = 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: PyStaticExpr = type_hint_identifier!("datetime", "timezone");
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: PyStaticExpr = 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: PyStaticExpr = 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: PyStaticExpr = type_hint_identifier!("datetime", "timezone");
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: PyStaticExpr = 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 #[cfg(feature = "experimental-inspect")]
505 const INPUT_TYPE: PyStaticExpr = Utc::OUTPUT_TYPE;
506
507 fn extract(ob: Borrowed<'_, '_, PyAny>) -> Result<Self, Self::Error> {
508 let py_utc = Utc.into_pyobject(ob.py())?;
509 if ob.eq(py_utc)? {
510 Ok(Utc)
511 } else {
512 Err(PyValueError::new_err("expected datetime.timezone.utc"))
513 }
514 }
515}
516
517#[cfg(feature = "chrono-local")]
518impl<'py> IntoPyObject<'py> for Local {
519 type Target = PyTzInfo;
520 type Output = Borrowed<'static, 'py, Self::Target>;
521 type Error = PyErr;
522
523 #[cfg(all(feature = "experimental-inspect", Py_3_9))]
524 const OUTPUT_TYPE: PyStaticExpr = type_hint_identifier!("zoneinfo", "ZoneInfo");
525
526 #[cfg(all(feature = "experimental-inspect", not(Py_3_9)))]
527 const OUTPUT_TYPE: PyStaticExpr = PyTzInfo::TYPE_HINT;
528
529 fn into_pyobject(self, py: Python<'py>) -> Result<Self::Output, Self::Error> {
530 static LOCAL_TZ: PyOnceLock<Py<PyTzInfo>> = PyOnceLock::new();
531 let tz = LOCAL_TZ
532 .get_or_try_init(py, || {
533 let iana_name = iana_time_zone::get_timezone().map_err(|e| {
534 PyRuntimeError::new_err(format!("Could not get local timezone: {e}"))
535 })?;
536 PyTzInfo::timezone(py, iana_name).map(Bound::unbind)
537 })?
538 .bind_borrowed(py);
539 Ok(tz)
540 }
541}
542
543#[cfg(feature = "chrono-local")]
544impl<'py> IntoPyObject<'py> for &Local {
545 type Target = PyTzInfo;
546 type Output = Borrowed<'static, 'py, Self::Target>;
547 type Error = PyErr;
548
549 #[cfg(feature = "experimental-inspect")]
550 const OUTPUT_TYPE: PyStaticExpr = Local::OUTPUT_TYPE;
551
552 #[inline]
553 fn into_pyobject(self, py: Python<'py>) -> Result<Self::Output, Self::Error> {
554 (*self).into_pyobject(py)
555 }
556}
557
558#[cfg(feature = "chrono-local")]
559impl FromPyObject<'_, '_> for Local {
560 type Error = PyErr;
561
562 #[cfg(feature = "experimental-inspect")]
563 const INPUT_TYPE: PyStaticExpr = Local::OUTPUT_TYPE;
564
565 fn extract(ob: Borrowed<'_, '_, PyAny>) -> PyResult<Local> {
566 let local_tz = Local.into_pyobject(ob.py())?;
567 if ob.eq(local_tz)? {
568 Ok(Local)
569 } else {
570 let name = local_tz.getattr("key")?.cast_into::<PyString>()?;
571 Err(PyValueError::new_err(format!(
572 "expected local timezone {}",
573 name.to_cow()?
574 )))
575 }
576 }
577
578 #[inline]
579 fn as_local_tz(_: crate::conversion::private::Token) -> Option<Self> {
580 Some(Local)
581 }
582}
583
584struct DateArgs {
585 year: i32,
586 month: u8,
587 day: u8,
588}
589
590impl From<&NaiveDate> for DateArgs {
591 fn from(value: &NaiveDate) -> Self {
592 Self {
593 year: value.year(),
594 month: value.month() as u8,
595 day: value.day() as u8,
596 }
597 }
598}
599
600struct TimeArgs {
601 hour: u8,
602 min: u8,
603 sec: u8,
604 micro: u32,
605 truncated_leap_second: bool,
606}
607
608impl From<&NaiveTime> for TimeArgs {
609 fn from(value: &NaiveTime) -> Self {
610 let ns = value.nanosecond();
611 let checked_sub = ns.checked_sub(1_000_000_000);
612 let truncated_leap_second = checked_sub.is_some();
613 let micro = checked_sub.unwrap_or(ns) / 1000;
614 Self {
615 hour: value.hour() as u8,
616 min: value.minute() as u8,
617 sec: value.second() as u8,
618 micro,
619 truncated_leap_second,
620 }
621 }
622}
623
624fn warn_truncated_leap_second(obj: &Bound<'_, PyAny>) {
625 let py = obj.py();
626 if let Err(e) = PyErr::warn(
627 py,
628 &py.get_type::<PyUserWarning>(),
629 c"ignored leap-second, `datetime` does not support leap-seconds",
630 0,
631 ) {
632 e.write_unraisable(py, Some(obj))
633 };
634}
635
636#[cfg(not(Py_LIMITED_API))]
637fn py_date_to_naive_date(
638 py_date: impl std::ops::Deref<Target = impl PyDateAccess>,
639) -> PyResult<NaiveDate> {
640 NaiveDate::from_ymd_opt(
641 py_date.get_year(),
642 py_date.get_month().into(),
643 py_date.get_day().into(),
644 )
645 .ok_or_else(|| PyValueError::new_err("invalid or out-of-range date"))
646}
647
648#[cfg(Py_LIMITED_API)]
649fn py_date_to_naive_date(py_date: &Bound<'_, PyAny>) -> PyResult<NaiveDate> {
650 NaiveDate::from_ymd_opt(
651 py_date.getattr(intern!(py_date.py(), "year"))?.extract()?,
652 py_date.getattr(intern!(py_date.py(), "month"))?.extract()?,
653 py_date.getattr(intern!(py_date.py(), "day"))?.extract()?,
654 )
655 .ok_or_else(|| PyValueError::new_err("invalid or out-of-range date"))
656}
657
658#[cfg(not(Py_LIMITED_API))]
659fn py_time_to_naive_time(
660 py_time: impl std::ops::Deref<Target = impl PyTimeAccess>,
661) -> PyResult<NaiveTime> {
662 NaiveTime::from_hms_micro_opt(
663 py_time.get_hour().into(),
664 py_time.get_minute().into(),
665 py_time.get_second().into(),
666 py_time.get_microsecond(),
667 )
668 .ok_or_else(|| PyValueError::new_err("invalid or out-of-range time"))
669}
670
671#[cfg(Py_LIMITED_API)]
672fn py_time_to_naive_time(py_time: &Bound<'_, PyAny>) -> PyResult<NaiveTime> {
673 NaiveTime::from_hms_micro_opt(
674 py_time.getattr(intern!(py_time.py(), "hour"))?.extract()?,
675 py_time
676 .getattr(intern!(py_time.py(), "minute"))?
677 .extract()?,
678 py_time
679 .getattr(intern!(py_time.py(), "second"))?
680 .extract()?,
681 py_time
682 .getattr(intern!(py_time.py(), "microsecond"))?
683 .extract()?,
684 )
685 .ok_or_else(|| PyValueError::new_err("invalid or out-of-range time"))
686}
687
688fn py_datetime_to_datetime_with_timezone<Tz: TimeZone>(
689 dt: &Bound<'_, PyDateTime>,
690 tz: Tz,
691) -> PyResult<DateTime<Tz>> {
692 let naive_dt = NaiveDateTime::new(py_date_to_naive_date(dt)?, py_time_to_naive_time(dt)?);
693 match naive_dt.and_local_timezone(tz) {
694 LocalResult::Single(value) => Ok(value),
695 LocalResult::Ambiguous(earliest, latest) => {
696 #[cfg(not(Py_LIMITED_API))]
697 let fold = dt.get_fold();
698
699 #[cfg(Py_LIMITED_API)]
700 let fold = dt.getattr(intern!(dt.py(), "fold"))?.extract::<usize>()? > 0;
701
702 if fold {
703 Ok(latest)
704 } else {
705 Ok(earliest)
706 }
707 }
708 LocalResult::None => Err(PyValueError::new_err(format!(
709 "The datetime {dt:?} contains an incompatible timezone"
710 ))),
711 }
712}
713
714#[cfg(test)]
715mod tests {
716 use super::*;
717 use crate::{test_utils::assert_warnings, types::PyTuple, BoundObject};
718 use std::{cmp::Ordering, panic};
719
720 #[test]
721 #[cfg(all(Py_3_9, not(target_os = "windows")))]
725 fn test_zoneinfo_is_not_fixed_offset() {
726 use crate::types::any::PyAnyMethods;
727 use crate::types::dict::PyDictMethods;
728
729 Python::attach(|py| {
730 let locals = crate::types::PyDict::new(py);
731 py.run(
732 c"import zoneinfo; zi = zoneinfo.ZoneInfo('Europe/London')",
733 None,
734 Some(&locals),
735 )
736 .unwrap();
737 let result: PyResult<FixedOffset> = locals.get_item("zi").unwrap().unwrap().extract();
738 assert!(result.is_err());
739 let res = result.err().unwrap();
740 let msg = res.value(py).repr().unwrap().to_string();
742 assert_eq!(msg, "TypeError(\"zoneinfo.ZoneInfo(key='Europe/London') is not a fixed offset timezone\")");
743 });
744 }
745
746 #[test]
747 fn test_timezone_aware_to_naive_fails() {
748 Python::attach(|py| {
751 let py_datetime =
752 new_py_datetime_ob(py, "datetime", (2022, 1, 1, 1, 0, 0, 0, python_utc(py)));
753 let res: PyResult<NaiveDateTime> = py_datetime.extract();
755 assert_eq!(
756 res.unwrap_err().value(py).repr().unwrap().to_string(),
757 "TypeError('expected a datetime without tzinfo')"
758 );
759 });
760 }
761
762 #[test]
763 fn test_naive_to_timezone_aware_fails() {
764 Python::attach(|py| {
767 let py_datetime = new_py_datetime_ob(py, "datetime", (2022, 1, 1, 1, 0, 0, 0));
768 let res: PyResult<DateTime<Utc>> = py_datetime.extract();
770 assert_eq!(
771 res.unwrap_err().value(py).repr().unwrap().to_string(),
772 "TypeError('expected a datetime with non-None tzinfo')"
773 );
774
775 let res: PyResult<DateTime<FixedOffset>> = py_datetime.extract();
777 assert_eq!(
778 res.unwrap_err().value(py).repr().unwrap().to_string(),
779 "TypeError('expected a datetime with non-None tzinfo')"
780 );
781 });
782 }
783
784 #[test]
785 fn test_invalid_types_fail() {
786 Python::attach(|py| {
789 let none = py.None().into_bound(py);
790 assert_eq!(
791 none.extract::<Duration>().unwrap_err().to_string(),
792 "TypeError: 'None' is not an instance of 'timedelta'"
793 );
794 assert_eq!(
795 none.extract::<FixedOffset>().unwrap_err().to_string(),
796 "TypeError: 'None' is not an instance of 'tzinfo'"
797 );
798 assert_eq!(
799 none.extract::<Utc>().unwrap_err().to_string(),
800 "ValueError: expected datetime.timezone.utc"
801 );
802 assert_eq!(
803 none.extract::<NaiveTime>().unwrap_err().to_string(),
804 "TypeError: 'None' is not an instance of 'time'"
805 );
806 assert_eq!(
807 none.extract::<NaiveDate>().unwrap_err().to_string(),
808 "TypeError: 'None' is not an instance of 'date'"
809 );
810 assert_eq!(
811 none.extract::<NaiveDateTime>().unwrap_err().to_string(),
812 "TypeError: 'None' is not an instance of 'datetime'"
813 );
814 assert_eq!(
815 none.extract::<DateTime<Utc>>().unwrap_err().to_string(),
816 "TypeError: 'None' is not an instance of 'datetime'"
817 );
818 assert_eq!(
819 none.extract::<DateTime<FixedOffset>>()
820 .unwrap_err()
821 .to_string(),
822 "TypeError: 'None' is not an instance of 'datetime'"
823 );
824 });
825 }
826
827 #[test]
828 fn test_pyo3_timedelta_into_pyobject() {
829 let check = |name: &'static str, delta: Duration, py_days, py_seconds, py_ms| {
832 Python::attach(|py| {
833 let delta = delta.into_pyobject(py).unwrap();
834 let py_delta = new_py_datetime_ob(py, "timedelta", (py_days, py_seconds, py_ms));
835 assert!(
836 delta.eq(&py_delta).unwrap(),
837 "{name}: {delta} != {py_delta}"
838 );
839 });
840 };
841
842 let delta = Duration::days(-1) + Duration::seconds(1) + Duration::microseconds(-10);
843 check("delta normalization", delta, -1, 1, -10);
844
845 let delta = Duration::seconds(-86399999913600); check("delta min value", delta, -999999999, 0, 0);
849
850 let delta = Duration::seconds(86399999999999) + Duration::nanoseconds(999999000); check("delta max value", delta, 999999999, 86399, 999999);
853
854 Python::attach(|py| {
856 #[allow(deprecated)]
858 {
859 assert!(Duration::min_value().into_pyobject(py).is_err());
860 assert!(Duration::max_value().into_pyobject(py).is_err());
861 }
862 });
863 }
864
865 #[test]
866 fn test_pyo3_timedelta_frompyobject() {
867 let check = |name: &'static str, delta: Duration, py_days, py_seconds, py_ms| {
870 Python::attach(|py| {
871 let py_delta = new_py_datetime_ob(py, "timedelta", (py_days, py_seconds, py_ms));
872 let py_delta: Duration = py_delta.extract().unwrap();
873 assert_eq!(py_delta, delta, "{name}: {py_delta} != {delta}");
874 })
875 };
876
877 check(
880 "min py_delta value",
881 Duration::seconds(-86399999913600),
882 -999999999,
883 0,
884 0,
885 );
886 check(
888 "max py_delta value",
889 Duration::seconds(86399999999999) + Duration::microseconds(999999),
890 999999999,
891 86399,
892 999999,
893 );
894
895 Python::attach(|py| {
898 let low_days: i32 = -1000000000;
899 assert!(panic::catch_unwind(|| Duration::days(low_days as i64)).is_ok());
901 assert!(panic::catch_unwind(|| {
903 let py_delta = new_py_datetime_ob(py, "timedelta", (low_days, 0, 0));
904 if let Ok(_duration) = py_delta.extract::<Duration>() {
905 }
907 })
908 .is_err());
909
910 let high_days: i32 = 1000000000;
911 assert!(panic::catch_unwind(|| Duration::days(high_days as i64)).is_ok());
913 assert!(panic::catch_unwind(|| {
915 let py_delta = new_py_datetime_ob(py, "timedelta", (high_days, 0, 0));
916 if let Ok(_duration) = py_delta.extract::<Duration>() {
917 }
919 })
920 .is_err());
921 });
922 }
923
924 #[test]
925 fn test_pyo3_date_into_pyobject() {
926 let eq_ymd = |name: &'static str, year, month, day| {
927 Python::attach(|py| {
928 let date = NaiveDate::from_ymd_opt(year, month, day)
929 .unwrap()
930 .into_pyobject(py)
931 .unwrap();
932 let py_date = new_py_datetime_ob(py, "date", (year, month, day));
933 assert_eq!(
934 date.compare(&py_date).unwrap(),
935 Ordering::Equal,
936 "{name}: {date} != {py_date}"
937 );
938 })
939 };
940
941 eq_ymd("past date", 2012, 2, 29);
942 eq_ymd("min date", 1, 1, 1);
943 eq_ymd("future date", 3000, 6, 5);
944 eq_ymd("max date", 9999, 12, 31);
945 }
946
947 #[test]
948 fn test_pyo3_date_frompyobject() {
949 let eq_ymd = |name: &'static str, year, month, day| {
950 Python::attach(|py| {
951 let py_date = new_py_datetime_ob(py, "date", (year, month, day));
952 let py_date: NaiveDate = py_date.extract().unwrap();
953 let date = NaiveDate::from_ymd_opt(year, month, day).unwrap();
954 assert_eq!(py_date, date, "{name}: {date} != {py_date}");
955 })
956 };
957
958 eq_ymd("past date", 2012, 2, 29);
959 eq_ymd("min date", 1, 1, 1);
960 eq_ymd("future date", 3000, 6, 5);
961 eq_ymd("max date", 9999, 12, 31);
962 }
963
964 #[test]
965 fn test_pyo3_datetime_into_pyobject_utc() {
966 Python::attach(|py| {
967 let check_utc =
968 |name: &'static str, year, month, day, hour, minute, second, ms, py_ms| {
969 let datetime = NaiveDate::from_ymd_opt(year, month, day)
970 .unwrap()
971 .and_hms_micro_opt(hour, minute, second, ms)
972 .unwrap()
973 .and_utc();
974 let datetime = datetime.into_pyobject(py).unwrap();
975 let py_datetime = new_py_datetime_ob(
976 py,
977 "datetime",
978 (
979 year,
980 month,
981 day,
982 hour,
983 minute,
984 second,
985 py_ms,
986 python_utc(py),
987 ),
988 );
989 assert_eq!(
990 datetime.compare(&py_datetime).unwrap(),
991 Ordering::Equal,
992 "{name}: {datetime} != {py_datetime}"
993 );
994 };
995
996 check_utc("regular", 2014, 5, 6, 7, 8, 9, 999_999, 999_999);
997
998 assert_warnings!(
999 py,
1000 check_utc("leap second", 2014, 5, 6, 7, 8, 59, 1_999_999, 999_999),
1001 [(
1002 PyUserWarning,
1003 "ignored leap-second, `datetime` does not support leap-seconds"
1004 )]
1005 );
1006 })
1007 }
1008
1009 #[test]
1010 fn test_pyo3_datetime_into_pyobject_fixed_offset() {
1011 Python::attach(|py| {
1012 let check_fixed_offset =
1013 |name: &'static str, year, month, day, hour, minute, second, ms, py_ms| {
1014 let offset = FixedOffset::east_opt(3600).unwrap();
1015 let datetime = NaiveDate::from_ymd_opt(year, month, day)
1016 .unwrap()
1017 .and_hms_micro_opt(hour, minute, second, ms)
1018 .unwrap()
1019 .and_local_timezone(offset)
1020 .unwrap();
1021 let datetime = datetime.into_pyobject(py).unwrap();
1022 let py_tz = offset.into_pyobject(py).unwrap();
1023 let py_datetime = new_py_datetime_ob(
1024 py,
1025 "datetime",
1026 (year, month, day, hour, minute, second, py_ms, py_tz),
1027 );
1028 assert_eq!(
1029 datetime.compare(&py_datetime).unwrap(),
1030 Ordering::Equal,
1031 "{name}: {datetime} != {py_datetime}"
1032 );
1033 };
1034
1035 check_fixed_offset("regular", 2014, 5, 6, 7, 8, 9, 999_999, 999_999);
1036
1037 assert_warnings!(
1038 py,
1039 check_fixed_offset("leap second", 2014, 5, 6, 7, 8, 59, 1_999_999, 999_999),
1040 [(
1041 PyUserWarning,
1042 "ignored leap-second, `datetime` does not support leap-seconds"
1043 )]
1044 );
1045 })
1046 }
1047
1048 #[test]
1049 #[cfg(all(Py_3_9, feature = "chrono-tz", not(windows)))]
1050 fn test_pyo3_datetime_into_pyobject_tz() {
1051 Python::attach(|py| {
1052 let datetime = NaiveDate::from_ymd_opt(2024, 12, 11)
1053 .unwrap()
1054 .and_hms_opt(23, 3, 13)
1055 .unwrap()
1056 .and_local_timezone(chrono_tz::Tz::Europe__London)
1057 .unwrap();
1058 let datetime = datetime.into_pyobject(py).unwrap();
1059 let py_datetime = new_py_datetime_ob(
1060 py,
1061 "datetime",
1062 (
1063 2024,
1064 12,
1065 11,
1066 23,
1067 3,
1068 13,
1069 0,
1070 python_zoneinfo(py, "Europe/London"),
1071 ),
1072 );
1073 assert_eq!(datetime.compare(&py_datetime).unwrap(), Ordering::Equal);
1074 })
1075 }
1076
1077 #[test]
1078 fn test_pyo3_datetime_frompyobject_utc() {
1079 Python::attach(|py| {
1080 let year = 2014;
1081 let month = 5;
1082 let day = 6;
1083 let hour = 7;
1084 let minute = 8;
1085 let second = 9;
1086 let micro = 999_999;
1087 let tz_utc = PyTzInfo::utc(py).unwrap();
1088 let py_datetime = new_py_datetime_ob(
1089 py,
1090 "datetime",
1091 (year, month, day, hour, minute, second, micro, tz_utc),
1092 );
1093 let py_datetime: DateTime<Utc> = py_datetime.extract().unwrap();
1094 let datetime = NaiveDate::from_ymd_opt(year, month, day)
1095 .unwrap()
1096 .and_hms_micro_opt(hour, minute, second, micro)
1097 .unwrap()
1098 .and_utc();
1099 assert_eq!(py_datetime, datetime,);
1100 })
1101 }
1102
1103 #[test]
1104 #[cfg(feature = "chrono-local")]
1105 fn test_pyo3_naive_datetime_frompyobject_local() {
1106 Python::attach(|py| {
1107 let year = 2014;
1108 let month = 5;
1109 let day = 6;
1110 let hour = 7;
1111 let minute = 8;
1112 let second = 9;
1113 let micro = 999_999;
1114 let py_datetime = new_py_datetime_ob(
1115 py,
1116 "datetime",
1117 (year, month, day, hour, minute, second, micro),
1118 );
1119 let py_datetime: DateTime<Local> = py_datetime.extract().unwrap();
1120 let expected_datetime = NaiveDate::from_ymd_opt(year, month, day)
1121 .unwrap()
1122 .and_hms_micro_opt(hour, minute, second, micro)
1123 .unwrap()
1124 .and_local_timezone(Local)
1125 .unwrap();
1126 assert_eq!(py_datetime, expected_datetime);
1127 })
1128 }
1129
1130 #[test]
1131 fn test_pyo3_datetime_frompyobject_fixed_offset() {
1132 Python::attach(|py| {
1133 let year = 2014;
1134 let month = 5;
1135 let day = 6;
1136 let hour = 7;
1137 let minute = 8;
1138 let second = 9;
1139 let micro = 999_999;
1140 let offset = FixedOffset::east_opt(3600).unwrap();
1141 let py_tz = offset.into_pyobject(py).unwrap();
1142 let py_datetime = new_py_datetime_ob(
1143 py,
1144 "datetime",
1145 (year, month, day, hour, minute, second, micro, py_tz),
1146 );
1147 let datetime_from_py: DateTime<FixedOffset> = py_datetime.extract().unwrap();
1148 let datetime = NaiveDate::from_ymd_opt(year, month, day)
1149 .unwrap()
1150 .and_hms_micro_opt(hour, minute, second, micro)
1151 .unwrap();
1152 let datetime = datetime.and_local_timezone(offset).unwrap();
1153
1154 assert_eq!(datetime_from_py, datetime);
1155 assert!(
1156 py_datetime.extract::<DateTime<Utc>>().is_err(),
1157 "Extracting Utc from nonzero FixedOffset timezone will fail"
1158 );
1159
1160 let utc = python_utc(py);
1161 let py_datetime_utc = new_py_datetime_ob(
1162 py,
1163 "datetime",
1164 (year, month, day, hour, minute, second, micro, utc),
1165 );
1166 assert!(
1167 py_datetime_utc.extract::<DateTime<FixedOffset>>().is_ok(),
1168 "Extracting FixedOffset from Utc timezone will succeed"
1169 );
1170 })
1171 }
1172
1173 #[test]
1174 fn test_pyo3_offset_fixed_into_pyobject() {
1175 Python::attach(|py| {
1176 let offset = FixedOffset::east_opt(3600)
1178 .unwrap()
1179 .into_pyobject(py)
1180 .unwrap();
1181 let td = new_py_datetime_ob(py, "timedelta", (0, 3600, 0));
1183 let py_timedelta = new_py_datetime_ob(py, "timezone", (td,));
1184 assert!(offset.eq(py_timedelta).unwrap());
1186
1187 let offset = FixedOffset::east_opt(-3600)
1189 .unwrap()
1190 .into_pyobject(py)
1191 .unwrap();
1192 let td = new_py_datetime_ob(py, "timedelta", (0, -3600, 0));
1193 let py_timedelta = new_py_datetime_ob(py, "timezone", (td,));
1194 assert!(offset.eq(py_timedelta).unwrap());
1195 })
1196 }
1197
1198 #[test]
1199 fn test_pyo3_offset_fixed_frompyobject() {
1200 Python::attach(|py| {
1201 let py_timedelta = new_py_datetime_ob(py, "timedelta", (0, 3600, 0));
1202 let py_tzinfo = new_py_datetime_ob(py, "timezone", (py_timedelta,));
1203 let offset: FixedOffset = py_tzinfo.extract().unwrap();
1204 assert_eq!(FixedOffset::east_opt(3600).unwrap(), offset);
1205 })
1206 }
1207
1208 #[test]
1209 fn test_pyo3_offset_utc_into_pyobject() {
1210 Python::attach(|py| {
1211 let utc = Utc.into_pyobject(py).unwrap();
1212 let py_utc = python_utc(py);
1213 assert!(utc.is(&py_utc));
1214 })
1215 }
1216
1217 #[test]
1218 fn test_pyo3_offset_utc_frompyobject() {
1219 Python::attach(|py| {
1220 let py_utc = python_utc(py);
1221 let py_utc: Utc = py_utc.extract().unwrap();
1222 assert_eq!(Utc, py_utc);
1223
1224 let py_timedelta = new_py_datetime_ob(py, "timedelta", (0, 0, 0));
1225 let py_timezone_utc = new_py_datetime_ob(py, "timezone", (py_timedelta,));
1226 let py_timezone_utc: Utc = py_timezone_utc.extract().unwrap();
1227 assert_eq!(Utc, py_timezone_utc);
1228
1229 let py_timedelta = new_py_datetime_ob(py, "timedelta", (0, 3600, 0));
1230 let py_timezone = new_py_datetime_ob(py, "timezone", (py_timedelta,));
1231 assert!(py_timezone.extract::<Utc>().is_err());
1232 })
1233 }
1234
1235 #[test]
1236 fn test_pyo3_time_into_pyobject() {
1237 Python::attach(|py| {
1238 let check_time = |name: &'static str, hour, minute, second, ms, py_ms| {
1239 let time = NaiveTime::from_hms_micro_opt(hour, minute, second, ms)
1240 .unwrap()
1241 .into_pyobject(py)
1242 .unwrap();
1243 let py_time = new_py_datetime_ob(py, "time", (hour, minute, second, py_ms));
1244 assert!(time.eq(&py_time).unwrap(), "{name}: {time} != {py_time}");
1245 };
1246
1247 check_time("regular", 3, 5, 7, 999_999, 999_999);
1248
1249 assert_warnings!(
1250 py,
1251 check_time("leap second", 3, 5, 59, 1_999_999, 999_999),
1252 [(
1253 PyUserWarning,
1254 "ignored leap-second, `datetime` does not support leap-seconds"
1255 )]
1256 );
1257 })
1258 }
1259
1260 #[test]
1261 fn test_pyo3_time_frompyobject() {
1262 let hour = 3;
1263 let minute = 5;
1264 let second = 7;
1265 let micro = 999_999;
1266 Python::attach(|py| {
1267 let py_time = new_py_datetime_ob(py, "time", (hour, minute, second, micro));
1268 let py_time: NaiveTime = py_time.extract().unwrap();
1269 let time = NaiveTime::from_hms_micro_opt(hour, minute, second, micro).unwrap();
1270 assert_eq!(py_time, time);
1271 })
1272 }
1273
1274 fn new_py_datetime_ob<'py, A>(py: Python<'py>, name: &str, args: A) -> Bound<'py, PyAny>
1275 where
1276 A: IntoPyObject<'py, Target = PyTuple>,
1277 {
1278 py.import("datetime")
1279 .unwrap()
1280 .getattr(name)
1281 .unwrap()
1282 .call1(
1283 args.into_pyobject(py)
1284 .map_err(Into::into)
1285 .unwrap()
1286 .into_bound(),
1287 )
1288 .unwrap()
1289 }
1290
1291 fn python_utc(py: Python<'_>) -> Bound<'_, PyAny> {
1292 py.import("datetime")
1293 .unwrap()
1294 .getattr("timezone")
1295 .unwrap()
1296 .getattr("utc")
1297 .unwrap()
1298 }
1299
1300 #[cfg(all(Py_3_9, feature = "chrono-tz", not(windows)))]
1301 fn python_zoneinfo<'py>(py: Python<'py>, timezone: &str) -> Bound<'py, PyAny> {
1302 py.import("zoneinfo")
1303 .unwrap()
1304 .getattr("ZoneInfo")
1305 .unwrap()
1306 .call1((timezone,))
1307 .unwrap()
1308 }
1309
1310 #[cfg(not(any(target_arch = "wasm32")))]
1311 mod proptests {
1312 use super::*;
1313 use crate::test_utils::CatchWarnings;
1314 use crate::types::IntoPyDict;
1315 use proptest::prelude::*;
1316 use std::ffi::CString;
1317
1318 proptest! {
1319
1320 #[test]
1322 fn test_pyo3_offset_fixed_frompyobject_created_in_python(timestamp in 0..(i32::MAX as i64), timedelta in -86399i32..=86399i32) {
1323 Python::attach(|py| {
1324
1325 let globals = [("datetime", py.import("datetime").unwrap())].into_py_dict(py).unwrap();
1326 let code = format!("datetime.datetime.fromtimestamp({timestamp}).replace(tzinfo=datetime.timezone(datetime.timedelta(seconds={timedelta})))");
1327 let t = py.eval(&CString::new(code).unwrap(), Some(&globals), None).unwrap();
1328
1329 let py_iso_str = t.call_method0("isoformat").unwrap();
1331
1332 let t = t.extract::<DateTime<FixedOffset>>().unwrap();
1334 let rust_iso_str = if timedelta % 60 == 0 {
1336 t.format("%Y-%m-%dT%H:%M:%S%:z").to_string()
1337 } else {
1338 t.format("%Y-%m-%dT%H:%M:%S%::z").to_string()
1339 };
1340
1341 assert_eq!(py_iso_str.to_string(), rust_iso_str);
1343 })
1344 }
1345
1346 #[test]
1347 fn test_duration_roundtrip(days in -999999999i64..=999999999i64) {
1348 Python::attach(|py| {
1351 let dur = Duration::days(days);
1352 let py_delta = dur.into_pyobject(py).unwrap();
1353 let roundtripped: Duration = py_delta.extract().expect("Round trip");
1354 assert_eq!(dur, roundtripped);
1355 })
1356 }
1357
1358 #[test]
1359 fn test_fixed_offset_roundtrip(secs in -86399i32..=86399i32) {
1360 Python::attach(|py| {
1361 let offset = FixedOffset::east_opt(secs).unwrap();
1362 let py_offset = offset.into_pyobject(py).unwrap();
1363 let roundtripped: FixedOffset = py_offset.extract().expect("Round trip");
1364 assert_eq!(offset, roundtripped);
1365 })
1366 }
1367
1368 #[test]
1369 fn test_naive_date_roundtrip(
1370 year in 1i32..=9999i32,
1371 month in 1u32..=12u32,
1372 day in 1u32..=31u32
1373 ) {
1374 Python::attach(|py| {
1377 if let Some(date) = NaiveDate::from_ymd_opt(year, month, day) {
1380 let py_date = date.into_pyobject(py).unwrap();
1381 let roundtripped: NaiveDate = py_date.extract().expect("Round trip");
1382 assert_eq!(date, roundtripped);
1383 }
1384 })
1385 }
1386
1387 #[test]
1388 fn test_naive_time_roundtrip(
1389 hour in 0u32..=23u32,
1390 min in 0u32..=59u32,
1391 sec in 0u32..=59u32,
1392 micro in 0u32..=1_999_999u32
1393 ) {
1394 Python::attach(|py| {
1399 if let Some(time) = NaiveTime::from_hms_micro_opt(hour, min, sec, micro) {
1400 let py_time = CatchWarnings::enter(py, |_| time.into_pyobject(py)).unwrap();
1402 let roundtripped: NaiveTime = py_time.extract().expect("Round trip");
1403 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);
1405 assert_eq!(expected_roundtrip_time, roundtripped);
1406 }
1407 })
1408 }
1409
1410 #[test]
1411 fn test_naive_datetime_roundtrip(
1412 year in 1i32..=9999i32,
1413 month in 1u32..=12u32,
1414 day in 1u32..=31u32,
1415 hour in 0u32..=24u32,
1416 min in 0u32..=60u32,
1417 sec in 0u32..=60u32,
1418 micro in 0u32..=999_999u32
1419 ) {
1420 Python::attach(|py| {
1421 let date_opt = NaiveDate::from_ymd_opt(year, month, day);
1422 let time_opt = NaiveTime::from_hms_micro_opt(hour, min, sec, micro);
1423 if let (Some(date), Some(time)) = (date_opt, time_opt) {
1424 let dt = NaiveDateTime::new(date, time);
1425 let pydt = dt.into_pyobject(py).unwrap();
1426 let roundtripped: NaiveDateTime = pydt.extract().expect("Round trip");
1427 assert_eq!(dt, roundtripped);
1428 }
1429 })
1430 }
1431
1432 #[test]
1433 fn test_utc_datetime_roundtrip(
1434 year in 1i32..=9999i32,
1435 month in 1u32..=12u32,
1436 day in 1u32..=31u32,
1437 hour in 0u32..=23u32,
1438 min in 0u32..=59u32,
1439 sec in 0u32..=59u32,
1440 micro in 0u32..=1_999_999u32
1441 ) {
1442 Python::attach(|py| {
1443 let date_opt = NaiveDate::from_ymd_opt(year, month, day);
1444 let time_opt = NaiveTime::from_hms_micro_opt(hour, min, sec, micro);
1445 if let (Some(date), Some(time)) = (date_opt, time_opt) {
1446 let dt: DateTime<Utc> = NaiveDateTime::new(date, time).and_utc();
1447 let py_dt = CatchWarnings::enter(py, |_| dt.into_pyobject(py)).unwrap();
1449 let roundtripped: DateTime<Utc> = py_dt.extract().expect("Round trip");
1450 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);
1452 let expected_roundtrip_dt: DateTime<Utc> = NaiveDateTime::new(date, expected_roundtrip_time).and_utc();
1453 assert_eq!(expected_roundtrip_dt, roundtripped);
1454 }
1455 })
1456 }
1457
1458 #[test]
1459 fn test_fixed_offset_datetime_roundtrip(
1460 year in 1i32..=9999i32,
1461 month in 1u32..=12u32,
1462 day in 1u32..=31u32,
1463 hour in 0u32..=23u32,
1464 min in 0u32..=59u32,
1465 sec in 0u32..=59u32,
1466 micro in 0u32..=1_999_999u32,
1467 offset_secs in -86399i32..=86399i32
1468 ) {
1469 Python::attach(|py| {
1470 let date_opt = NaiveDate::from_ymd_opt(year, month, day);
1471 let time_opt = NaiveTime::from_hms_micro_opt(hour, min, sec, micro);
1472 let offset = FixedOffset::east_opt(offset_secs).unwrap();
1473 if let (Some(date), Some(time)) = (date_opt, time_opt) {
1474 let dt: DateTime<FixedOffset> = NaiveDateTime::new(date, time).and_local_timezone(offset).unwrap();
1475 let py_dt = CatchWarnings::enter(py, |_| dt.into_pyobject(py)).unwrap();
1477 let roundtripped: DateTime<FixedOffset> = py_dt.extract().expect("Round trip");
1478 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);
1480 let expected_roundtrip_dt: DateTime<FixedOffset> = NaiveDateTime::new(date, expected_roundtrip_time).and_local_timezone(offset).unwrap();
1481 assert_eq!(expected_roundtrip_dt, roundtripped);
1482 }
1483 })
1484 }
1485
1486 #[test]
1487 #[cfg(all(feature = "chrono-local", not(target_os = "windows")))]
1488 fn test_local_datetime_roundtrip(
1489 year in 1i32..=9999i32,
1490 month in 1u32..=12u32,
1491 day in 1u32..=31u32,
1492 hour in 0u32..=23u32,
1493 min in 0u32..=59u32,
1494 sec in 0u32..=59u32,
1495 micro in 0u32..=1_999_999u32,
1496 ) {
1497 Python::attach(|py| {
1498 let date_opt = NaiveDate::from_ymd_opt(year, month, day);
1499 let time_opt = NaiveTime::from_hms_micro_opt(hour, min, sec, micro);
1500 if let (Some(date), Some(time)) = (date_opt, time_opt) {
1501 let dts = match NaiveDateTime::new(date, time).and_local_timezone(Local) {
1502 LocalResult::None => return,
1503 LocalResult::Single(dt) => [Some((dt, false)), None],
1504 LocalResult::Ambiguous(dt1, dt2) => [Some((dt1, false)), Some((dt2, true))],
1505 };
1506 for (dt, fold) in dts.iter().filter_map(|input| *input) {
1507 let py_dt = CatchWarnings::enter(py, |_| dt.into_pyobject(py)).unwrap();
1509 let roundtripped: DateTime<Local> = py_dt.extract().expect("Round trip");
1510 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);
1512 let expected_roundtrip_dt: DateTime<Local> = if fold {
1513 NaiveDateTime::new(date, expected_roundtrip_time).and_local_timezone(Local).latest()
1514 } else {
1515 NaiveDateTime::new(date, expected_roundtrip_time).and_local_timezone(Local).earliest()
1516 }.unwrap();
1517 assert_eq!(expected_roundtrip_dt, roundtripped);
1518 }
1519 }
1520 })
1521 }
1522 }
1523 }
1524}