diff --git a/newsfragments/3648.changed.md b/newsfragments/3648.changed.md new file mode 100644 index 00000000000..e2fcf345148 --- /dev/null +++ b/newsfragments/3648.changed.md @@ -0,0 +1 @@ +Add internal `timezone_from_offset` function to construct `datetime.timezone` objects for conversions from `chrono` \ No newline at end of file diff --git a/src/conversions/chrono.rs b/src/conversions/chrono.rs index 9ecbce37ded..536f93f1cb7 100644 --- a/src/conversions/chrono.rs +++ b/src/conversions/chrono.rs @@ -42,15 +42,14 @@ //! ``` use crate::exceptions::{PyTypeError, PyUserWarning, PyValueError}; use crate::types::{ - timezone_utc, PyDate, PyDateAccess, PyDateTime, PyDelta, PyDeltaAccess, PyTime, PyTimeAccess, - PyTzInfo, PyTzInfoAccess, PyUnicode, + timezone_from_offset, timezone_utc, PyDate, PyDateAccess, PyDateTime, PyDelta, PyDeltaAccess, + PyTime, PyTimeAccess, PyTzInfo, PyTzInfoAccess, PyUnicode, }; use crate::{FromPyObject, IntoPy, PyAny, PyErr, 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 { @@ -231,22 +230,14 @@ impl FromPyObject<'_> for DateTime { } } -// Utility function used to convert PyDelta to timezone -fn py_timezone_from_offset<'a>(py: &Python<'a>, td: &PyDelta) -> &'a PyAny { - // Safety: py.from_owned_ptr needs the cast to be valid. - // Since we are forcing a &PyDelta as input, the cast should always be valid. - 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() + timezone_from_offset(py, td) + .expect("Failed to construct PyTimezone") + .into() } } @@ -847,14 +838,14 @@ mod tests { let offset = FixedOffset::east_opt(3600).unwrap().to_object(py); // Python timezone from timedelta let td = PyDelta::new(py, 0, 3600, 0, true).unwrap(); - let py_timedelta = py_timezone_from_offset(&py, td); + let py_timedelta = timezone_from_offset(py, td).unwrap(); // Should be equal assert!(offset.as_ref(py).eq(py_timedelta).unwrap()); // Same but with negative values 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); + let py_timedelta = timezone_from_offset(py, td).unwrap(); assert!(offset.as_ref(py).eq(py_timedelta).unwrap()); }) } @@ -863,7 +854,7 @@ mod tests { 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 py_tzinfo = timezone_from_offset(py, py_timedelta).unwrap(); let offset: FixedOffset = py_tzinfo.extract().unwrap(); assert_eq!(FixedOffset::east_opt(3600).unwrap(), offset); }) @@ -886,12 +877,12 @@ mod tests { 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 = timezone_from_offset(py, py_timedelta).unwrap(); 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); + let py_timezone = timezone_from_offset(py, py_timedelta).unwrap(); assert!(py_timezone.extract::().is_err()); }) } diff --git a/src/types/datetime.rs b/src/types/datetime.rs index a2f9f5cefee..1f9db92a877 100644 --- a/src/types/datetime.rs +++ b/src/types/datetime.rs @@ -21,8 +21,10 @@ use crate::ffi::{ }; use crate::instance::PyNativeType; use crate::types::PyTuple; -use crate::{IntoPy, Py, PyAny, Python}; +use crate::{AsPyPointer, IntoPy, Py, PyAny, Python}; use std::os::raw::c_int; +#[cfg(feature = "chrono")] +use std::ptr; fn ensure_datetime_api(_py: Python<'_>) -> &'static PyDateTime_CAPI { unsafe { @@ -487,6 +489,18 @@ pub fn timezone_utc(py: Python<'_>) -> &PyTzInfo { unsafe { &*(ensure_datetime_api(py).TimeZone_UTC as *const PyTzInfo) } } +/// Equivalent to `datetime.timezone` constructor +/// +/// Only used internally +#[cfg(feature = "chrono")] +pub fn timezone_from_offset<'a>(py: Python<'a>, offset: &PyDelta) -> PyResult<&'a PyTzInfo> { + let api = ensure_datetime_api(py); + unsafe { + let ptr = (api.TimeZone_FromTimeZone)(offset.as_ptr(), ptr::null_mut()); + py.from_owned_ptr_or_err(ptr) + } +} + /// Bindings for `datetime.timedelta` #[repr(transparent)] pub struct PyDelta(PyAny); @@ -620,4 +634,35 @@ mod tests { assert!(t.get_tzinfo().is_none()); }); } + + #[test] + #[cfg(all(feature = "macros", feature = "chrono"))] + #[cfg_attr(target_arch = "wasm32", ignore)] // DateTime import fails on wasm for mysterious reasons + fn test_timezone_from_offset() { + Python::with_gil(|py| { + assert!( + timezone_from_offset(py, PyDelta::new(py, 0, -3600, 0, true).unwrap()) + .unwrap() + .call_method1("utcoffset", ((),)) + .unwrap() + .extract::<&PyDelta>() + .unwrap() + .eq(PyDelta::new(py, 0, -3600, 0, true).unwrap()) + .unwrap() + ); + + assert!( + timezone_from_offset(py, PyDelta::new(py, 0, 3600, 0, true).unwrap()) + .unwrap() + .call_method1("utcoffset", ((),)) + .unwrap() + .extract::<&PyDelta>() + .unwrap() + .eq(PyDelta::new(py, 0, 3600, 0, true).unwrap()) + .unwrap() + ); + + timezone_from_offset(py, PyDelta::new(py, 1, 0, 0, true).unwrap()).unwrap_err(); + }) + } } diff --git a/src/types/mod.rs b/src/types/mod.rs index ef2390cc809..07cc52bc684 100644 --- a/src/types/mod.rs +++ b/src/types/mod.rs @@ -8,6 +8,8 @@ pub use self::capsule::PyCapsule; #[cfg(not(Py_LIMITED_API))] pub use self::code::PyCode; pub use self::complex::PyComplex; +#[cfg(all(feature = "chrono", not(Py_LIMITED_API)))] +pub(crate) use self::datetime::timezone_from_offset; #[cfg(not(Py_LIMITED_API))] pub use self::datetime::{ timezone_utc, PyDate, PyDateAccess, PyDateTime, PyDelta, PyDeltaAccess, PyTime, PyTimeAccess,