Files
j7s_diagnostics_py/vendor/pyo3/src/conversions/chrono.rs
T
2026-09-16 20:10:35 -04:00

1526 lines
55 KiB
Rust
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#![cfg(feature = "chrono")]
//! Conversions to and from [chrono](https://docs.rs/chrono/)’s `Duration`,
//! `NaiveDate`, `NaiveTime`, `DateTime<Tz>`, `FixedOffset`, and `Utc`.
//!
//! # Setup
//!
//! To use this feature, add this to your **`Cargo.toml`**:
//!
//! ```toml
//! [dependencies]
//! chrono = "0.4"
#![doc = concat!("pyo3 = { version = \"", env!("CARGO_PKG_VERSION"), "\", features = [\"chrono\"] }")]
//! ```
//!
//! Note that you must use compatible versions of chrono and PyO3.
//! The required chrono version may vary based on the version of PyO3.
//!
//! # Example: Convert a `datetime.datetime` to chrono's `DateTime<Utc>`
//!
//! ```rust
//! use chrono::{DateTime, Duration, TimeZone, Utc};
//! use pyo3::{Python, PyResult, IntoPyObject, types::PyAnyMethods};
//!
//! fn main() -> PyResult<()> {
//! Python::initialize();
//! Python::attach(|py| {
//! // Build some chrono values
//! let chrono_datetime = Utc.with_ymd_and_hms(2022, 1, 1, 12, 0, 0).unwrap();
//! let chrono_duration = Duration::seconds(1);
//! // Convert them to Python
//! let py_datetime = chrono_datetime.into_pyobject(py)?;
//! let py_timedelta = chrono_duration.into_pyobject(py)?;
//! // Do an operation in Python
//! let py_sum = py_datetime.call_method1("__add__", (py_timedelta,))?;
//! // Convert back to Rust
//! let chrono_sum: DateTime<Utc> = py_sum.extract()?;
//! println!("DateTime<Utc>: {}", chrono_datetime);
//! Ok(())
//! })
//! }
//! ```
use crate::conversion::{FromPyObjectOwned, IntoPyObject};
use crate::exceptions::{PyTypeError, PyUserWarning, PyValueError};
#[cfg(feature = "experimental-inspect")]
use crate::inspect::PyStaticExpr;
use crate::intern;
use crate::types::any::PyAnyMethods;
use crate::types::PyNone;
use crate::types::{PyDate, PyDateTime, PyDelta, PyTime, PyTzInfo, PyTzInfoAccess};
#[cfg(not(Py_LIMITED_API))]
use crate::types::{PyDateAccess, PyDeltaAccess, PyTimeAccess};
#[cfg(feature = "chrono-local")]
use crate::{
exceptions::PyRuntimeError,
sync::PyOnceLock,
types::{PyString, PyStringMethods},
Py,
};
#[cfg(feature = "experimental-inspect")]
use crate::{type_hint_identifier, PyTypeInfo};
use crate::{Borrowed, Bound, FromPyObject, IntoPyObjectExt, PyAny, PyErr, PyResult, Python};
use chrono::offset::{FixedOffset, Utc};
#[cfg(feature = "chrono-local")]
use chrono::Local;
use chrono::{
DateTime, Datelike, Duration, LocalResult, NaiveDate, NaiveDateTime, NaiveTime, Offset,
TimeZone, Timelike,
};
impl<'py> IntoPyObject<'py> for Duration {
type Target = PyDelta;
type Output = Bound<'py, Self::Target>;
type Error = PyErr;
#[cfg(feature = "experimental-inspect")]
const OUTPUT_TYPE: PyStaticExpr = PyDelta::TYPE_HINT;
fn into_pyobject(self, py: Python<'py>) -> Result<Self::Output, Self::Error> {
// Total number of days
let days = self.num_days();
// Remainder of seconds
let secs_dur = self - Duration::days(days);
let secs = secs_dur.num_seconds();
// Fractional part of the microseconds
let micros = (secs_dur - Duration::seconds(secs_dur.num_seconds()))
.num_microseconds()
// This should never panic since we are just getting the fractional
// part of the total microseconds, which should never overflow.
.unwrap();
// We do not need to check the days i64 to i32 cast from rust because
// python will panic with OverflowError.
// We pass true as the `normalize` parameter since we'd need to do several checks here to
// avoid that, and it shouldn't have a big performance impact.
// The seconds and microseconds cast should never overflow since it's at most the number of seconds per day
PyDelta::new(
py,
days.try_into().unwrap_or(i32::MAX),
secs.try_into()?,
micros.try_into()?,
true,
)
}
}
impl<'py> IntoPyObject<'py> for &Duration {
type Target = PyDelta;
type Output = Bound<'py, Self::Target>;
type Error = PyErr;
#[cfg(feature = "experimental-inspect")]
const OUTPUT_TYPE: PyStaticExpr = Duration::OUTPUT_TYPE;
#[inline]
fn into_pyobject(self, py: Python<'py>) -> Result<Self::Output, Self::Error> {
(*self).into_pyobject(py)
}
}
impl FromPyObject<'_, '_> for Duration {
type Error = PyErr;
#[cfg(feature = "experimental-inspect")]
const INPUT_TYPE: PyStaticExpr = PyDelta::TYPE_HINT;
fn extract(ob: Borrowed<'_, '_, PyAny>) -> Result<Self, Self::Error> {
let delta = ob.cast::<PyDelta>()?;
// Python size are much lower than rust size so we do not need bound checks.
// 0 <= microseconds < 1000000
// 0 <= seconds < 3600*24
// -999999999 <= days <= 999999999
#[cfg(not(Py_LIMITED_API))]
let (days, seconds, microseconds) = {
(
delta.get_days().into(),
delta.get_seconds().into(),
delta.get_microseconds().into(),
)
};
#[cfg(Py_LIMITED_API)]
let (days, seconds, microseconds) = {
let py = delta.py();
(
delta.getattr(intern!(py, "days"))?.extract()?,
delta.getattr(intern!(py, "seconds"))?.extract()?,
delta.getattr(intern!(py, "microseconds"))?.extract()?,
)
};
Ok(
Duration::days(days)
+ Duration::seconds(seconds)
+ Duration::microseconds(microseconds),
)
}
}
impl<'py> IntoPyObject<'py> for NaiveDate {
type Target = PyDate;
type Output = Bound<'py, Self::Target>;
type Error = PyErr;
#[cfg(feature = "experimental-inspect")]
const OUTPUT_TYPE: PyStaticExpr = PyDate::TYPE_HINT;
fn into_pyobject(self, py: Python<'py>) -> Result<Self::Output, Self::Error> {
let DateArgs { year, month, day } = (&self).into();
PyDate::new(py, year, month, day)
}
}
impl<'py> IntoPyObject<'py> for &NaiveDate {
type Target = PyDate;
type Output = Bound<'py, Self::Target>;
type Error = PyErr;
#[cfg(feature = "experimental-inspect")]
const OUTPUT_TYPE: PyStaticExpr = NaiveDate::OUTPUT_TYPE;
#[inline]
fn into_pyobject(self, py: Python<'py>) -> Result<Self::Output, Self::Error> {
(*self).into_pyobject(py)
}
}
impl FromPyObject<'_, '_> for NaiveDate {
type Error = PyErr;
#[cfg(feature = "experimental-inspect")]
const INPUT_TYPE: PyStaticExpr = PyDate::TYPE_HINT;
fn extract(ob: Borrowed<'_, '_, PyAny>) -> Result<Self, Self::Error> {
let date = &*ob.cast::<PyDate>()?;
py_date_to_naive_date(date)
}
}
impl<'py> IntoPyObject<'py> for NaiveTime {
type Target = PyTime;
type Output = Bound<'py, Self::Target>;
type Error = PyErr;
#[cfg(feature = "experimental-inspect")]
const OUTPUT_TYPE: PyStaticExpr = PyTime::TYPE_HINT;
fn into_pyobject(self, py: Python<'py>) -> Result<Self::Output, Self::Error> {
let TimeArgs {
hour,
min,
sec,
micro,
truncated_leap_second,
} = (&self).into();
let time = PyTime::new(py, hour, min, sec, micro, None)?;
if truncated_leap_second {
warn_truncated_leap_second(&time);
}
Ok(time)
}
}
impl<'py> IntoPyObject<'py> for &NaiveTime {
type Target = PyTime;
type Output = Bound<'py, Self::Target>;
type Error = PyErr;
#[cfg(feature = "experimental-inspect")]
const OUTPUT_TYPE: PyStaticExpr = NaiveTime::OUTPUT_TYPE;
#[inline]
fn into_pyobject(self, py: Python<'py>) -> Result<Self::Output, Self::Error> {
(*self).into_pyobject(py)
}
}
impl FromPyObject<'_, '_> for NaiveTime {
type Error = PyErr;
#[cfg(feature = "experimental-inspect")]
const INPUT_TYPE: PyStaticExpr = PyTime::TYPE_HINT;
fn extract(ob: Borrowed<'_, '_, PyAny>) -> Result<Self, Self::Error> {
let time = &*ob.cast::<PyTime>()?;
py_time_to_naive_time(time)
}
}
impl<'py> IntoPyObject<'py> for NaiveDateTime {
type Target = PyDateTime;
type Output = Bound<'py, Self::Target>;
type Error = PyErr;
#[cfg(feature = "experimental-inspect")]
const OUTPUT_TYPE: PyStaticExpr = PyDateTime::TYPE_HINT;
fn into_pyobject(self, py: Python<'py>) -> Result<Self::Output, Self::Error> {
let DateArgs { year, month, day } = (&self.date()).into();
let TimeArgs {
hour,
min,
sec,
micro,
truncated_leap_second,
} = (&self.time()).into();
let datetime = PyDateTime::new(py, year, month, day, hour, min, sec, micro, None)?;
if truncated_leap_second {
warn_truncated_leap_second(&datetime);
}
Ok(datetime)
}
}
impl<'py> IntoPyObject<'py> for &NaiveDateTime {
type Target = PyDateTime;
type Output = Bound<'py, Self::Target>;
type Error = PyErr;
#[cfg(feature = "experimental-inspect")]
const OUTPUT_TYPE: PyStaticExpr = NaiveDateTime::OUTPUT_TYPE;
#[inline]
fn into_pyobject(self, py: Python<'py>) -> Result<Self::Output, Self::Error> {
(*self).into_pyobject(py)
}
}
impl FromPyObject<'_, '_> for NaiveDateTime {
type Error = PyErr;
#[cfg(feature = "experimental-inspect")]
const INPUT_TYPE: PyStaticExpr = PyDateTime::TYPE_HINT;
fn extract(dt: Borrowed<'_, '_, PyAny>) -> Result<Self, Self::Error> {
let dt = &*dt.cast::<PyDateTime>()?;
// If the user tries to convert a timezone aware datetime into a naive one,
// we return a hard error. We could silently remove tzinfo, or assume local timezone
// and do a conversion, but better leave this decision to the user of the library.
let has_tzinfo = dt.get_tzinfo().is_some();
if has_tzinfo {
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)?);
Ok(dt)
}
}
impl<'py, Tz: TimeZone> IntoPyObject<'py> for DateTime<Tz>
where
Tz: IntoPyObject<'py>,
{
type Target = PyDateTime;
type Output = Bound<'py, Self::Target>;
type Error = PyErr;
#[cfg(feature = "experimental-inspect")]
const OUTPUT_TYPE: PyStaticExpr = <&DateTime<Tz>>::OUTPUT_TYPE;
#[inline]
fn into_pyobject(self, py: Python<'py>) -> Result<Self::Output, Self::Error> {
(&self).into_pyobject(py)
}
}
impl<'py, Tz: TimeZone> IntoPyObject<'py> for &DateTime<Tz>
where
Tz: IntoPyObject<'py>,
{
type Target = PyDateTime;
type Output = Bound<'py, Self::Target>;
type Error = PyErr;
#[cfg(feature = "experimental-inspect")]
const OUTPUT_TYPE: PyStaticExpr = PyDateTime::TYPE_HINT;
fn into_pyobject(self, py: Python<'py>) -> Result<Self::Output, Self::Error> {
let tz = self.timezone().into_bound_py_any(py)?.cast_into()?;
let DateArgs { year, month, day } = (&self.naive_local().date()).into();
let TimeArgs {
hour,
min,
sec,
micro,
truncated_leap_second,
} = (&self.naive_local().time()).into();
let fold = matches!(
self.timezone().offset_from_local_datetime(&self.naive_local()),
LocalResult::Ambiguous(_, latest) if self.offset().fix() == latest.fix()
);
let datetime = PyDateTime::new_with_fold(
py,
year,
month,
day,
hour,
min,
sec,
micro,
Some(&tz),
fold,
)?;
if truncated_leap_second {
warn_truncated_leap_second(&datetime);
}
Ok(datetime)
}
}
impl<'py, Tz> FromPyObject<'_, 'py> for DateTime<Tz>
where
Tz: TimeZone + FromPyObjectOwned<'py>,
{
type Error = PyErr;
#[cfg(feature = "experimental-inspect")]
const INPUT_TYPE: PyStaticExpr = PyDateTime::TYPE_HINT;
fn extract(dt: Borrowed<'_, 'py, PyAny>) -> Result<Self, Self::Error> {
let dt = &*dt.cast::<PyDateTime>()?;
let tzinfo = dt.get_tzinfo();
let tz = if let Some(tzinfo) = tzinfo {
tzinfo.extract().map_err(Into::into)?
} else {
// Special case: allow naive `datetime` objects for `DateTime<Local>`, interpreting them as local time.
#[cfg(feature = "chrono-local")]
if let Some(tz) = Tz::as_local_tz(crate::conversion::private::Token) {
return py_datetime_to_datetime_with_timezone(dt, tz);
}
return Err(PyTypeError::new_err(
"expected a datetime with non-None tzinfo",
));
};
py_datetime_to_datetime_with_timezone(dt, tz)
}
}
impl<'py> IntoPyObject<'py> for FixedOffset {
type Target = PyTzInfo;
type Output = Bound<'py, Self::Target>;
type Error = PyErr;
#[cfg(feature = "experimental-inspect")]
const OUTPUT_TYPE: PyStaticExpr = type_hint_identifier!("datetime", "timezone");
fn into_pyobject(self, py: Python<'py>) -> Result<Self::Output, Self::Error> {
let seconds_offset = self.local_minus_utc();
let td = PyDelta::new(py, 0, seconds_offset, 0, true)?;
PyTzInfo::fixed_offset(py, td)
}
}
impl<'py> IntoPyObject<'py> for &FixedOffset {
type Target = PyTzInfo;
type Output = Bound<'py, Self::Target>;
type Error = PyErr;
#[cfg(feature = "experimental-inspect")]
const OUTPUT_TYPE: PyStaticExpr = FixedOffset::OUTPUT_TYPE;
#[inline]
fn into_pyobject(self, py: Python<'py>) -> Result<Self::Output, Self::Error> {
(*self).into_pyobject(py)
}
}
impl FromPyObject<'_, '_> for FixedOffset {
type Error = PyErr;
#[cfg(feature = "experimental-inspect")]
const INPUT_TYPE: PyStaticExpr = PyTzInfo::TYPE_HINT;
/// Convert python tzinfo to rust [`FixedOffset`].
///
/// Note that the conversion will result in precision lost in microseconds as chrono offset
/// does not supports microseconds.
fn extract(ob: Borrowed<'_, '_, PyAny>) -> Result<Self, Self::Error> {
let ob = ob.cast::<PyTzInfo>()?;
// Passing Python's None to the `utcoffset` function will only
// work for timezones defined as fixed offsets in Python.
// Any other timezone would require a datetime as the parameter, and return
// None if the datetime is not provided.
// Trying to convert None to a PyDelta in the next line will then fail.
let py_timedelta =
ob.call_method1(intern!(ob.py(), "utcoffset"), (PyNone::get(ob.py()),))?;
if py_timedelta.is_none() {
return Err(PyTypeError::new_err(format!(
"{ob:?} is not a fixed offset timezone"
)));
}
let total_seconds: Duration = py_timedelta.extract()?;
// This cast is safe since the timedelta is limited to -24 hours and 24 hours.
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<'py> IntoPyObject<'py> for Utc {
type Target = PyTzInfo;
type Output = Borrowed<'static, 'py, Self::Target>;
type Error = PyErr;
#[cfg(feature = "experimental-inspect")]
const OUTPUT_TYPE: PyStaticExpr = type_hint_identifier!("datetime", "timezone");
fn into_pyobject(self, py: Python<'py>) -> Result<Self::Output, Self::Error> {
PyTzInfo::utc(py)
}
}
impl<'py> IntoPyObject<'py> for &Utc {
type Target = PyTzInfo;
type Output = Borrowed<'static, 'py, Self::Target>;
type Error = PyErr;
#[cfg(feature = "experimental-inspect")]
const OUTPUT_TYPE: PyStaticExpr = Utc::OUTPUT_TYPE;
#[inline]
fn into_pyobject(self, py: Python<'py>) -> Result<Self::Output, Self::Error> {
(*self).into_pyobject(py)
}
}
impl FromPyObject<'_, '_> for Utc {
type Error = PyErr;
#[cfg(feature = "experimental-inspect")]
const INPUT_TYPE: PyStaticExpr = Utc::OUTPUT_TYPE;
fn extract(ob: Borrowed<'_, '_, PyAny>) -> Result<Self, Self::Error> {
let py_utc = Utc.into_pyobject(ob.py())?;
if ob.eq(py_utc)? {
Ok(Utc)
} else {
Err(PyValueError::new_err("expected datetime.timezone.utc"))
}
}
}
#[cfg(feature = "chrono-local")]
impl<'py> IntoPyObject<'py> for Local {
type Target = PyTzInfo;
type Output = Borrowed<'static, 'py, Self::Target>;
type Error = PyErr;
#[cfg(all(feature = "experimental-inspect", Py_3_9))]
const OUTPUT_TYPE: PyStaticExpr = type_hint_identifier!("zoneinfo", "ZoneInfo");
#[cfg(all(feature = "experimental-inspect", not(Py_3_9)))]
const OUTPUT_TYPE: PyStaticExpr = PyTzInfo::TYPE_HINT;
fn into_pyobject(self, py: Python<'py>) -> Result<Self::Output, Self::Error> {
static LOCAL_TZ: PyOnceLock<Py<PyTzInfo>> = PyOnceLock::new();
let tz = LOCAL_TZ
.get_or_try_init(py, || {
let iana_name = iana_time_zone::get_timezone().map_err(|e| {
PyRuntimeError::new_err(format!("Could not get local timezone: {e}"))
})?;
PyTzInfo::timezone(py, iana_name).map(Bound::unbind)
})?
.bind_borrowed(py);
Ok(tz)
}
}
#[cfg(feature = "chrono-local")]
impl<'py> IntoPyObject<'py> for &Local {
type Target = PyTzInfo;
type Output = Borrowed<'static, 'py, Self::Target>;
type Error = PyErr;
#[cfg(feature = "experimental-inspect")]
const OUTPUT_TYPE: PyStaticExpr = Local::OUTPUT_TYPE;
#[inline]
fn into_pyobject(self, py: Python<'py>) -> Result<Self::Output, Self::Error> {
(*self).into_pyobject(py)
}
}
#[cfg(feature = "chrono-local")]
impl FromPyObject<'_, '_> for Local {
type Error = PyErr;
#[cfg(feature = "experimental-inspect")]
const INPUT_TYPE: PyStaticExpr = Local::OUTPUT_TYPE;
fn extract(ob: Borrowed<'_, '_, PyAny>) -> PyResult<Local> {
let local_tz = Local.into_pyobject(ob.py())?;
if ob.eq(local_tz)? {
Ok(Local)
} else {
let name = local_tz.getattr("key")?.cast_into::<PyString>()?;
Err(PyValueError::new_err(format!(
"expected local timezone {}",
name.to_cow()?
)))
}
}
#[inline]
fn as_local_tz(_: crate::conversion::private::Token) -> Option<Self> {
Some(Local)
}
}
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,
truncated_leap_second: 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 truncated_leap_second = 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,
truncated_leap_second,
}
}
}
fn warn_truncated_leap_second(obj: &Bound<'_, PyAny>) {
let py = obj.py();
if let Err(e) = PyErr::warn(
py,
&py.get_type::<PyUserWarning>(),
c"ignored leap-second, `datetime` does not support leap-seconds",
0,
) {
e.write_unraisable(py, Some(obj))
};
}
#[cfg(not(Py_LIMITED_API))]
fn py_date_to_naive_date(
py_date: impl core::ops::Deref<Target = 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"))
}
#[cfg(Py_LIMITED_API)]
fn py_date_to_naive_date(py_date: &Bound<'_, PyAny>) -> PyResult<NaiveDate> {
NaiveDate::from_ymd_opt(
py_date.getattr(intern!(py_date.py(), "year"))?.extract()?,
py_date.getattr(intern!(py_date.py(), "month"))?.extract()?,
py_date.getattr(intern!(py_date.py(), "day"))?.extract()?,
)
.ok_or_else(|| PyValueError::new_err("invalid or out-of-range date"))
}
#[cfg(not(Py_LIMITED_API))]
fn py_time_to_naive_time(
py_time: impl core::ops::Deref<Target = impl PyTimeAccess>,
) -> 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(),
)
.ok_or_else(|| PyValueError::new_err("invalid or out-of-range time"))
}
#[cfg(Py_LIMITED_API)]
fn py_time_to_naive_time(py_time: &Bound<'_, PyAny>) -> PyResult<NaiveTime> {
NaiveTime::from_hms_micro_opt(
py_time.getattr(intern!(py_time.py(), "hour"))?.extract()?,
py_time
.getattr(intern!(py_time.py(), "minute"))?
.extract()?,
py_time
.getattr(intern!(py_time.py(), "second"))?
.extract()?,
py_time
.getattr(intern!(py_time.py(), "microsecond"))?
.extract()?,
)
.ok_or_else(|| PyValueError::new_err("invalid or out-of-range time"))
}
fn py_datetime_to_datetime_with_timezone<Tz: TimeZone>(
dt: &Bound<'_, PyDateTime>,
tz: Tz,
) -> PyResult<DateTime<Tz>> {
let naive_dt = NaiveDateTime::new(py_date_to_naive_date(dt)?, py_time_to_naive_time(dt)?);
match naive_dt.and_local_timezone(tz) {
LocalResult::Single(value) => Ok(value),
LocalResult::Ambiguous(earliest, latest) => {
#[cfg(not(Py_LIMITED_API))]
let fold = dt.get_fold();
#[cfg(Py_LIMITED_API)]
let fold = dt.getattr(intern!(dt.py(), "fold"))?.extract::<usize>()? > 0;
if fold {
Ok(latest)
} else {
Ok(earliest)
}
}
LocalResult::None => Err(PyValueError::new_err(format!(
"The datetime {dt:?} contains an incompatible timezone"
))),
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::{test_utils::assert_warnings, types::PyTuple, BoundObject};
use core::cmp::Ordering;
use std::panic;
#[test]
// Only Python>=3.9 has the zoneinfo package
// We skip the test on windows too since we'd need to install
// tzdata there to make this work.
#[cfg(all(Py_3_9, not(target_os = "windows")))]
fn test_zoneinfo_is_not_fixed_offset() {
use crate::types::any::PyAnyMethods;
use crate::types::dict::PyDictMethods;
Python::attach(|py| {
let locals = crate::types::PyDict::new(py);
py.run(
c"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();
// Also check the error message is what we expect
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() {
// Test that if a user tries to convert a python's timezone aware datetime into a naive
// one, the conversion fails.
Python::attach(|py| {
let py_datetime =
new_py_datetime_ob(py, "datetime", (2022, 1, 1, 1, 0, 0, 0, python_utc(py)));
// Now test that converting a PyDateTime with tzinfo to a NaiveDateTime fails
let res: PyResult<NaiveDateTime> = py_datetime.extract();
assert_eq!(
res.unwrap_err().value(py).repr().unwrap().to_string(),
"TypeError('expected a datetime without tzinfo')"
);
});
}
#[test]
fn test_naive_to_timezone_aware_fails() {
// Test that if a user tries to convert a python's timezone aware datetime into a naive
// one, the conversion fails.
Python::attach(|py| {
let py_datetime = new_py_datetime_ob(py, "datetime", (2022, 1, 1, 1, 0, 0, 0));
// Now test that converting a PyDateTime with tzinfo to a NaiveDateTime fails
let res: PyResult<DateTime<Utc>> = py_datetime.extract();
assert_eq!(
res.unwrap_err().value(py).repr().unwrap().to_string(),
"TypeError('expected a datetime with non-None tzinfo')"
);
// Now test that converting a PyDateTime with tzinfo to a NaiveDateTime fails
let res: PyResult<DateTime<FixedOffset>> = py_datetime.extract();
assert_eq!(
res.unwrap_err().value(py).repr().unwrap().to_string(),
"TypeError('expected a datetime with non-None tzinfo')"
);
});
}
#[test]
fn test_invalid_types_fail() {
// Test that if a user tries to convert a python's timezone aware datetime into a naive
// one, the conversion fails.
Python::attach(|py| {
let none = py.None().into_bound(py);
assert_eq!(
none.extract::<Duration>().unwrap_err().to_string(),
"TypeError: 'None' is not an instance of 'timedelta'"
);
assert_eq!(
none.extract::<FixedOffset>().unwrap_err().to_string(),
"TypeError: 'None' is not an instance of 'tzinfo'"
);
assert_eq!(
none.extract::<Utc>().unwrap_err().to_string(),
"ValueError: expected datetime.timezone.utc"
);
assert_eq!(
none.extract::<NaiveTime>().unwrap_err().to_string(),
"TypeError: 'None' is not an instance of 'time'"
);
assert_eq!(
none.extract::<NaiveDate>().unwrap_err().to_string(),
"TypeError: 'None' is not an instance of 'date'"
);
assert_eq!(
none.extract::<NaiveDateTime>().unwrap_err().to_string(),
"TypeError: 'None' is not an instance of 'datetime'"
);
assert_eq!(
none.extract::<DateTime<Utc>>().unwrap_err().to_string(),
"TypeError: 'None' is not an instance of 'datetime'"
);
assert_eq!(
none.extract::<DateTime<FixedOffset>>()
.unwrap_err()
.to_string(),
"TypeError: 'None' is not an instance of 'datetime'"
);
});
}
#[test]
fn test_pyo3_timedelta_into_pyobject() {
// Utility function used to check different durations.
// The `name` parameter is used to identify the check in case of a failure.
let check = |name: &'static str, delta: Duration, py_days, py_seconds, py_ms| {
Python::attach(|py| {
let delta = delta.into_pyobject(py).unwrap();
let py_delta = new_py_datetime_ob(py, "timedelta", (py_days, py_seconds, py_ms));
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);
// Check the minimum value allowed by PyDelta, which is different
// from the minimum value allowed in Duration. This should pass.
let delta = Duration::seconds(-86399999913600); // min
check("delta min value", delta, -999999999, 0, 0);
// Same, for max value
let delta = Duration::seconds(86399999999999) + Duration::nanoseconds(999999000); // max
check("delta max value", delta, 999999999, 86399, 999999);
// Also check that trying to convert an out of bound value errors.
Python::attach(|py| {
// min_value and max_value were deprecated in chrono 0.4.39
#[allow(deprecated)]
{
assert!(Duration::min_value().into_pyobject(py).is_err());
assert!(Duration::max_value().into_pyobject(py).is_err());
}
});
}
#[test]
fn test_pyo3_timedelta_frompyobject() {
// Utility function used to check different durations.
// The `name` parameter is used to identify the check in case of a failure.
let check = |name: &'static str, delta: Duration, py_days, py_seconds, py_ms| {
Python::attach(|py| {
let py_delta = new_py_datetime_ob(py, "timedelta", (py_days, py_seconds, py_ms));
let py_delta: Duration = py_delta.extract().unwrap();
assert_eq!(py_delta, delta, "{name}: {py_delta} != {delta}");
})
};
// Check the minimum value allowed by PyDelta, which is different
// from the minimum value allowed in Duration. This should pass.
check(
"min py_delta value",
Duration::seconds(-86399999913600),
-999999999,
0,
0,
);
// Same, for max value
check(
"max py_delta value",
Duration::seconds(86399999999999) + Duration::microseconds(999999),
999999999,
86399,
999999,
);
// This check is to assert that we can't construct every possible Duration from a PyDelta
// since they have different bounds.
Python::attach(|py| {
let low_days: i32 = -1000000000;
// This is possible
assert!(panic::catch_unwind(|| Duration::days(low_days as i64)).is_ok());
// This panics on PyDelta::new
assert!(panic::catch_unwind(|| {
let py_delta = new_py_datetime_ob(py, "timedelta", (low_days, 0, 0));
if let Ok(_duration) = py_delta.extract::<Duration>() {
// So we should never get here
}
})
.is_err());
let high_days: i32 = 1000000000;
// This is possible
assert!(panic::catch_unwind(|| Duration::days(high_days as i64)).is_ok());
// This panics on PyDelta::new
assert!(panic::catch_unwind(|| {
let py_delta = new_py_datetime_ob(py, "timedelta", (high_days, 0, 0));
if let Ok(_duration) = py_delta.extract::<Duration>() {
// So we should never get here
}
})
.is_err());
});
}
#[test]
fn test_pyo3_date_into_pyobject() {
let eq_ymd = |name: &'static str, year, month, day| {
Python::attach(|py| {
let date = NaiveDate::from_ymd_opt(year, month, day)
.unwrap()
.into_pyobject(py)
.unwrap();
let py_date = new_py_datetime_ob(py, "date", (year, month, day));
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::attach(|py| {
let py_date = new_py_datetime_ob(py, "date", (year, month, day));
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_into_pyobject_utc() {
Python::attach(|py| {
let check_utc =
|name: &'static str, year, month, day, hour, minute, second, ms, py_ms| {
let datetime = NaiveDate::from_ymd_opt(year, month, day)
.unwrap()
.and_hms_micro_opt(hour, minute, second, ms)
.unwrap()
.and_utc();
let datetime = datetime.into_pyobject(py).unwrap();
let py_datetime = new_py_datetime_ob(
py,
"datetime",
(
year,
month,
day,
hour,
minute,
second,
py_ms,
python_utc(py),
),
);
assert_eq!(
datetime.compare(&py_datetime).unwrap(),
Ordering::Equal,
"{name}: {datetime} != {py_datetime}"
);
};
check_utc("regular", 2014, 5, 6, 7, 8, 9, 999_999, 999_999);
assert_warnings!(
py,
check_utc("leap second", 2014, 5, 6, 7, 8, 59, 1_999_999, 999_999),
[(
PyUserWarning,
"ignored leap-second, `datetime` does not support leap-seconds"
)]
);
})
}
#[test]
fn test_pyo3_datetime_into_pyobject_fixed_offset() {
Python::attach(|py| {
let check_fixed_offset =
|name: &'static str, year, month, day, hour, minute, second, ms, py_ms| {
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.into_pyobject(py).unwrap();
let py_tz = offset.into_pyobject(py).unwrap();
let py_datetime = new_py_datetime_ob(
py,
"datetime",
(year, month, day, hour, minute, second, py_ms, py_tz),
);
assert_eq!(
datetime.compare(&py_datetime).unwrap(),
Ordering::Equal,
"{name}: {datetime} != {py_datetime}"
);
};
check_fixed_offset("regular", 2014, 5, 6, 7, 8, 9, 999_999, 999_999);
assert_warnings!(
py,
check_fixed_offset("leap second", 2014, 5, 6, 7, 8, 59, 1_999_999, 999_999),
[(
PyUserWarning,
"ignored leap-second, `datetime` does not support leap-seconds"
)]
);
})
}
#[test]
#[cfg(all(Py_3_9, feature = "chrono-tz", not(windows)))]
fn test_pyo3_datetime_into_pyobject_tz() {
Python::attach(|py| {
let datetime = NaiveDate::from_ymd_opt(2024, 12, 11)
.unwrap()
.and_hms_opt(23, 3, 13)
.unwrap()
.and_local_timezone(chrono_tz::Tz::Europe__London)
.unwrap();
let datetime = datetime.into_pyobject(py).unwrap();
let py_datetime = new_py_datetime_ob(
py,
"datetime",
(
2024,
12,
11,
23,
3,
13,
0,
python_zoneinfo(py, "Europe/London"),
),
);
assert_eq!(datetime.compare(&py_datetime).unwrap(), Ordering::Equal);
})
}
#[test]
fn test_pyo3_datetime_frompyobject_utc() {
Python::attach(|py| {
let year = 2014;
let month = 5;
let day = 6;
let hour = 7;
let minute = 8;
let second = 9;
let micro = 999_999;
let tz_utc = PyTzInfo::utc(py).unwrap();
let py_datetime = new_py_datetime_ob(
py,
"datetime",
(year, month, day, hour, minute, second, micro, tz_utc),
);
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, micro)
.unwrap()
.and_utc();
assert_eq!(py_datetime, datetime,);
})
}
#[test]
#[cfg(feature = "chrono-local")]
fn test_pyo3_naive_datetime_frompyobject_local() {
Python::attach(|py| {
let year = 2014;
let month = 5;
let day = 6;
let hour = 7;
let minute = 8;
let second = 9;
let micro = 999_999;
let py_datetime = new_py_datetime_ob(
py,
"datetime",
(year, month, day, hour, minute, second, micro),
);
let py_datetime: DateTime<Local> = py_datetime.extract().unwrap();
let expected_datetime = NaiveDate::from_ymd_opt(year, month, day)
.unwrap()
.and_hms_micro_opt(hour, minute, second, micro)
.unwrap()
.and_local_timezone(Local)
.unwrap();
assert_eq!(py_datetime, expected_datetime);
})
}
#[test]
fn test_pyo3_datetime_frompyobject_fixed_offset() {
Python::attach(|py| {
let year = 2014;
let month = 5;
let day = 6;
let hour = 7;
let minute = 8;
let second = 9;
let micro = 999_999;
let offset = FixedOffset::east_opt(3600).unwrap();
let py_tz = offset.into_pyobject(py).unwrap();
let py_datetime = new_py_datetime_ob(
py,
"datetime",
(year, month, day, hour, minute, second, micro, py_tz),
);
let datetime_from_py: DateTime<FixedOffset> = py_datetime.extract().unwrap();
let datetime = NaiveDate::from_ymd_opt(year, month, day)
.unwrap()
.and_hms_micro_opt(hour, minute, second, micro)
.unwrap();
let datetime = datetime.and_local_timezone(offset).unwrap();
assert_eq!(datetime_from_py, datetime);
assert!(
py_datetime.extract::<DateTime<Utc>>().is_err(),
"Extracting Utc from nonzero FixedOffset timezone will fail"
);
let utc = python_utc(py);
let py_datetime_utc = new_py_datetime_ob(
py,
"datetime",
(year, month, day, hour, minute, second, micro, utc),
);
assert!(
py_datetime_utc.extract::<DateTime<FixedOffset>>().is_ok(),
"Extracting FixedOffset from Utc timezone will succeed"
);
})
}
#[test]
fn test_pyo3_offset_fixed_into_pyobject() {
Python::attach(|py| {
// Chrono offset
let offset = FixedOffset::east_opt(3600)
.unwrap()
.into_pyobject(py)
.unwrap();
// Python timezone from timedelta
let td = new_py_datetime_ob(py, "timedelta", (0, 3600, 0));
let py_timedelta = new_py_datetime_ob(py, "timezone", (td,));
// Should be equal
assert!(offset.eq(py_timedelta).unwrap());
// Same but with negative values
let offset = FixedOffset::east_opt(-3600)
.unwrap()
.into_pyobject(py)
.unwrap();
let td = new_py_datetime_ob(py, "timedelta", (0, -3600, 0));
let py_timedelta = new_py_datetime_ob(py, "timezone", (td,));
assert!(offset.eq(py_timedelta).unwrap());
})
}
#[test]
fn test_pyo3_offset_fixed_frompyobject() {
Python::attach(|py| {
let py_timedelta = new_py_datetime_ob(py, "timedelta", (0, 3600, 0));
let py_tzinfo = new_py_datetime_ob(py, "timezone", (py_timedelta,));
let offset: FixedOffset = py_tzinfo.extract().unwrap();
assert_eq!(FixedOffset::east_opt(3600).unwrap(), offset);
})
}
#[test]
fn test_pyo3_offset_utc_into_pyobject() {
Python::attach(|py| {
let utc = Utc.into_pyobject(py).unwrap();
let py_utc = python_utc(py);
assert!(utc.is(&py_utc));
})
}
#[test]
fn test_pyo3_offset_utc_frompyobject() {
Python::attach(|py| {
let py_utc = python_utc(py);
let py_utc: Utc = py_utc.extract().unwrap();
assert_eq!(Utc, py_utc);
let py_timedelta = new_py_datetime_ob(py, "timedelta", (0, 0, 0));
let py_timezone_utc = new_py_datetime_ob(py, "timezone", (py_timedelta,));
let py_timezone_utc: Utc = py_timezone_utc.extract().unwrap();
assert_eq!(Utc, py_timezone_utc);
let py_timedelta = new_py_datetime_ob(py, "timedelta", (0, 3600, 0));
let py_timezone = new_py_datetime_ob(py, "timezone", (py_timedelta,));
assert!(py_timezone.extract::<Utc>().is_err());
})
}
#[test]
fn test_pyo3_time_into_pyobject() {
Python::attach(|py| {
let check_time = |name: &'static str, hour, minute, second, ms, py_ms| {
let time = NaiveTime::from_hms_micro_opt(hour, minute, second, ms)
.unwrap()
.into_pyobject(py)
.unwrap();
let py_time = new_py_datetime_ob(py, "time", (hour, minute, second, py_ms));
assert!(time.eq(&py_time).unwrap(), "{name}: {time} != {py_time}");
};
check_time("regular", 3, 5, 7, 999_999, 999_999);
assert_warnings!(
py,
check_time("leap second", 3, 5, 59, 1_999_999, 999_999),
[(
PyUserWarning,
"ignored leap-second, `datetime` does not support leap-seconds"
)]
);
})
}
#[test]
fn test_pyo3_time_frompyobject() {
let hour = 3;
let minute = 5;
let second = 7;
let micro = 999_999;
Python::attach(|py| {
let py_time = new_py_datetime_ob(py, "time", (hour, minute, second, micro));
let py_time: NaiveTime = py_time.extract().unwrap();
let time = NaiveTime::from_hms_micro_opt(hour, minute, second, micro).unwrap();
assert_eq!(py_time, time);
})
}
fn new_py_datetime_ob<'py, A>(py: Python<'py>, name: &str, args: A) -> Bound<'py, PyAny>
where
A: IntoPyObject<'py, Target = PyTuple>,
{
py.import("datetime")
.unwrap()
.getattr(name)
.unwrap()
.call1(
args.into_pyobject(py)
.map_err(Into::into)
.unwrap()
.into_bound(),
)
.unwrap()
}
fn python_utc(py: Python<'_>) -> Bound<'_, PyAny> {
py.import("datetime")
.unwrap()
.getattr("timezone")
.unwrap()
.getattr("utc")
.unwrap()
}
#[cfg(all(Py_3_9, feature = "chrono-tz", not(windows)))]
fn python_zoneinfo<'py>(py: Python<'py>, timezone: &str) -> Bound<'py, PyAny> {
py.import("zoneinfo")
.unwrap()
.getattr("ZoneInfo")
.unwrap()
.call1((timezone,))
.unwrap()
}
#[cfg(not(any(target_arch = "wasm32")))]
mod proptests {
use super::*;
use crate::test_utils::CatchWarnings;
use crate::types::IntoPyDict;
use alloc::ffi::CString;
use proptest::prelude::*;
proptest! {
// Range is limited to 1970 to 2038 due to windows limitations
#[test]
fn test_pyo3_offset_fixed_frompyobject_created_in_python(timestamp in 0..(i32::MAX as i64), timedelta in -86399i32..=86399i32) {
Python::attach(|py| {
let globals = [("datetime", py.import("datetime").unwrap())].into_py_dict(py).unwrap();
let code = format!("datetime.datetime.fromtimestamp({timestamp}).replace(tzinfo=datetime.timezone(datetime.timedelta(seconds={timedelta})))");
let t = py.eval(&CString::new(code).unwrap(), Some(&globals), None).unwrap();
// Get ISO 8601 string from python
let py_iso_str = t.call_method0("isoformat").unwrap();
// Get ISO 8601 string from rust
let t = t.extract::<DateTime<FixedOffset>>().unwrap();
// Python doesn't print the seconds of the offset if they are 0
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()
};
// They should be equal
assert_eq!(py_iso_str.to_string(), rust_iso_str);
})
}
#[test]
fn test_duration_roundtrip(days in -999999999i64..=999999999i64) {
// Test roundtrip conversion rust->python->rust for all allowed
// python values of durations (from -999999999 to 999999999 days),
Python::attach(|py| {
let dur = Duration::days(days);
let py_delta = dur.into_pyobject(py).unwrap();
let roundtripped: Duration = py_delta.extract().expect("Round trip");
assert_eq!(dur, roundtripped);
})
}
#[test]
fn test_fixed_offset_roundtrip(secs in -86399i32..=86399i32) {
Python::attach(|py| {
let offset = FixedOffset::east_opt(secs).unwrap();
let py_offset = offset.into_pyobject(py).unwrap();
let roundtripped: FixedOffset = py_offset.extract().expect("Round trip");
assert_eq!(offset, roundtripped);
})
}
#[test]
fn test_naive_date_roundtrip(
year in 1i32..=9999i32,
month in 1u32..=12u32,
day in 1u32..=31u32
) {
// Test roundtrip conversion rust->python->rust for all allowed
// python dates (from year 1 to year 9999)
Python::attach(|py| {
// We use to `from_ymd_opt` constructor so that we only test valid `NaiveDate`s.
// This is to skip the test if we are creating an invalid date, like February 31.
if let Some(date) = NaiveDate::from_ymd_opt(year, month, day) {
let py_date = date.into_pyobject(py).unwrap();
let roundtripped: NaiveDate = py_date.extract().expect("Round trip");
assert_eq!(date, roundtripped);
}
})
}
#[test]
fn test_naive_time_roundtrip(
hour in 0u32..=23u32,
min in 0u32..=59u32,
sec in 0u32..=59u32,
micro in 0u32..=1_999_999u32
) {
// Test roundtrip conversion rust->python->rust for naive times.
// Python time has a resolution of microseconds, so we only test
// NaiveTimes with microseconds resolution, even if NaiveTime has nanosecond
// resolution.
Python::attach(|py| {
if let Some(time) = NaiveTime::from_hms_micro_opt(hour, min, sec, micro) {
// Wrap in CatchWarnings to avoid to_object firing warning for truncated leap second
let py_time = CatchWarnings::enter(py, |_| time.into_pyobject(py)).unwrap();
let roundtripped: NaiveTime = py_time.extract().expect("Round trip");
// Leap seconds are not roundtripped
let expected_roundtrip_time = micro.checked_sub(1_000_000).map(|micro| NaiveTime::from_hms_micro_opt(hour, min, sec, micro).unwrap()).unwrap_or(time);
assert_eq!(expected_roundtrip_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::attach(|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.into_pyobject(py).unwrap();
let roundtripped: NaiveDateTime = pydt.extract().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..=23u32,
min in 0u32..=59u32,
sec in 0u32..=59u32,
micro in 0u32..=1_999_999u32
) {
Python::attach(|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();
// Wrap in CatchWarnings to avoid into_py firing warning for truncated leap second
let py_dt = CatchWarnings::enter(py, |_| dt.into_pyobject(py)).unwrap();
let roundtripped: DateTime<Utc> = py_dt.extract().expect("Round trip");
// Leap seconds are not roundtripped
let expected_roundtrip_time = micro.checked_sub(1_000_000).map(|micro| NaiveTime::from_hms_micro_opt(hour, min, sec, micro).unwrap()).unwrap_or(time);
let expected_roundtrip_dt: DateTime<Utc> = NaiveDateTime::new(date, expected_roundtrip_time).and_utc();
assert_eq!(expected_roundtrip_dt, roundtripped);
}
})
}
#[test]
fn test_fixed_offset_datetime_roundtrip(
year in 1i32..=9999i32,
month in 1u32..=12u32,
day in 1u32..=31u32,
hour in 0u32..=23u32,
min in 0u32..=59u32,
sec in 0u32..=59u32,
micro in 0u32..=1_999_999u32,
offset_secs in -86399i32..=86399i32
) {
Python::attach(|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();
// Wrap in CatchWarnings to avoid into_py firing warning for truncated leap second
let py_dt = CatchWarnings::enter(py, |_| dt.into_pyobject(py)).unwrap();
let roundtripped: DateTime<FixedOffset> = py_dt.extract().expect("Round trip");
// Leap seconds are not roundtripped
let expected_roundtrip_time = micro.checked_sub(1_000_000).map(|micro| NaiveTime::from_hms_micro_opt(hour, min, sec, micro).unwrap()).unwrap_or(time);
let expected_roundtrip_dt: DateTime<FixedOffset> = NaiveDateTime::new(date, expected_roundtrip_time).and_local_timezone(offset).unwrap();
assert_eq!(expected_roundtrip_dt, roundtripped);
}
})
}
#[test]
#[cfg(all(feature = "chrono-local", not(target_os = "windows")))]
fn test_local_datetime_roundtrip(
year in 1i32..=9999i32,
month in 1u32..=12u32,
day in 1u32..=31u32,
hour in 0u32..=23u32,
min in 0u32..=59u32,
sec in 0u32..=59u32,
micro in 0u32..=1_999_999u32,
) {
Python::attach(|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 dts = match NaiveDateTime::new(date, time).and_local_timezone(Local) {
LocalResult::None => return,
LocalResult::Single(dt) => [Some((dt, false)), None],
LocalResult::Ambiguous(dt1, dt2) => [Some((dt1, false)), Some((dt2, true))],
};
for (dt, fold) in dts.iter().filter_map(|input| *input) {
// Wrap in CatchWarnings to avoid into_py firing warning for truncated leap second
let py_dt = CatchWarnings::enter(py, |_| dt.into_pyobject(py)).unwrap();
let roundtripped: DateTime<Local> = py_dt.extract().expect("Round trip");
// Leap seconds are not roundtripped
let expected_roundtrip_time = micro.checked_sub(1_000_000).map(|micro| NaiveTime::from_hms_micro_opt(hour, min, sec, micro).unwrap()).unwrap_or(time);
let expected_roundtrip_dt: DateTime<Local> = if fold {
NaiveDateTime::new(date, expected_roundtrip_time).and_local_timezone(Local).latest()
} else {
NaiveDateTime::new(date, expected_roundtrip_time).and_local_timezone(Local).earliest()
}.unwrap();
assert_eq!(expected_roundtrip_dt, roundtripped);
}
}
})
}
}
}
}