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