#![cfg(all(feature = "chrono", not(Py_LIMITED_API)))]
#![doc = concat!("pyo3 = { version = \"", env!("CARGO_PKG_VERSION"), "\", features = [\"chrono\"] }")]
use crate::exceptions::{PyTypeError, PyValueError};
use crate::types::{
timezone_utc, PyDate, PyDateAccess, PyDateTime, PyDelta, PyDeltaAccess, PyTime, PyTimeAccess,
PyTzInfo, PyTzInfoAccess, PyUnicode,
};
use crate::{FromPyObject, IntoPy, PyAny, PyObject, PyResult, Python, ToPyObject};
use chrono::offset::{FixedOffset, Utc};
use chrono::{
DateTime, Datelike, Duration, NaiveDate, NaiveDateTime, NaiveTime, Offset, TimeZone, Timelike,
};
use pyo3_ffi::{PyDateTime_IMPORT, PyTimeZone_FromOffset};
use std::convert::TryInto;
impl ToPyObject for Duration {
fn to_object(&self, py: Python<'_>) -> PyObject {
let days = self.num_days();
let secs_dur = *self - Duration::days(days);
let secs = secs_dur.num_seconds().try_into().unwrap();
let micros = (secs_dur - Duration::seconds(secs_dur.num_seconds()))
.num_microseconds()
.unwrap()
.try_into()
.unwrap();
let delta = PyDelta::new(py, days.try_into().unwrap_or(i32::MAX), secs, micros, true)
.expect("failed to construct delta");
delta.into()
}
}
impl IntoPy<PyObject> for Duration {
fn into_py(self, py: Python<'_>) -> PyObject {
ToPyObject::to_object(&self, py)
}
}
impl FromPyObject<'_> for Duration {
fn extract(ob: &PyAny) -> PyResult<Duration> {
let delta: &PyDelta = ob.downcast()?;
Ok(Duration::days(delta.get_days().into())
+ Duration::seconds(delta.get_seconds().into())
+ Duration::microseconds(delta.get_microseconds().into()))
}
}
impl ToPyObject for NaiveDate {
fn to_object(&self, py: Python<'_>) -> PyObject {
(*self).into_py(py)
}
}
impl IntoPy<PyObject> for NaiveDate {
fn into_py(self, py: Python<'_>) -> PyObject {
let DateArgs { year, month, day } = self.into();
PyDate::new(py, year, month, day)
.expect("failed to construct date")
.into()
}
}
impl FromPyObject<'_> for NaiveDate {
fn extract(ob: &PyAny) -> PyResult<NaiveDate> {
let date: &PyDate = ob.downcast()?;
py_date_to_naive_date(date)
}
}
impl ToPyObject for NaiveTime {
fn to_object(&self, py: Python<'_>) -> PyObject {
(*self).into_py(py)
}
}
impl IntoPy<PyObject> for NaiveTime {
fn into_py(self, py: Python<'_>) -> PyObject {
let TimeArgs {
hour,
min,
sec,
micro,
fold,
} = self.into();
let time = PyTime::new_with_fold(py, hour, min, sec, micro, None, fold)
.expect("failed to construct time");
time.into()
}
}
impl FromPyObject<'_> for NaiveTime {
fn extract(ob: &PyAny) -> PyResult<NaiveTime> {
let time: &PyTime = ob.downcast()?;
py_time_to_naive_time(time, true)
}
}
impl ToPyObject for NaiveDateTime {
fn to_object(&self, py: Python<'_>) -> PyObject {
naive_datetime_to_py_datetime(py, self, None)
.expect("failed to construct datetime")
.into()
}
}
impl IntoPy<PyObject> for NaiveDateTime {
fn into_py(self, py: Python<'_>) -> PyObject {
ToPyObject::to_object(&self, py)
}
}
impl FromPyObject<'_> for NaiveDateTime {
fn extract(ob: &PyAny) -> PyResult<NaiveDateTime> {
let dt: &PyDateTime = ob.downcast()?;
if dt.get_tzinfo().is_some() {
return Err(PyTypeError::new_err("expected a datetime without tzinfo"));
}
let dt = NaiveDateTime::new(
py_date_to_naive_date(dt)?,
py_time_to_naive_time(dt, false)?,
);
Ok(dt)
}
}
impl<Tz: TimeZone> ToPyObject for DateTime<Tz> {
fn to_object(&self, py: Python<'_>) -> PyObject {
let tz = self.offset().fix().to_object(py);
let tz = tz.downcast(py).unwrap();
naive_datetime_to_py_datetime(py, &self.naive_local(), Some(tz))
.expect("failed to construct datetime")
.into()
}
}
impl<Tz: TimeZone> IntoPy<PyObject> for DateTime<Tz> {
fn into_py(self, py: Python<'_>) -> PyObject {
ToPyObject::to_object(&self, py)
}
}
impl FromPyObject<'_> for DateTime<FixedOffset> {
fn extract(ob: &PyAny) -> PyResult<DateTime<FixedOffset>> {
let dt: &PyDateTime = ob.downcast()?;
let tz: FixedOffset = if let Some(tzinfo) = dt.get_tzinfo() {
tzinfo.extract()?
} else {
return Err(PyTypeError::new_err(
"expected a datetime with non-None tzinfo",
));
};
let dt = NaiveDateTime::new(
py_date_to_naive_date(dt)?,
py_time_to_naive_time(dt, false)?,
);
Ok(dt.and_local_timezone(tz).unwrap())
}
}
impl FromPyObject<'_> for DateTime<Utc> {
fn extract(ob: &PyAny) -> PyResult<DateTime<Utc>> {
let dt: &PyDateTime = ob.downcast()?;
let _: Utc = if let Some(tzinfo) = dt.get_tzinfo() {
tzinfo.extract()?
} else {
return Err(PyTypeError::new_err(
"expected a datetime with non-None tzinfo",
));
};
let dt = NaiveDateTime::new(py_date_to_naive_date(dt)?, py_time_to_naive_time(dt, true)?);
Ok(dt.and_utc())
}
}
fn py_timezone_from_offset<'a>(py: &Python<'a>, td: &PyDelta) -> &'a PyAny {
unsafe {
PyDateTime_IMPORT();
py.from_owned_ptr(PyTimeZone_FromOffset(td.as_ptr()))
}
}
impl ToPyObject for FixedOffset {
fn to_object(&self, py: Python<'_>) -> PyObject {
let seconds_offset = self.local_minus_utc();
let td =
PyDelta::new(py, 0, seconds_offset, 0, true).expect("failed to construct timedelta");
py_timezone_from_offset(&py, td).into()
}
}
impl IntoPy<PyObject> for FixedOffset {
fn into_py(self, py: Python<'_>) -> PyObject {
ToPyObject::to_object(&self, py)
}
}
impl FromPyObject<'_> for FixedOffset {
fn extract(ob: &PyAny) -> PyResult<FixedOffset> {
let py_tzinfo: &PyTzInfo = ob.downcast()?;
let py_timedelta = py_tzinfo.call_method1("utcoffset", (ob.py().None(),))?;
let py_timedelta: &PyDelta = py_timedelta.downcast().map_err(|_| {
PyTypeError::new_err(format!(
"{:?} is not a fixed offset timezone",
py_tzinfo
.repr()
.unwrap_or_else(|_| PyUnicode::new(ob.py(), "repr failed"))
))
})?;
let days = py_timedelta.get_days() as i64;
let seconds = py_timedelta.get_seconds() as i64;
let total_seconds = Duration::days(days) + Duration::seconds(seconds);
let total_seconds = total_seconds.num_seconds() as i32;
FixedOffset::east_opt(total_seconds)
.ok_or_else(|| PyValueError::new_err("fixed offset out of bounds"))
}
}
impl ToPyObject for Utc {
fn to_object(&self, py: Python<'_>) -> PyObject {
timezone_utc(py).to_object(py)
}
}
impl IntoPy<PyObject> for Utc {
fn into_py(self, py: Python<'_>) -> PyObject {
ToPyObject::to_object(&self, py)
}
}
impl FromPyObject<'_> for Utc {
fn extract(ob: &PyAny) -> PyResult<Utc> {
let py_tzinfo: &PyTzInfo = ob.downcast()?;
let py_utc = timezone_utc(ob.py());
if py_tzinfo.eq(py_utc)? {
Ok(Utc)
} else {
Err(PyValueError::new_err("expected datetime.timezone.utc"))
}
}
}
struct DateArgs {
year: i32,
month: u8,
day: u8,
}
impl From<NaiveDate> for DateArgs {
fn from(value: NaiveDate) -> Self {
Self {
year: value.year(),
month: value.month() as u8,
day: value.day() as u8,
}
}
}
struct TimeArgs {
hour: u8,
min: u8,
sec: u8,
micro: u32,
fold: bool,
}
impl From<NaiveTime> for TimeArgs {
fn from(value: NaiveTime) -> Self {
let ns = value.nanosecond();
let checked_sub = ns.checked_sub(1_000_000_000);
let fold = checked_sub.is_some();
let micro = checked_sub.unwrap_or(ns) / 1000;
Self {
hour: value.hour() as u8,
min: value.minute() as u8,
sec: value.second() as u8,
micro,
fold,
}
}
}
fn naive_datetime_to_py_datetime<'py>(
py: Python<'py>,
naive_datetime: &NaiveDateTime,
tzinfo: Option<&PyTzInfo>,
) -> PyResult<&'py PyDateTime> {
let DateArgs { year, month, day } = naive_datetime.date().into();
let TimeArgs {
hour,
min,
sec,
micro,
fold,
} = naive_datetime.time().into();
let datetime =
PyDateTime::new_with_fold(py, year, month, day, hour, min, sec, micro, tzinfo, fold)?;
Ok(datetime)
}
fn py_date_to_naive_date(py_date: &impl PyDateAccess) -> PyResult<NaiveDate> {
NaiveDate::from_ymd_opt(
py_date.get_year(),
py_date.get_month().into(),
py_date.get_day().into(),
)
.ok_or_else(|| PyValueError::new_err("invalid or out-of-range date"))
}
fn py_time_to_naive_time(py_time: &impl PyTimeAccess, add_fold: bool) -> PyResult<NaiveTime> {
NaiveTime::from_hms_micro_opt(
py_time.get_hour().into(),
py_time.get_minute().into(),
py_time.get_second().into(),
py_time.get_microsecond()
+ if add_fold && py_time.get_fold() {
1_000_000
} else {
0
},
)
.ok_or_else(|| PyValueError::new_err("invalid or out-of-range time"))
}
#[cfg(test)]
mod tests {
use std::{cmp::Ordering, panic};
use super::*;
#[test]
#[cfg(all(Py_3_9, not(target_os = "windows")))]
fn test_zoneinfo_is_not_fixed_offset() {
Python::with_gil(|py| {
let locals = crate::types::PyDict::new(py);
py.run(
"import zoneinfo; zi = zoneinfo.ZoneInfo('Europe/London')",
None,
Some(locals),
)
.unwrap();
let result: PyResult<FixedOffset> = locals.get_item("zi").unwrap().unwrap().extract();
assert!(result.is_err());
let res = result.err().unwrap();
let msg = res.value(py).repr().unwrap().to_string();
assert_eq!(msg, "TypeError('\"zoneinfo.ZoneInfo(key=\\'Europe/London\\')\" is not a fixed offset timezone')");
});
}
#[test]
fn test_timezone_aware_to_naive_fails() {
Python::with_gil(|py| {
let utc = timezone_utc(py);
let py_datetime = PyDateTime::new(py, 2022, 1, 1, 1, 0, 0, 0, Some(utc)).unwrap();
let res: PyResult<NaiveDateTime> = py_datetime.extract();
assert!(res.is_err());
let res = res.err().unwrap();
let msg = res.value(py).repr().unwrap().to_string();
assert_eq!(msg, "TypeError('expected a datetime without tzinfo')");
});
}
#[test]
fn test_naive_to_timezone_aware_fails() {
Python::with_gil(|py| {
let py_datetime = PyDateTime::new(py, 2022, 1, 1, 1, 0, 0, 0, None).unwrap();
let res: PyResult<DateTime<Utc>> = py_datetime.extract();
assert!(res.is_err());
let res = res.err().unwrap();
let msg = res.value(py).repr().unwrap().to_string();
assert_eq!(msg, "TypeError('expected a datetime with non-None tzinfo')");
let res: PyResult<DateTime<FixedOffset>> = py_datetime.extract();
assert!(res.is_err());
let res = res.err().unwrap();
let msg = res.value(py).repr().unwrap().to_string();
assert_eq!(msg, "TypeError('expected a datetime with non-None tzinfo')");
});
}
#[test]
fn test_invalid_types_fail() {
Python::with_gil(|py| {
let none = py.None().into_ref(py);
assert_eq!(
none.extract::<Duration>().unwrap_err().to_string(),
"TypeError: 'NoneType' object cannot be converted to 'PyDelta'"
);
assert_eq!(
none.extract::<FixedOffset>().unwrap_err().to_string(),
"TypeError: 'NoneType' object cannot be converted to 'PyTzInfo'"
);
assert_eq!(
none.extract::<Utc>().unwrap_err().to_string(),
"TypeError: 'NoneType' object cannot be converted to 'PyTzInfo'"
);
assert_eq!(
none.extract::<NaiveTime>().unwrap_err().to_string(),
"TypeError: 'NoneType' object cannot be converted to 'PyTime'"
);
assert_eq!(
none.extract::<NaiveDate>().unwrap_err().to_string(),
"TypeError: 'NoneType' object cannot be converted to 'PyDate'"
);
assert_eq!(
none.extract::<NaiveDateTime>().unwrap_err().to_string(),
"TypeError: 'NoneType' object cannot be converted to 'PyDateTime'"
);
assert_eq!(
none.extract::<DateTime<Utc>>().unwrap_err().to_string(),
"TypeError: 'NoneType' object cannot be converted to 'PyDateTime'"
);
assert_eq!(
none.extract::<DateTime<FixedOffset>>()
.unwrap_err()
.to_string(),
"TypeError: 'NoneType' object cannot be converted to 'PyDateTime'"
);
});
}
#[test]
fn test_pyo3_timedelta_topyobject() {
let check = |name: &'static str, delta: Duration, py_days, py_seconds, py_ms| {
Python::with_gil(|py| {
let delta = delta.to_object(py);
let delta: &PyDelta = delta.extract(py).unwrap();
let py_delta = PyDelta::new(py, py_days, py_seconds, py_ms, true).unwrap();
assert!(
delta.eq(py_delta).unwrap(),
"{}: {} != {}",
name,
delta,
py_delta
);
});
};
let delta = Duration::days(-1) + Duration::seconds(1) + Duration::microseconds(-10);
check("delta normalization", delta, -1, 1, -10);
let delta = Duration::seconds(-86399999913600); check("delta min value", delta, -999999999, 0, 0);
let delta = Duration::seconds(86399999999999) + Duration::nanoseconds(999999000); check("delta max value", delta, 999999999, 86399, 999999);
Python::with_gil(|py| {
assert!(panic::catch_unwind(|| Duration::min_value().to_object(py)).is_err());
assert!(panic::catch_unwind(|| Duration::max_value().to_object(py)).is_err());
});
}
#[test]
fn test_pyo3_timedelta_frompyobject() {
let check = |name: &'static str, delta: Duration, py_days, py_seconds, py_ms| {
Python::with_gil(|py| {
let py_delta = PyDelta::new(py, py_days, py_seconds, py_ms, true).unwrap();
let py_delta: Duration = py_delta.extract().unwrap();
assert_eq!(py_delta, delta, "{}: {} != {}", name, py_delta, delta);
})
};
check(
"min pydelta value",
Duration::seconds(-86399999913600),
-999999999,
0,
0,
);
check(
"max pydelta value",
Duration::seconds(86399999999999) + Duration::microseconds(999999),
999999999,
86399,
999999,
);
Python::with_gil(|py| {
let low_days: i32 = -1000000000;
assert!(panic::catch_unwind(|| Duration::days(low_days as i64)).is_ok());
assert!(panic::catch_unwind(|| {
let pydelta = PyDelta::new(py, low_days, 0, 0, true).unwrap();
if let Ok(_duration) = pydelta.extract::<Duration>() {
}
})
.is_err());
let high_days: i32 = 1000000000;
assert!(panic::catch_unwind(|| Duration::days(high_days as i64)).is_ok());
assert!(panic::catch_unwind(|| {
let pydelta = PyDelta::new(py, high_days, 0, 0, true).unwrap();
if let Ok(_duration) = pydelta.extract::<Duration>() {
}
})
.is_err());
});
}
#[test]
fn test_pyo3_date_topyobject() {
let eq_ymd = |name: &'static str, year, month, day| {
Python::with_gil(|py| {
let date = NaiveDate::from_ymd_opt(year, month, day)
.unwrap()
.to_object(py);
let date: &PyDate = date.extract(py).unwrap();
let py_date = PyDate::new(py, year, month as u8, day as u8).unwrap();
assert_eq!(
date.compare(py_date).unwrap(),
Ordering::Equal,
"{}: {} != {}",
name,
date,
py_date
);
})
};
eq_ymd("past date", 2012, 2, 29);
eq_ymd("min date", 1, 1, 1);
eq_ymd("future date", 3000, 6, 5);
eq_ymd("max date", 9999, 12, 31);
}
#[test]
fn test_pyo3_date_frompyobject() {
let eq_ymd = |name: &'static str, year, month, day| {
Python::with_gil(|py| {
let py_date = PyDate::new(py, year, month as u8, day as u8).unwrap();
let py_date: NaiveDate = py_date.extract().unwrap();
let date = NaiveDate::from_ymd_opt(year, month, day).unwrap();
assert_eq!(py_date, date, "{}: {} != {}", name, date, py_date);
})
};
eq_ymd("past date", 2012, 2, 29);
eq_ymd("min date", 1, 1, 1);
eq_ymd("future date", 3000, 6, 5);
eq_ymd("max date", 9999, 12, 31);
}
#[test]
fn test_pyo3_datetime_topyobject() {
let check_utc =
|name: &'static str, year, month, day, hour, minute, second, ms, py_ms, fold| {
Python::with_gil(|py| {
let datetime = NaiveDate::from_ymd_opt(year, month, day)
.unwrap()
.and_hms_micro_opt(hour, minute, second, ms)
.unwrap()
.and_utc();
let datetime = datetime.to_object(py);
let datetime: &PyDateTime = datetime.extract(py).unwrap();
let py_tz = Utc.to_object(py);
let py_tz = py_tz.downcast(py).unwrap();
let py_datetime = PyDateTime::new_with_fold(
py,
year,
month as u8,
day as u8,
hour as u8,
minute as u8,
second as u8,
py_ms,
Some(py_tz),
fold,
)
.unwrap();
assert_eq!(
datetime.compare(py_datetime).unwrap(),
Ordering::Equal,
"{}: {} != {}",
name,
datetime,
py_datetime
);
})
};
check_utc("fold", 2014, 5, 6, 7, 8, 59, 1_999_999, 999_999, true);
check_utc("non fold", 2014, 5, 6, 7, 8, 9, 999_999, 999_999, false);
let check_fixed_offset =
|name: &'static str, year, month, day, hour, minute, second, ms, py_ms, fold| {
Python::with_gil(|py| {
let offset = FixedOffset::east_opt(3600).unwrap();
let datetime = NaiveDate::from_ymd_opt(year, month, day)
.unwrap()
.and_hms_micro_opt(hour, minute, second, ms)
.unwrap()
.and_local_timezone(offset)
.unwrap();
let datetime = datetime.to_object(py);
let datetime: &PyDateTime = datetime.extract(py).unwrap();
let py_tz = offset.to_object(py);
let py_tz = py_tz.downcast(py).unwrap();
let py_datetime = PyDateTime::new_with_fold(
py,
year,
month as u8,
day as u8,
hour as u8,
minute as u8,
second as u8,
py_ms,
Some(py_tz),
fold,
)
.unwrap();
assert_eq!(
datetime.compare(py_datetime).unwrap(),
Ordering::Equal,
"{}: {} != {}",
name,
datetime,
py_datetime
);
})
};
check_fixed_offset("fold", 2014, 5, 6, 7, 8, 59, 1_999_999, 999_999, true);
check_fixed_offset("non fold", 2014, 5, 6, 7, 8, 9, 999_999, 999_999, false);
}
#[test]
fn test_pyo3_datetime_frompyobject() {
let check_utc =
|name: &'static str, year, month, day, hour, minute, second, ms, py_ms, fold| {
Python::with_gil(|py| {
let py_tz = Utc.to_object(py);
let py_tz = py_tz.downcast(py).unwrap();
let py_datetime = PyDateTime::new_with_fold(
py,
year,
month as u8,
day as u8,
hour as u8,
minute as u8,
second as u8,
py_ms,
Some(py_tz),
fold,
)
.unwrap();
let py_datetime: DateTime<Utc> = py_datetime.extract().unwrap();
let datetime = NaiveDate::from_ymd_opt(year, month, day)
.unwrap()
.and_hms_micro_opt(hour, minute, second, ms)
.unwrap();
let datetime = datetime.and_utc();
assert_eq!(
py_datetime, datetime,
"{}: {} != {}",
name, datetime, py_datetime
);
})
};
check_utc("fold", 2014, 5, 6, 7, 8, 59, 1_999_999, 999_999, true);
check_utc("non fold", 2014, 5, 6, 7, 8, 9, 999_999, 999_999, false);
let check_fixed_offset = |year, month, day, hour, minute, second, ms| {
Python::with_gil(|py| {
let offset = FixedOffset::east_opt(3600).unwrap();
let py_tz = offset.to_object(py);
let py_tz = py_tz.downcast(py).unwrap();
let py_datetime = PyDateTime::new_with_fold(
py,
year,
month as u8,
day as u8,
hour as u8,
minute as u8,
second as u8,
ms,
Some(py_tz),
false, )
.unwrap();
let py_datetime: DateTime<FixedOffset> = py_datetime.extract().unwrap();
let datetime = NaiveDate::from_ymd_opt(year, month, day)
.unwrap()
.and_hms_micro_opt(hour, minute, second, ms)
.unwrap();
let datetime = datetime.and_local_timezone(offset).unwrap();
assert_eq!(py_datetime, datetime, "{} != {}", datetime, py_datetime);
})
};
check_fixed_offset(2014, 5, 6, 7, 8, 9, 999_999);
Python::with_gil(|py| {
let py_tz = Utc.to_object(py);
let py_tz = py_tz.downcast(py).unwrap();
let py_datetime =
PyDateTime::new_with_fold(py, 2014, 5, 6, 7, 8, 9, 999_999, Some(py_tz), false)
.unwrap();
assert!(py_datetime.extract::<DateTime<FixedOffset>>().is_ok());
let offset = FixedOffset::east_opt(3600).unwrap();
let py_tz = offset.to_object(py);
let py_tz = py_tz.downcast(py).unwrap();
let py_datetime =
PyDateTime::new_with_fold(py, 2014, 5, 6, 7, 8, 9, 999_999, Some(py_tz), false)
.unwrap();
assert!(py_datetime.extract::<DateTime<Utc>>().is_err());
})
}
#[test]
fn test_pyo3_offset_fixed_topyobject() {
Python::with_gil(|py| {
let offset = FixedOffset::east_opt(3600).unwrap().to_object(py);
let td = PyDelta::new(py, 0, 3600, 0, true).unwrap();
let py_timedelta = py_timezone_from_offset(&py, td);
assert!(offset.as_ref(py).eq(py_timedelta).unwrap());
let offset = FixedOffset::east_opt(-3600).unwrap().to_object(py);
let td = PyDelta::new(py, 0, -3600, 0, true).unwrap();
let py_timedelta = py_timezone_from_offset(&py, td);
assert!(offset.as_ref(py).eq(py_timedelta).unwrap());
})
}
#[test]
fn test_pyo3_offset_fixed_frompyobject() {
Python::with_gil(|py| {
let py_timedelta = PyDelta::new(py, 0, 3600, 0, true).unwrap();
let py_tzinfo = py_timezone_from_offset(&py, py_timedelta);
let offset: FixedOffset = py_tzinfo.extract().unwrap();
assert_eq!(FixedOffset::east_opt(3600).unwrap(), offset);
})
}
#[test]
fn test_pyo3_offset_utc_topyobject() {
Python::with_gil(|py| {
let utc = Utc.to_object(py);
let py_utc = timezone_utc(py);
assert!(utc.as_ref(py).is(py_utc));
})
}
#[test]
fn test_pyo3_offset_utc_frompyobject() {
Python::with_gil(|py| {
let py_utc = timezone_utc(py);
let py_utc: Utc = py_utc.extract().unwrap();
assert_eq!(Utc, py_utc);
let py_timedelta = PyDelta::new(py, 0, 0, 0, true).unwrap();
let py_timezone_utc = py_timezone_from_offset(&py, py_timedelta);
let py_timezone_utc: Utc = py_timezone_utc.extract().unwrap();
assert_eq!(Utc, py_timezone_utc);
let py_timedelta = PyDelta::new(py, 0, 3600, 0, true).unwrap();
let py_timezone = py_timezone_from_offset(&py, py_timedelta);
assert!(py_timezone.extract::<Utc>().is_err());
})
}
#[test]
fn test_pyo3_time_topyobject() {
let check_time = |name: &'static str, hour, minute, second, ms, py_ms, fold| {
Python::with_gil(|py| {
let time = NaiveTime::from_hms_micro_opt(hour, minute, second, ms)
.unwrap()
.to_object(py);
let time: &PyTime = time.extract(py).unwrap();
let py_time = PyTime::new_with_fold(
py,
hour as u8,
minute as u8,
second as u8,
py_ms,
None,
fold,
)
.unwrap();
assert_eq!(
time.compare(py_time).unwrap(),
Ordering::Equal,
"{}: {} != {}",
name,
time,
py_time
);
})
};
check_time("fold", 3, 5, 59, 1_999_999, 999_999, true);
check_time("non fold", 3, 5, 7, 999_999, 999_999, false);
}
#[test]
fn test_pyo3_time_frompyobject() {
let check_time = |name: &'static str, hour, minute, second, ms, py_ms, fold| {
Python::with_gil(|py| {
let py_time = PyTime::new_with_fold(
py,
hour as u8,
minute as u8,
second as u8,
py_ms,
None,
fold,
)
.unwrap();
let py_time: NaiveTime = py_time.extract().unwrap();
let time = NaiveTime::from_hms_micro_opt(hour, minute, second, ms).unwrap();
assert_eq!(py_time, time, "{}: {} != {}", name, py_time, time);
})
};
check_time("fold", 3, 5, 59, 1_999_999, 999_999, true);
check_time("non fold", 3, 5, 7, 999_999, 999_999, false);
}
#[cfg(all(test, not(target_arch = "wasm32")))]
mod proptests {
use super::*;
use crate::types::IntoPyDict;
use proptest::prelude::*;
proptest! {
#[test]
fn test_pyo3_offset_fixed_frompyobject_created_in_python(timestamp in 0..(i32::MAX as i64), timedelta in -86399i32..=86399i32) {
Python::with_gil(|py| {
let globals = [("datetime", py.import("datetime").unwrap())].into_py_dict(py);
let code = format!("datetime.datetime.fromtimestamp({}).replace(tzinfo=datetime.timezone(datetime.timedelta(seconds={})))", timestamp, timedelta);
let t = py.eval(&code, Some(globals), None).unwrap();
let py_iso_str = t.call_method0("isoformat").unwrap();
let t = t.extract::<DateTime<FixedOffset>>().unwrap();
let rust_iso_str = if timedelta % 60 == 0 {
t.format("%Y-%m-%dT%H:%M:%S%:z").to_string()
} else {
t.format("%Y-%m-%dT%H:%M:%S%::z").to_string()
};
assert_eq!(py_iso_str.to_string(), rust_iso_str);
})
}
#[test]
fn test_duration_roundtrip(days in -999999999i64..=999999999i64) {
Python::with_gil(|py| {
let dur = Duration::days(days);
let pydelta = dur.into_py(py);
let roundtripped: Duration = pydelta.extract(py).expect("Round trip");
assert_eq!(dur, roundtripped);
})
}
#[test]
fn test_fixedoffset_roundtrip(secs in -86399i32..=86399i32) {
Python::with_gil(|py| {
let offset = FixedOffset::east_opt(secs).unwrap();
let pyoffset = offset.into_py(py);
let roundtripped: FixedOffset = pyoffset.extract(py).expect("Round trip");
assert_eq!(offset, roundtripped);
})
}
#[test]
fn test_naivedate_roundtrip(
year in 1i32..=9999i32,
month in 1u32..=12u32,
day in 1u32..=31u32
) {
Python::with_gil(|py| {
if let Some(date) = NaiveDate::from_ymd_opt(year, month, day) {
let pydate = date.to_object(py);
let roundtripped: NaiveDate = pydate.extract(py).expect("Round trip");
assert_eq!(date, roundtripped);
}
})
}
#[test]
fn test_naivetime_roundtrip(
hour in 0u32..=24u32,
min in 0u32..=60u32,
sec in 0u32..=60u32,
micro in 0u32..=2_000_000u32
) {
Python::with_gil(|py| {
if let Some(time) = NaiveTime::from_hms_micro_opt(hour, min, sec, micro) {
let pytime = time.to_object(py);
let roundtripped: NaiveTime = pytime.extract(py).expect("Round trip");
assert_eq!(time, roundtripped);
}
})
}
#[test]
fn test_naive_datetime_roundtrip(
year in 1i32..=9999i32,
month in 1u32..=12u32,
day in 1u32..=31u32,
hour in 0u32..=24u32,
min in 0u32..=60u32,
sec in 0u32..=60u32,
micro in 0u32..=999_999u32
) {
Python::with_gil(|py| {
let date_opt = NaiveDate::from_ymd_opt(year, month, day);
let time_opt = NaiveTime::from_hms_micro_opt(hour, min, sec, micro);
if let (Some(date), Some(time)) = (date_opt, time_opt) {
let dt = NaiveDateTime::new(date, time);
let pydt = dt.to_object(py);
let roundtripped: NaiveDateTime = pydt.extract(py).expect("Round trip");
assert_eq!(dt, roundtripped);
}
})
}
#[test]
fn test_utc_datetime_roundtrip(
year in 1i32..=9999i32,
month in 1u32..=12u32,
day in 1u32..=31u32,
hour in 0u32..=24u32,
min in 0u32..=60u32,
sec in 0u32..=60u32,
micro in 0u32..=2_000_000u32
) {
Python::with_gil(|py| {
let date_opt = NaiveDate::from_ymd_opt(year, month, day);
let time_opt = NaiveTime::from_hms_micro_opt(hour, min, sec, micro);
if let (Some(date), Some(time)) = (date_opt, time_opt) {
let dt: DateTime<Utc> = NaiveDateTime::new(date, time).and_utc();
let pydt = dt.into_py(py);
let roundtripped: DateTime<Utc> = pydt.extract(py).expect("Round trip");
assert_eq!(dt, roundtripped);
}
})
}
#[test]
fn test_fixedoffset_datetime_roundtrip(
year in 1i32..=9999i32,
month in 1u32..=12u32,
day in 1u32..=31u32,
hour in 0u32..=24u32,
min in 0u32..=60u32,
sec in 0u32..=60u32,
micro in 0u32..=1_000_000u32,
offset_secs in -86399i32..=86399i32
) {
Python::with_gil(|py| {
let date_opt = NaiveDate::from_ymd_opt(year, month, day);
let time_opt = NaiveTime::from_hms_micro_opt(hour, min, sec, micro);
let offset = FixedOffset::east_opt(offset_secs).unwrap();
if let (Some(date), Some(time)) = (date_opt, time_opt) {
let dt: DateTime<FixedOffset> = NaiveDateTime::new(date, time).and_local_timezone(offset).unwrap();
let pydt = dt.into_py(py);
let roundtripped: DateTime<FixedOffset> = pydt.extract(py).expect("Round trip");
assert_eq!(dt, roundtripped);
}
})
}
}
}
}