1#![cfg(feature = "jiff-02")]
2
3#![doc = concat!("pyo3 = { version = \"", env!("CARGO_PKG_VERSION"), "\", features = [\"jiff-02\"] }")]
14use crate::exceptions::{PyTypeError, PyValueError};
49#[cfg(feature = "experimental-inspect")]
50use crate::inspect::PyStaticExpr;
51use crate::types::{PyAnyMethods, PyNone};
52use crate::types::{PyDate, PyDateTime, PyDelta, PyTime, PyTzInfo, PyTzInfoAccess};
53#[cfg(not(Py_LIMITED_API))]
54use crate::types::{PyDateAccess, PyDeltaAccess, PyTimeAccess};
55use crate::{intern, Borrowed, Bound, FromPyObject, IntoPyObject, PyAny, PyErr, PyResult, Python};
56#[cfg(feature = "experimental-inspect")]
57use crate::{type_hint_identifier, PyTypeInfo};
58use jiff::civil::{Date, DateTime, ISOWeekDate, Time};
59use jiff::tz::{Offset, TimeZone};
60use jiff::{SignedDuration, Span, Timestamp, Zoned};
61#[cfg(feature = "jiff-02")]
62use jiff_02 as jiff;
63use std::borrow::Cow;
64
65fn datetime_to_pydatetime<'py>(
66 py: Python<'py>,
67 datetime: DateTime,
68 fold: bool,
69 timezone: Option<&TimeZone>,
70) -> PyResult<Bound<'py, PyDateTime>> {
71 PyDateTime::new_with_fold(
72 py,
73 datetime.year().into(),
74 datetime.month().try_into()?,
75 datetime.day().try_into()?,
76 datetime.hour().try_into()?,
77 datetime.minute().try_into()?,
78 datetime.second().try_into()?,
79 (datetime.subsec_nanosecond() / 1000).try_into()?,
80 timezone
81 .map(|tz| tz.into_pyobject(py))
82 .transpose()?
83 .as_ref(),
84 fold,
85 )
86}
87
88#[cfg(not(Py_LIMITED_API))]
89fn pytime_to_time(time: &impl PyTimeAccess) -> PyResult<Time> {
90 Ok(Time::new(
91 time.get_hour().try_into()?,
92 time.get_minute().try_into()?,
93 time.get_second().try_into()?,
94 (time.get_microsecond() * 1000).try_into()?,
95 )?)
96}
97
98#[cfg(Py_LIMITED_API)]
99fn pytime_to_time(time: &Bound<'_, PyAny>) -> PyResult<Time> {
100 let py = time.py();
101 Ok(Time::new(
102 time.getattr(intern!(py, "hour"))?.extract()?,
103 time.getattr(intern!(py, "minute"))?.extract()?,
104 time.getattr(intern!(py, "second"))?.extract()?,
105 time.getattr(intern!(py, "microsecond"))?.extract::<i32>()? * 1000,
106 )?)
107}
108
109impl<'py> IntoPyObject<'py> for Timestamp {
110 type Target = PyDateTime;
111 type Output = Bound<'py, Self::Target>;
112 type Error = PyErr;
113
114 #[cfg(feature = "experimental-inspect")]
115 const OUTPUT_TYPE: PyStaticExpr = Zoned::OUTPUT_TYPE;
116
117 fn into_pyobject(self, py: Python<'py>) -> Result<Self::Output, Self::Error> {
118 self.to_zoned(TimeZone::UTC).into_pyobject(py)
119 }
120}
121
122impl<'py> IntoPyObject<'py> for &Timestamp {
123 type Target = PyDateTime;
124 type Output = Bound<'py, Self::Target>;
125 type Error = PyErr;
126
127 #[cfg(feature = "experimental-inspect")]
128 const OUTPUT_TYPE: PyStaticExpr = Timestamp::OUTPUT_TYPE;
129
130 fn into_pyobject(self, py: Python<'py>) -> Result<Self::Output, Self::Error> {
131 (*self).into_pyobject(py)
132 }
133}
134
135impl<'a, 'py> FromPyObject<'a, 'py> for Timestamp {
136 type Error = <Zoned as FromPyObject<'a, 'py>>::Error;
137
138 #[cfg(feature = "experimental-inspect")]
139 const INPUT_TYPE: PyStaticExpr = Zoned::INPUT_TYPE;
140
141 fn extract(ob: Borrowed<'_, 'py, PyAny>) -> Result<Self, Self::Error> {
142 let zoned = ob.extract::<Zoned>()?;
143 Ok(zoned.timestamp())
144 }
145}
146
147impl<'py> IntoPyObject<'py> for Date {
148 type Target = PyDate;
149 type Output = Bound<'py, Self::Target>;
150 type Error = PyErr;
151
152 #[cfg(feature = "experimental-inspect")]
153 const OUTPUT_TYPE: PyStaticExpr = PyDate::TYPE_HINT;
154
155 fn into_pyobject(self, py: Python<'py>) -> Result<Self::Output, Self::Error> {
156 PyDate::new(
157 py,
158 self.year().into(),
159 self.month().try_into()?,
160 self.day().try_into()?,
161 )
162 }
163}
164
165impl<'py> IntoPyObject<'py> for &Date {
166 type Target = PyDate;
167 type Output = Bound<'py, Self::Target>;
168 type Error = PyErr;
169
170 #[cfg(feature = "experimental-inspect")]
171 const OUTPUT_TYPE: PyStaticExpr = Date::OUTPUT_TYPE;
172
173 fn into_pyobject(self, py: Python<'py>) -> Result<Self::Output, Self::Error> {
174 (*self).into_pyobject(py)
175 }
176}
177
178impl<'py> FromPyObject<'_, 'py> for Date {
179 type Error = PyErr;
180
181 #[cfg(feature = "experimental-inspect")]
182 const INPUT_TYPE: PyStaticExpr = PyDate::TYPE_HINT;
183
184 fn extract(ob: Borrowed<'_, 'py, PyAny>) -> PyResult<Self> {
185 let date = ob.cast::<PyDate>()?;
186
187 #[cfg(not(Py_LIMITED_API))]
188 {
189 Ok(Date::new(
190 date.get_year().try_into()?,
191 date.get_month().try_into()?,
192 date.get_day().try_into()?,
193 )?)
194 }
195
196 #[cfg(Py_LIMITED_API)]
197 {
198 let py = date.py();
199 Ok(Date::new(
200 date.getattr(intern!(py, "year"))?.extract()?,
201 date.getattr(intern!(py, "month"))?.extract()?,
202 date.getattr(intern!(py, "day"))?.extract()?,
203 )?)
204 }
205 }
206}
207
208impl<'py> IntoPyObject<'py> for Time {
209 type Target = PyTime;
210 type Output = Bound<'py, Self::Target>;
211 type Error = PyErr;
212
213 #[cfg(feature = "experimental-inspect")]
214 const OUTPUT_TYPE: PyStaticExpr = PyTime::TYPE_HINT;
215
216 fn into_pyobject(self, py: Python<'py>) -> Result<Self::Output, Self::Error> {
217 PyTime::new(
218 py,
219 self.hour().try_into()?,
220 self.minute().try_into()?,
221 self.second().try_into()?,
222 (self.subsec_nanosecond() / 1000).try_into()?,
223 None,
224 )
225 }
226}
227
228impl<'py> IntoPyObject<'py> for &Time {
229 type Target = PyTime;
230 type Output = Bound<'py, Self::Target>;
231 type Error = PyErr;
232
233 #[cfg(feature = "experimental-inspect")]
234 const OUTPUT_TYPE: PyStaticExpr = Time::OUTPUT_TYPE;
235
236 fn into_pyobject(self, py: Python<'py>) -> Result<Self::Output, Self::Error> {
237 (*self).into_pyobject(py)
238 }
239}
240
241impl<'py> FromPyObject<'_, 'py> for Time {
242 type Error = PyErr;
243
244 #[cfg(feature = "experimental-inspect")]
245 const INPUT_TYPE: PyStaticExpr = PyTime::TYPE_HINT;
246
247 fn extract(ob: Borrowed<'_, 'py, PyAny>) -> PyResult<Self> {
248 let ob = ob.cast::<PyTime>()?;
249 #[allow(clippy::explicit_auto_deref)]
250 pytime_to_time(&*ob)
251 }
252}
253
254impl<'py> IntoPyObject<'py> for DateTime {
255 type Target = PyDateTime;
256 type Output = Bound<'py, Self::Target>;
257 type Error = PyErr;
258
259 #[cfg(feature = "experimental-inspect")]
260 const OUTPUT_TYPE: PyStaticExpr = PyDateTime::TYPE_HINT;
261
262 fn into_pyobject(self, py: Python<'py>) -> Result<Self::Output, Self::Error> {
263 datetime_to_pydatetime(py, self, false, None)
264 }
265}
266
267impl<'py> IntoPyObject<'py> for &DateTime {
268 type Target = PyDateTime;
269 type Output = Bound<'py, Self::Target>;
270 type Error = PyErr;
271
272 #[cfg(feature = "experimental-inspect")]
273 const OUTPUT_TYPE: PyStaticExpr = DateTime::OUTPUT_TYPE;
274
275 fn into_pyobject(self, py: Python<'py>) -> Result<Self::Output, Self::Error> {
276 (*self).into_pyobject(py)
277 }
278}
279
280impl<'py> FromPyObject<'_, 'py> for DateTime {
281 type Error = PyErr;
282
283 #[cfg(feature = "experimental-inspect")]
284 const INPUT_TYPE: PyStaticExpr = PyDateTime::TYPE_HINT;
285
286 fn extract(dt: Borrowed<'_, 'py, PyAny>) -> PyResult<Self> {
287 let dt = dt.cast::<PyDateTime>()?;
288 let has_tzinfo = dt.get_tzinfo().is_some();
289
290 if has_tzinfo {
291 return Err(PyTypeError::new_err("expected a datetime without tzinfo"));
292 }
293
294 #[allow(clippy::explicit_auto_deref)]
295 Ok(DateTime::from_parts(dt.extract()?, pytime_to_time(&*dt)?))
296 }
297}
298
299impl<'py> IntoPyObject<'py> for Zoned {
300 type Target = PyDateTime;
301 type Output = Bound<'py, Self::Target>;
302 type Error = PyErr;
303
304 #[cfg(feature = "experimental-inspect")]
305 const OUTPUT_TYPE: PyStaticExpr = <&Self>::OUTPUT_TYPE;
306
307 fn into_pyobject(self, py: Python<'py>) -> Result<Self::Output, Self::Error> {
308 (&self).into_pyobject(py)
309 }
310}
311impl<'py> IntoPyObject<'py> for &Zoned {
312 type Target = PyDateTime;
313 type Output = Bound<'py, Self::Target>;
314 type Error = PyErr;
315
316 #[cfg(feature = "experimental-inspect")]
317 const OUTPUT_TYPE: PyStaticExpr = PyDateTime::TYPE_HINT;
318
319 fn into_pyobject(self, py: Python<'py>) -> Result<Self::Output, Self::Error> {
320 fn fold(zoned: &Zoned) -> Option<bool> {
321 let prev = zoned.time_zone().preceding(zoned.timestamp()).next()?;
322 let next = zoned.time_zone().following(prev.timestamp()).next()?;
323 let start_of_current_offset = if next.timestamp() == zoned.timestamp() {
324 next.timestamp()
325 } else {
326 prev.timestamp()
327 };
328 Some(zoned.timestamp() + (zoned.offset() - prev.offset()) <= start_of_current_offset)
329 }
330
331 datetime_to_pydatetime(
332 py,
333 self.datetime(),
334 fold(self).unwrap_or(false),
335 Some(self.time_zone()),
336 )
337 }
338}
339
340impl<'py> FromPyObject<'_, 'py> for Zoned {
341 type Error = PyErr;
342
343 #[cfg(feature = "experimental-inspect")]
344 const INPUT_TYPE: PyStaticExpr = PyDateTime::TYPE_HINT;
345
346 fn extract(dt: Borrowed<'_, 'py, PyAny>) -> PyResult<Self> {
347 let dt = dt.cast::<PyDateTime>()?;
348
349 let tz = dt
350 .get_tzinfo()
351 .map(|tz| tz.extract::<TimeZone>())
352 .unwrap_or_else(|| {
353 Err(PyTypeError::new_err(
354 "expected a datetime with non-None tzinfo",
355 ))
356 })?;
357 #[allow(clippy::explicit_auto_deref)]
358 let datetime = DateTime::from_parts(dt.extract()?, pytime_to_time(&*dt)?);
359 let zoned = tz.into_ambiguous_zoned(datetime);
360
361 #[cfg(not(Py_LIMITED_API))]
362 let fold = dt.get_fold();
363
364 #[cfg(Py_LIMITED_API)]
365 let fold = dt.getattr(intern!(dt.py(), "fold"))?.extract::<usize>()? > 0;
366
367 if fold {
368 Ok(zoned.later()?)
369 } else {
370 Ok(zoned.earlier()?)
371 }
372 }
373}
374
375impl<'py> IntoPyObject<'py> for TimeZone {
376 type Target = PyTzInfo;
377 type Output = Bound<'py, Self::Target>;
378 type Error = PyErr;
379
380 #[cfg(feature = "experimental-inspect")]
381 const OUTPUT_TYPE: PyStaticExpr = <&Self>::OUTPUT_TYPE;
382
383 fn into_pyobject(self, py: Python<'py>) -> Result<Self::Output, Self::Error> {
384 (&self).into_pyobject(py)
385 }
386}
387
388impl<'py> IntoPyObject<'py> for &TimeZone {
389 type Target = PyTzInfo;
390 type Output = Bound<'py, Self::Target>;
391 type Error = PyErr;
392
393 #[cfg(feature = "experimental-inspect")]
394 const OUTPUT_TYPE: PyStaticExpr = PyTzInfo::TYPE_HINT;
395
396 fn into_pyobject(self, py: Python<'py>) -> Result<Self::Output, Self::Error> {
397 if self == &TimeZone::UTC {
398 return Ok(PyTzInfo::utc(py)?.to_owned());
399 }
400
401 if let Some(iana_name) = self.iana_name() {
402 return PyTzInfo::timezone(py, iana_name);
403 }
404
405 self.to_fixed_offset()?.into_pyobject(py)
406 }
407}
408
409impl<'py> FromPyObject<'_, 'py> for TimeZone {
410 type Error = PyErr;
411
412 #[cfg(feature = "experimental-inspect")]
413 const INPUT_TYPE: PyStaticExpr = PyTzInfo::TYPE_HINT;
414
415 fn extract(ob: Borrowed<'_, 'py, PyAny>) -> PyResult<Self> {
416 let ob = ob.cast::<PyTzInfo>()?;
417
418 let attr = intern!(ob.py(), "key");
419 if ob.hasattr(attr)? {
420 Ok(TimeZone::get(
421 &ob.getattr(attr)?.extract::<Cow<'_, str>>()?,
422 )?)
423 } else {
424 Ok(ob.extract::<Offset>()?.to_time_zone())
425 }
426 }
427}
428
429impl<'py> IntoPyObject<'py> for Offset {
430 type Target = PyTzInfo;
431 type Output = Bound<'py, Self::Target>;
432 type Error = PyErr;
433
434 #[cfg(feature = "experimental-inspect")]
435 const OUTPUT_TYPE: PyStaticExpr = type_hint_identifier!("datetime", "timezone");
436
437 fn into_pyobject(self, py: Python<'py>) -> Result<Self::Output, Self::Error> {
438 if self == Offset::UTC {
439 return Ok(PyTzInfo::utc(py)?.to_owned());
440 }
441
442 PyTzInfo::fixed_offset(py, self.duration_since(Offset::UTC))
443 }
444}
445
446impl<'py> IntoPyObject<'py> for &Offset {
447 type Target = PyTzInfo;
448 type Output = Bound<'py, Self::Target>;
449 type Error = PyErr;
450
451 #[cfg(feature = "experimental-inspect")]
452 const OUTPUT_TYPE: PyStaticExpr = Offset::OUTPUT_TYPE;
453
454 fn into_pyobject(self, py: Python<'py>) -> Result<Self::Output, Self::Error> {
455 (*self).into_pyobject(py)
456 }
457}
458
459impl<'py> FromPyObject<'_, 'py> for Offset {
460 type Error = PyErr;
461
462 #[cfg(feature = "experimental-inspect")]
463 const INPUT_TYPE: PyStaticExpr = PyTzInfo::TYPE_HINT;
464
465 fn extract(ob: Borrowed<'_, 'py, PyAny>) -> PyResult<Self> {
466 let py = ob.py();
467 let ob = ob.cast::<PyTzInfo>()?;
468
469 let py_timedelta = ob.call_method1(intern!(py, "utcoffset"), (PyNone::get(py),))?;
470 if py_timedelta.is_none() {
471 return Err(PyTypeError::new_err(format!(
472 "{ob:?} is not a fixed offset timezone"
473 )));
474 }
475
476 let total_seconds = py_timedelta.extract::<SignedDuration>()?.as_secs();
477 debug_assert!(
478 (total_seconds / 3600).abs() <= 24,
479 "Offset must be between -24 hours and 24 hours but was {}h",
480 total_seconds / 3600
481 );
482 Ok(Offset::from_seconds(total_seconds as i32)?)
484 }
485}
486
487impl<'py> IntoPyObject<'py> for SignedDuration {
488 type Target = PyDelta;
489 type Output = Bound<'py, Self::Target>;
490 type Error = PyErr;
491
492 #[cfg(feature = "experimental-inspect")]
493 const OUTPUT_TYPE: PyStaticExpr = PyDelta::TYPE_HINT;
494
495 fn into_pyobject(self, py: Python<'py>) -> Result<Self::Output, Self::Error> {
496 let total_seconds = self.as_secs();
497 let days: i32 = (total_seconds / (24 * 60 * 60)).try_into()?;
498 let seconds: i32 = (total_seconds % (24 * 60 * 60)).try_into()?;
499 let microseconds = self.subsec_micros();
500
501 PyDelta::new(py, days, seconds, microseconds, true)
502 }
503}
504
505impl<'py> IntoPyObject<'py> for &SignedDuration {
506 type Target = PyDelta;
507 type Output = Bound<'py, Self::Target>;
508 type Error = PyErr;
509
510 #[cfg(feature = "experimental-inspect")]
511 const OUTPUT_TYPE: PyStaticExpr = SignedDuration::OUTPUT_TYPE;
512
513 fn into_pyobject(self, py: Python<'py>) -> Result<Self::Output, Self::Error> {
514 (*self).into_pyobject(py)
515 }
516}
517
518impl<'py> FromPyObject<'_, 'py> for SignedDuration {
519 type Error = PyErr;
520
521 #[cfg(feature = "experimental-inspect")]
522 const INPUT_TYPE: PyStaticExpr = PyDelta::TYPE_HINT;
523
524 fn extract(ob: Borrowed<'_, 'py, PyAny>) -> PyResult<Self> {
525 let delta = ob.cast::<PyDelta>()?;
526
527 #[cfg(not(Py_LIMITED_API))]
528 let (seconds, microseconds) = {
529 let days = delta.get_days() as i64;
530 let seconds = delta.get_seconds() as i64;
531 let microseconds = delta.get_microseconds();
532 (days * 24 * 60 * 60 + seconds, microseconds)
533 };
534
535 #[cfg(Py_LIMITED_API)]
536 let (seconds, microseconds) = {
537 let py = delta.py();
538 let days = delta.getattr(intern!(py, "days"))?.extract::<i64>()?;
539 let seconds = delta.getattr(intern!(py, "seconds"))?.extract::<i64>()?;
540 let microseconds = ob.getattr(intern!(py, "microseconds"))?.extract::<i32>()?;
541 (days * 24 * 60 * 60 + seconds, microseconds)
542 };
543
544 Ok(SignedDuration::new(seconds, microseconds * 1000))
545 }
546}
547
548impl<'py> FromPyObject<'_, 'py> for Span {
549 type Error = PyErr;
550
551 #[cfg(feature = "experimental-inspect")]
552 const INPUT_TYPE: PyStaticExpr = SignedDuration::INPUT_TYPE;
553
554 fn extract(ob: Borrowed<'_, 'py, PyAny>) -> PyResult<Self> {
555 let duration = ob.extract::<SignedDuration>()?;
556 Ok(duration.try_into()?)
557 }
558}
559
560impl<'py> IntoPyObject<'py> for ISOWeekDate {
561 type Target = PyDate;
562 type Output = Bound<'py, Self::Target>;
563 type Error = PyErr;
564
565 #[cfg(feature = "experimental-inspect")]
566 const OUTPUT_TYPE: PyStaticExpr = Date::OUTPUT_TYPE;
567
568 fn into_pyobject(self, py: Python<'py>) -> Result<Self::Output, Self::Error> {
569 self.date().into_pyobject(py)
570 }
571}
572
573impl<'py> IntoPyObject<'py> for &ISOWeekDate {
574 type Target = PyDate;
575 type Output = Bound<'py, Self::Target>;
576 type Error = PyErr;
577
578 #[cfg(feature = "experimental-inspect")]
579 const OUTPUT_TYPE: PyStaticExpr = ISOWeekDate::OUTPUT_TYPE;
580
581 fn into_pyobject(self, py: Python<'py>) -> Result<Self::Output, Self::Error> {
582 (*self).into_pyobject(py)
583 }
584}
585
586impl FromPyObject<'_, '_> for ISOWeekDate {
587 type Error = PyErr;
588
589 #[cfg(feature = "experimental-inspect")]
590 const INPUT_TYPE: PyStaticExpr = Date::INPUT_TYPE;
591
592 fn extract(ob: Borrowed<'_, '_, PyAny>) -> PyResult<Self> {
593 Ok(ob.extract::<Date>()?.iso_week_date())
594 }
595}
596
597impl From<jiff::Error> for PyErr {
598 fn from(e: jiff::Error) -> Self {
599 PyValueError::new_err(e.to_string())
600 }
601}
602
603#[cfg(test)]
604mod tests {
605 use super::*;
606 use crate::{types::PyTuple, BoundObject};
607 use jiff::tz::Offset;
608 use std::cmp::Ordering;
609
610 #[test]
611 #[cfg(all(Py_3_9, not(target_os = "windows")))]
615 fn test_zoneinfo_is_not_fixed_offset() {
616 use crate::types::any::PyAnyMethods;
617 use crate::types::dict::PyDictMethods;
618
619 Python::attach(|py| {
620 let locals = crate::types::PyDict::new(py);
621 py.run(
622 c"import zoneinfo; zi = zoneinfo.ZoneInfo('Europe/London')",
623 None,
624 Some(&locals),
625 )
626 .unwrap();
627 let result: PyResult<Offset> = locals.get_item("zi").unwrap().unwrap().extract();
628 assert!(result.is_err());
629 let res = result.err().unwrap();
630 let msg = res.value(py).repr().unwrap().to_string();
632 assert_eq!(msg, "TypeError(\"zoneinfo.ZoneInfo(key='Europe/London') is not a fixed offset timezone\")");
633 });
634 }
635
636 #[test]
637 fn test_timezone_aware_to_naive_fails() {
638 Python::attach(|py| {
641 let py_datetime =
642 new_py_datetime_ob(py, "datetime", (2022, 1, 1, 1, 0, 0, 0, python_utc(py)));
643 let res: PyResult<DateTime> = py_datetime.extract();
645 assert_eq!(
646 res.unwrap_err().value(py).repr().unwrap().to_string(),
647 "TypeError('expected a datetime without tzinfo')"
648 );
649 });
650 }
651
652 #[test]
653 fn test_naive_to_timezone_aware_fails() {
654 Python::attach(|py| {
657 let py_datetime = new_py_datetime_ob(py, "datetime", (2022, 1, 1, 1, 0, 0, 0));
658 let res: PyResult<Zoned> = py_datetime.extract();
659 assert_eq!(
660 res.unwrap_err().value(py).repr().unwrap().to_string(),
661 "TypeError('expected a datetime with non-None tzinfo')"
662 );
663 });
664 }
665
666 #[test]
667 fn test_invalid_types_fail() {
668 Python::attach(|py| {
669 let none = py.None().into_bound(py);
670 assert_eq!(
671 none.extract::<Span>().unwrap_err().to_string(),
672 "TypeError: 'None' is not an instance of 'timedelta'"
673 );
674 assert_eq!(
675 none.extract::<Offset>().unwrap_err().to_string(),
676 "TypeError: 'None' is not an instance of 'tzinfo'"
677 );
678 assert_eq!(
679 none.extract::<TimeZone>().unwrap_err().to_string(),
680 "TypeError: 'None' is not an instance of 'tzinfo'"
681 );
682 assert_eq!(
683 none.extract::<Time>().unwrap_err().to_string(),
684 "TypeError: 'None' is not an instance of 'time'"
685 );
686 assert_eq!(
687 none.extract::<Date>().unwrap_err().to_string(),
688 "TypeError: 'None' is not an instance of 'date'"
689 );
690 assert_eq!(
691 none.extract::<DateTime>().unwrap_err().to_string(),
692 "TypeError: 'None' is not an instance of 'datetime'"
693 );
694 assert_eq!(
695 none.extract::<Zoned>().unwrap_err().to_string(),
696 "TypeError: 'None' is not an instance of 'datetime'"
697 );
698 });
699 }
700
701 #[test]
702 fn test_pyo3_date_into_pyobject() {
703 let eq_ymd = |name: &'static str, year, month, day| {
704 Python::attach(|py| {
705 let date = Date::new(year, month, day)
706 .unwrap()
707 .into_pyobject(py)
708 .unwrap();
709 let py_date = new_py_datetime_ob(py, "date", (year, month, day));
710 assert_eq!(
711 date.compare(&py_date).unwrap(),
712 Ordering::Equal,
713 "{name}: {date} != {py_date}"
714 );
715 })
716 };
717
718 eq_ymd("past date", 2012, 2, 29);
719 eq_ymd("min date", 1, 1, 1);
720 eq_ymd("future date", 3000, 6, 5);
721 eq_ymd("max date", 9999, 12, 31);
722 }
723
724 #[test]
725 fn test_pyo3_date_frompyobject() {
726 let eq_ymd = |name: &'static str, year, month, day| {
727 Python::attach(|py| {
728 let py_date = new_py_datetime_ob(py, "date", (year, month, day));
729 let py_date: Date = py_date.extract().unwrap();
730 let date = Date::new(year, month, day).unwrap();
731 assert_eq!(py_date, date, "{name}: {date} != {py_date}");
732 })
733 };
734
735 eq_ymd("past date", 2012, 2, 29);
736 eq_ymd("min date", 1, 1, 1);
737 eq_ymd("future date", 3000, 6, 5);
738 eq_ymd("max date", 9999, 12, 31);
739 }
740
741 #[test]
742 fn test_pyo3_datetime_into_pyobject_utc() {
743 Python::attach(|py| {
744 let check_utc =
745 |name: &'static str, year, month, day, hour, minute, second, ms, py_ms| {
746 let datetime = DateTime::new(year, month, day, hour, minute, second, ms * 1000)
747 .unwrap()
748 .to_zoned(TimeZone::UTC)
749 .unwrap();
750 let datetime = datetime.into_pyobject(py).unwrap();
751 let py_datetime = new_py_datetime_ob(
752 py,
753 "datetime",
754 (
755 year,
756 month,
757 day,
758 hour,
759 minute,
760 second,
761 py_ms,
762 python_utc(py),
763 ),
764 );
765 assert_eq!(
766 datetime.compare(&py_datetime).unwrap(),
767 Ordering::Equal,
768 "{name}: {datetime} != {py_datetime}"
769 );
770 };
771
772 check_utc("regular", 2014, 5, 6, 7, 8, 9, 999_999, 999_999);
773 })
774 }
775
776 #[test]
777 fn test_pyo3_datetime_into_pyobject_fixed_offset() {
778 Python::attach(|py| {
779 let check_fixed_offset =
780 |name: &'static str, year, month, day, hour, minute, second, ms, py_ms| {
781 let offset = Offset::from_seconds(3600).unwrap();
782 let datetime = DateTime::new(year, month, day, hour, minute, second, ms * 1000)
783 .map_err(|e| {
784 eprintln!("{name}: {e}");
785 e
786 })
787 .unwrap()
788 .to_zoned(offset.to_time_zone())
789 .unwrap();
790 let datetime = datetime.into_pyobject(py).unwrap();
791 let py_tz = offset.into_pyobject(py).unwrap();
792 let py_datetime = new_py_datetime_ob(
793 py,
794 "datetime",
795 (year, month, day, hour, minute, second, py_ms, py_tz),
796 );
797 assert_eq!(
798 datetime.compare(&py_datetime).unwrap(),
799 Ordering::Equal,
800 "{name}: {datetime} != {py_datetime}"
801 );
802 };
803
804 check_fixed_offset("regular", 2014, 5, 6, 7, 8, 9, 999_999, 999_999);
805 })
806 }
807
808 #[test]
809 #[cfg(all(Py_3_9, not(windows)))]
810 fn test_pyo3_datetime_into_pyobject_tz() {
811 Python::attach(|py| {
812 let datetime = DateTime::new(2024, 12, 11, 23, 3, 13, 0)
813 .unwrap()
814 .to_zoned(TimeZone::get("Europe/London").unwrap())
815 .unwrap();
816 let datetime = datetime.into_pyobject(py).unwrap();
817 let py_datetime = new_py_datetime_ob(
818 py,
819 "datetime",
820 (
821 2024,
822 12,
823 11,
824 23,
825 3,
826 13,
827 0,
828 python_zoneinfo(py, "Europe/London"),
829 ),
830 );
831 assert_eq!(datetime.compare(&py_datetime).unwrap(), Ordering::Equal);
832 })
833 }
834
835 #[test]
836 fn test_pyo3_datetime_frompyobject_utc() {
837 Python::attach(|py| {
838 let year = 2014;
839 let month = 5;
840 let day = 6;
841 let hour = 7;
842 let minute = 8;
843 let second = 9;
844 let micro = 999_999;
845 let tz_utc = PyTzInfo::utc(py).unwrap();
846 let py_datetime = new_py_datetime_ob(
847 py,
848 "datetime",
849 (year, month, day, hour, minute, second, micro, tz_utc),
850 );
851 let py_datetime: Zoned = py_datetime.extract().unwrap();
852 let datetime = DateTime::new(year, month, day, hour, minute, second, micro * 1000)
853 .unwrap()
854 .to_zoned(TimeZone::UTC)
855 .unwrap();
856 assert_eq!(py_datetime, datetime,);
857 })
858 }
859
860 #[test]
861 #[cfg(all(Py_3_9, not(windows)))]
862 fn test_ambiguous_datetime_to_pyobject() {
863 use std::str::FromStr;
864 let dates = [
865 Zoned::from_str("2020-10-24 23:00:00[UTC]").unwrap(),
866 Zoned::from_str("2020-10-25 00:00:00[UTC]").unwrap(),
867 Zoned::from_str("2020-10-25 01:00:00[UTC]").unwrap(),
868 Zoned::from_str("2020-10-25 02:00:00[UTC]").unwrap(),
869 ];
870
871 let tz = TimeZone::get("Europe/London").unwrap();
872 let dates = dates.map(|dt| dt.with_time_zone(tz.clone()));
873
874 assert_eq!(
875 dates.clone().map(|ref dt| dt.to_string()),
876 [
877 "2020-10-25T00:00:00+01:00[Europe/London]",
878 "2020-10-25T01:00:00+01:00[Europe/London]",
879 "2020-10-25T01:00:00+00:00[Europe/London]",
880 "2020-10-25T02:00:00+00:00[Europe/London]",
881 ]
882 );
883
884 let dates = Python::attach(|py| {
885 let pydates = dates.map(|dt| dt.into_pyobject(py).unwrap());
886 assert_eq!(
887 pydates
888 .clone()
889 .map(|dt| dt.getattr("hour").unwrap().extract::<usize>().unwrap()),
890 [0, 1, 1, 2]
891 );
892
893 assert_eq!(
894 pydates
895 .clone()
896 .map(|dt| dt.getattr("fold").unwrap().extract::<usize>().unwrap() > 0),
897 [false, false, true, false]
898 );
899
900 pydates.map(|dt| dt.extract::<Zoned>().unwrap())
901 });
902
903 assert_eq!(
904 dates.map(|dt| dt.to_string()),
905 [
906 "2020-10-25T00:00:00+01:00[Europe/London]",
907 "2020-10-25T01:00:00+01:00[Europe/London]",
908 "2020-10-25T01:00:00+00:00[Europe/London]",
909 "2020-10-25T02:00:00+00:00[Europe/London]",
910 ]
911 );
912 }
913
914 #[test]
915 fn test_pyo3_datetime_frompyobject_fixed_offset() {
916 Python::attach(|py| {
917 let year = 2014;
918 let month = 5;
919 let day = 6;
920 let hour = 7;
921 let minute = 8;
922 let second = 9;
923 let micro = 999_999;
924 let offset = Offset::from_seconds(3600).unwrap();
925 let py_tz = offset.into_pyobject(py).unwrap();
926 let py_datetime = new_py_datetime_ob(
927 py,
928 "datetime",
929 (year, month, day, hour, minute, second, micro, py_tz),
930 );
931 let datetime_from_py: Zoned = py_datetime.extract().unwrap();
932 let datetime =
933 DateTime::new(year, month, day, hour, minute, second, micro * 1000).unwrap();
934 let datetime = datetime.to_zoned(offset.to_time_zone()).unwrap();
935
936 assert_eq!(datetime_from_py, datetime);
937 })
938 }
939
940 #[test]
941 fn test_pyo3_offset_fixed_into_pyobject() {
942 Python::attach(|py| {
943 let offset = Offset::from_seconds(3600)
945 .unwrap()
946 .into_pyobject(py)
947 .unwrap();
948 let td = new_py_datetime_ob(py, "timedelta", (0, 3600, 0));
950 let py_timedelta = new_py_datetime_ob(py, "timezone", (td,));
951 assert!(offset.eq(py_timedelta).unwrap());
953
954 let offset = Offset::from_seconds(-3600)
956 .unwrap()
957 .into_pyobject(py)
958 .unwrap();
959 let td = new_py_datetime_ob(py, "timedelta", (0, -3600, 0));
960 let py_timedelta = new_py_datetime_ob(py, "timezone", (td,));
961 assert!(offset.eq(py_timedelta).unwrap());
962 })
963 }
964
965 #[test]
966 fn test_pyo3_offset_fixed_frompyobject() {
967 Python::attach(|py| {
968 let py_timedelta = new_py_datetime_ob(py, "timedelta", (0, 3600, 0));
969 let py_tzinfo = new_py_datetime_ob(py, "timezone", (py_timedelta,));
970 let offset: Offset = py_tzinfo.extract().unwrap();
971 assert_eq!(Offset::from_seconds(3600).unwrap(), offset);
972 })
973 }
974
975 #[test]
976 fn test_pyo3_offset_utc_into_pyobject() {
977 Python::attach(|py| {
978 let utc = Offset::UTC.into_pyobject(py).unwrap();
979 let py_utc = python_utc(py);
980 assert!(utc.is(&py_utc));
981 })
982 }
983
984 #[test]
985 fn test_pyo3_offset_utc_frompyobject() {
986 Python::attach(|py| {
987 let py_utc = python_utc(py);
988 let py_utc: Offset = py_utc.extract().unwrap();
989 assert_eq!(Offset::UTC, py_utc);
990
991 let py_timedelta = new_py_datetime_ob(py, "timedelta", (0, 0, 0));
992 let py_timezone_utc = new_py_datetime_ob(py, "timezone", (py_timedelta,));
993 let py_timezone_utc: Offset = py_timezone_utc.extract().unwrap();
994 assert_eq!(Offset::UTC, py_timezone_utc);
995
996 let py_timedelta = new_py_datetime_ob(py, "timedelta", (0, 3600, 0));
997 let py_timezone = new_py_datetime_ob(py, "timezone", (py_timedelta,));
998 assert_ne!(Offset::UTC, py_timezone.extract::<Offset>().unwrap());
999 })
1000 }
1001
1002 #[test]
1003 fn test_pyo3_time_into_pyobject() {
1004 Python::attach(|py| {
1005 let check_time = |name: &'static str, hour, minute, second, ms, py_ms| {
1006 let time = Time::new(hour, minute, second, ms * 1000)
1007 .unwrap()
1008 .into_pyobject(py)
1009 .unwrap();
1010 let py_time = new_py_datetime_ob(py, "time", (hour, minute, second, py_ms));
1011 assert!(time.eq(&py_time).unwrap(), "{name}: {time} != {py_time}");
1012 };
1013
1014 check_time("regular", 3, 5, 7, 999_999, 999_999);
1015 })
1016 }
1017
1018 #[test]
1019 fn test_pyo3_time_frompyobject() {
1020 let hour = 3;
1021 let minute = 5;
1022 let second = 7;
1023 let micro = 999_999;
1024 Python::attach(|py| {
1025 let py_time = new_py_datetime_ob(py, "time", (hour, minute, second, micro));
1026 let py_time: Time = py_time.extract().unwrap();
1027 let time = Time::new(hour, minute, second, micro * 1000).unwrap();
1028 assert_eq!(py_time, time);
1029 })
1030 }
1031
1032 fn new_py_datetime_ob<'py, A>(py: Python<'py>, name: &str, args: A) -> Bound<'py, PyAny>
1033 where
1034 A: IntoPyObject<'py, Target = PyTuple>,
1035 {
1036 py.import("datetime")
1037 .unwrap()
1038 .getattr(name)
1039 .unwrap()
1040 .call1(
1041 args.into_pyobject(py)
1042 .map_err(Into::into)
1043 .unwrap()
1044 .into_bound(),
1045 )
1046 .unwrap()
1047 }
1048
1049 fn python_utc(py: Python<'_>) -> Bound<'_, PyAny> {
1050 py.import("datetime")
1051 .unwrap()
1052 .getattr("timezone")
1053 .unwrap()
1054 .getattr("utc")
1055 .unwrap()
1056 }
1057
1058 #[cfg(all(Py_3_9, not(windows)))]
1059 fn python_zoneinfo<'py>(py: Python<'py>, timezone: &str) -> Bound<'py, PyAny> {
1060 py.import("zoneinfo")
1061 .unwrap()
1062 .getattr("ZoneInfo")
1063 .unwrap()
1064 .call1((timezone,))
1065 .unwrap()
1066 }
1067
1068 #[cfg(not(any(target_arch = "wasm32", Py_GIL_DISABLED)))]
1069 mod proptests {
1070 use super::*;
1071 use crate::types::IntoPyDict;
1072 use jiff::tz::TimeZoneTransition;
1073 use jiff::SpanRelativeTo;
1074 use proptest::prelude::*;
1075 use std::ffi::CString;
1076
1077 #[track_caller]
1079 fn try_date(year: i16, month: i8, day: i8) -> Result<Date, TestCaseError> {
1080 let location = std::panic::Location::caller();
1081 Date::new(year, month, day)
1082 .map_err(|err| TestCaseError::reject(format!("{location}: {err:?}")))
1083 }
1084
1085 #[track_caller]
1086 fn try_time(hour: i8, min: i8, sec: i8, micro: i32) -> Result<Time, TestCaseError> {
1087 let location = std::panic::Location::caller();
1088 Time::new(hour, min, sec, micro * 1000)
1089 .map_err(|err| TestCaseError::reject(format!("{location}: {err:?}")))
1090 }
1091
1092 #[expect(clippy::too_many_arguments)]
1093 fn try_zoned(
1094 year: i16,
1095 month: i8,
1096 day: i8,
1097 hour: i8,
1098 min: i8,
1099 sec: i8,
1100 micro: i32,
1101 tz: TimeZone,
1102 ) -> Result<Zoned, TestCaseError> {
1103 let date = try_date(year, month, day)?;
1104 let time = try_time(hour, min, sec, micro)?;
1105 let location = std::panic::Location::caller();
1106 DateTime::from_parts(date, time)
1107 .to_zoned(tz)
1108 .map_err(|err| TestCaseError::reject(format!("{location}: {err:?}")))
1109 }
1110
1111 prop_compose! {
1112 fn timezone_transitions(timezone: &TimeZone)
1113 (year in 1900i16..=2100i16, month in 1i8..=12i8)
1114 -> TimeZoneTransition<'_> {
1115 let datetime = DateTime::new(year, month, 1, 0, 0, 0, 0).unwrap();
1116 let timestamp= timezone.to_zoned(datetime).unwrap().timestamp();
1117 timezone.following(timestamp).next().unwrap()
1118 }
1119 }
1120
1121 proptest! {
1122
1123 #[test]
1125 fn test_pyo3_offset_fixed_frompyobject_created_in_python(timestamp in 0..(i32::MAX as i64), timedelta in -86399i32..=86399i32) {
1126 Python::attach(|py| {
1127 let globals = [("datetime", py.import("datetime").unwrap())].into_py_dict(py).unwrap();
1128 let code = format!("datetime.datetime.fromtimestamp({timestamp}).replace(tzinfo=datetime.timezone(datetime.timedelta(seconds={timedelta})))");
1129 let t = py.eval(&CString::new(code).unwrap(), Some(&globals), None).unwrap();
1130
1131 let py_iso_str = t.call_method0("isoformat").unwrap();
1133
1134 let rust_iso_str = t.extract::<Zoned>().unwrap().strftime("%Y-%m-%dT%H:%M:%S%:z").to_string();
1136
1137 prop_assert_eq!(py_iso_str.to_string(), rust_iso_str);
1139 Ok(())
1140 })?;
1141 }
1142
1143 #[test]
1144 fn test_duration_roundtrip(days in -999999999i64..=999999999i64) {
1145 Python::attach(|py| {
1148 let dur = SignedDuration::new(days * 24 * 60 * 60, 0);
1149 let py_delta = dur.into_pyobject(py).unwrap();
1150 let roundtripped: SignedDuration = py_delta.extract().expect("Round trip");
1151 prop_assert_eq!(dur, roundtripped);
1152 Ok(())
1153 })?;
1154 }
1155
1156 #[test]
1157 fn test_span_roundtrip(days in -999999999i64..=999999999i64) {
1158 Python::attach(|py| {
1161 if let Ok(span) = Span::new().try_days(days) {
1162 let relative_to = SpanRelativeTo::days_are_24_hours();
1163 let jiff_duration = span.to_duration(relative_to).unwrap();
1164 let py_delta = jiff_duration.into_pyobject(py).unwrap();
1165 let roundtripped: Span = py_delta.extract().expect("Round trip");
1166 prop_assert_eq!(span.compare((roundtripped, relative_to)).unwrap(), Ordering::Equal);
1167 }
1168 Ok(())
1169 })?;
1170 }
1171
1172 #[test]
1173 fn test_fixed_offset_roundtrip(secs in -86399i32..=86399i32) {
1174 Python::attach(|py| {
1175 let offset = Offset::from_seconds(secs).unwrap();
1176 let py_offset = offset.into_pyobject(py).unwrap();
1177 let roundtripped: Offset = py_offset.extract().expect("Round trip");
1178 prop_assert_eq!(offset, roundtripped);
1179 Ok(())
1180 })?;
1181 }
1182
1183 #[test]
1184 fn test_naive_date_roundtrip(
1185 year in 1i16..=9999i16,
1186 month in 1i8..=12i8,
1187 day in 1i8..=31i8
1188 ) {
1189 Python::attach(|py| {
1192 let date = try_date(year, month, day)?;
1193 let py_date = date.into_pyobject(py).unwrap();
1194 let roundtripped: Date = py_date.extract().expect("Round trip");
1195 prop_assert_eq!(date, roundtripped);
1196 Ok(())
1197 })?;
1198 }
1199
1200 #[test]
1201 fn test_weekdate_roundtrip(
1202 year in 1i16..=9999i16,
1203 month in 1i8..=12i8,
1204 day in 1i8..=31i8
1205 ) {
1206 Python::attach(|py| {
1209 let weekdate = try_date(year, month, day)?.iso_week_date();
1210 let py_date = weekdate.into_pyobject(py).unwrap();
1211 let roundtripped = py_date.extract::<ISOWeekDate>().expect("Round trip");
1212 prop_assert_eq!(weekdate, roundtripped);
1213 Ok(())
1214 })?;
1215 }
1216
1217 #[test]
1218 fn test_naive_time_roundtrip(
1219 hour in 0i8..=23i8,
1220 min in 0i8..=59i8,
1221 sec in 0i8..=59i8,
1222 micro in 0i32..=999_999i32
1223 ) {
1224 Python::attach(|py| {
1225 let time = try_time(hour, min, sec, micro)?;
1226 let py_time = time.into_pyobject(py).unwrap();
1227 let roundtripped: Time = py_time.extract().expect("Round trip");
1228 prop_assert_eq!(time, roundtripped);
1229 Ok(())
1230 })?;
1231 }
1232
1233 #[test]
1234 fn test_naive_datetime_roundtrip(
1235 year in 1i16..=9999i16,
1236 month in 1i8..=12i8,
1237 day in 1i8..=31i8,
1238 hour in 0i8..=23i8,
1239 min in 0i8..=59i8,
1240 sec in 0i8..=59i8,
1241 micro in 0i32..=999_999i32
1242 ) {
1243 Python::attach(|py| {
1244 let date = try_date(year, month, day)?;
1245 let time = try_time(hour, min, sec, micro)?;
1246 let dt = DateTime::from_parts(date, time);
1247 let pydt = dt.into_pyobject(py).unwrap();
1248 let roundtripped: DateTime = pydt.extract().expect("Round trip");
1249 prop_assert_eq!(dt, roundtripped);
1250 Ok(())
1251 })?;
1252 }
1253
1254 #[test]
1255 fn test_utc_datetime_roundtrip(
1256 year in 1i16..=9999i16,
1257 month in 1i8..=12i8,
1258 day in 1i8..=31i8,
1259 hour in 0i8..=23i8,
1260 min in 0i8..=59i8,
1261 sec in 0i8..=59i8,
1262 micro in 0i32..=999_999i32
1263 ) {
1264 Python::attach(|py| {
1265 let dt: Zoned = try_zoned(year, month, day, hour, min, sec, micro, TimeZone::UTC)?;
1266 let py_dt = (&dt).into_pyobject(py).unwrap();
1267 let roundtripped: Zoned = py_dt.extract().expect("Round trip");
1268 prop_assert_eq!(dt, roundtripped);
1269 Ok(())
1270 })?;
1271 }
1272
1273 #[test]
1274 fn test_fixed_offset_datetime_roundtrip(
1275 year in 1i16..=9999i16,
1276 month in 1i8..=12i8,
1277 day in 1i8..=31i8,
1278 hour in 0i8..=23i8,
1279 min in 0i8..=59i8,
1280 sec in 0i8..=59i8,
1281 micro in 0i32..=999_999i32,
1282 offset_secs in -86399i32..=86399i32
1283 ) {
1284 Python::attach(|py| {
1285 let offset = Offset::from_seconds(offset_secs).unwrap();
1286 let dt = try_zoned(year, month, day, hour, min, sec, micro, offset.to_time_zone())?;
1287 let py_dt = (&dt).into_pyobject(py).unwrap();
1288 let roundtripped: Zoned = py_dt.extract().expect("Round trip");
1289 prop_assert_eq!(dt, roundtripped);
1290 Ok(())
1291 })?;
1292 }
1293
1294 #[test]
1295 #[cfg(all(Py_3_9, not(windows)))]
1296 fn test_zoned_datetime_roundtrip_around_timezone_transition(
1297 (timezone, transition) in prop_oneof![
1298 Just(&TimeZone::get("Europe/London").unwrap()),
1299 Just(&TimeZone::get("America/New_York").unwrap()),
1300 Just(&TimeZone::get("Australia/Sydney").unwrap()),
1301 ].prop_flat_map(|tz| (Just(tz), timezone_transitions(tz))),
1302 hour in -2i32..=2i32,
1303 min in 0u32..=59u32,
1304 ) {
1305 Python::attach(|py| {
1306 let transition_moment = transition.timestamp();
1307 let zoned = (transition_moment - Span::new().hours(hour).minutes(min))
1308 .to_zoned(timezone.clone());
1309
1310 let py_dt = (&zoned).into_pyobject(py).unwrap();
1311 let roundtripped: Zoned = py_dt.extract().expect("Round trip");
1312 prop_assert_eq!(zoned, roundtripped);
1313 Ok(())
1314 })?;
1315 }
1316 }
1317 }
1318}