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