From 8ec500b4dcc668e81db410fe11652fd26732532d Mon Sep 17 00:00:00 2001 From: Tpt Date: Thu, 14 Dec 2023 16:50:52 +0100 Subject: [PATCH] Adds timezone_from_offset and timezone_from_offset_and_name functions Exposes C functions provided by datetime.h It allows to build conversions from chrono without direct access to the C API --- newsfragments/3648.added.md | 1 + src/conversions/chrono.rs | 29 ++++------ src/types/datetime.rs | 104 +++++++++++++++++++++++++++++++++++- src/types/mod.rs | 4 +- 4 files changed, 115 insertions(+), 23 deletions(-) create mode 100644 newsfragments/3648.added.md diff --git a/newsfragments/3648.added.md b/newsfragments/3648.added.md new file mode 100644 index 00000000000..74bcb62b720 --- /dev/null +++ b/newsfragments/3648.added.md @@ -0,0 +1 @@ +Add `timezone_from_offset` and `timezone_from_offset_and_name` to construct `datetime.timezone` objects \ 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..f2cf240d69b 100644 --- a/src/types/datetime.rs +++ b/src/types/datetime.rs @@ -20,9 +20,10 @@ use crate::ffi::{ PyDateTime_TIME_GET_MINUTE, PyDateTime_TIME_GET_SECOND, }; use crate::instance::PyNativeType; -use crate::types::PyTuple; -use crate::{IntoPy, Py, PyAny, Python}; +use crate::types::{PyString, PyTuple}; +use crate::{AsPyPointer, IntoPy, Py, PyAny, Python}; use std::os::raw::c_int; +use std::ptr; fn ensure_datetime_api(_py: Python<'_>) -> &'static PyDateTime_CAPI { unsafe { @@ -487,6 +488,32 @@ pub fn timezone_utc(py: Python<'_>) -> &PyTzInfo { unsafe { &*(ensure_datetime_api(py).TimeZone_UTC as *const PyTzInfo) } } +/// Equivalent to `datetime.timezone` constructor +/// +/// Use [`timezone_from_offset_and_name`] for a named timezones. +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) + } +} + +/// Equivalent to `datetime.timezone` constructor with a timezone name +/// +/// Use [`timezone_from_offset`] for offset-only timezones. +pub fn timezone_from_offset_and_name<'a>( + py: Python<'a>, + offset: &PyDelta, + name: &PyString, +) -> PyResult<&'a PyTzInfo> { + let api = ensure_datetime_api(py); + unsafe { + let ptr = (api.TimeZone_FromTimeZone)(offset.as_ptr(), name.as_ptr()); + py.from_owned_ptr_or_err(ptr) + } +} + /// Bindings for `datetime.timedelta` #[repr(transparent)] pub struct PyDelta(PyAny); @@ -620,4 +647,77 @@ mod tests { assert!(t.get_tzinfo().is_none()); }); } + + #[test] + #[cfg(feature = "macros")] + #[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_eq( + py, + timezone_from_offset(py, PyDelta::new(py, 0, -3600, 0, true).unwrap()).unwrap(), + -3600, + "UTC-01:00", + ); + assert_timezone_eq( + py, + timezone_from_offset(py, PyDelta::new(py, 0, 3600, 0, true).unwrap()).unwrap(), + 3600, + "UTC+01:00", + ); + timezone_from_offset(py, PyDelta::new(py, 1, 0, 0, true).unwrap()).unwrap_err(); + }) + } + + #[test] + #[cfg(feature = "macros")] + #[cfg_attr(target_arch = "wasm32", ignore)] // DateTime import fails on wasm for mysterious reasons + fn test_timezone_from_offset_and_name() { + Python::with_gil(|py| { + let name = PyString::new(py, "foo"); + assert_timezone_eq( + py, + timezone_from_offset_and_name( + py, + PyDelta::new(py, 0, -3600, 0, true).unwrap(), + name, + ) + .unwrap(), + -3600, + "foo", + ); + assert_timezone_eq( + py, + timezone_from_offset_and_name( + py, + PyDelta::new(py, 0, 3600, 0, true).unwrap(), + name, + ) + .unwrap(), + 3600, + "foo", + ); + timezone_from_offset_and_name(py, PyDelta::new(py, 1, 0, 0, true).unwrap(), name) + .unwrap_err(); + }) + } + + #[cfg(feature = "macros")] + #[cfg_attr(target_arch = "wasm32", ignore)] // DateTime import fails on wasm for mysterious reasons + fn assert_timezone_eq(py: Python<'_>, timezone: &PyTzInfo, delta_s: i32, name: &str) { + let actual_delta = timezone + .call_method1("utcoffset", ((),)) + .unwrap() + .extract::<&PyDelta>() + .unwrap(); + let actual_name = timezone + .call_method1("tzname", ((),)) + .unwrap() + .extract::<&str>() + .unwrap(); + assert!(actual_delta + .eq(PyDelta::new(py, 0, delta_s, 0, true).unwrap()) + .unwrap()); + assert_eq!(actual_name, name); + } } diff --git a/src/types/mod.rs b/src/types/mod.rs index ef2390cc809..9c2a5e283ca 100644 --- a/src/types/mod.rs +++ b/src/types/mod.rs @@ -10,8 +10,8 @@ pub use self::code::PyCode; pub use self::complex::PyComplex; #[cfg(not(Py_LIMITED_API))] pub use self::datetime::{ - timezone_utc, PyDate, PyDateAccess, PyDateTime, PyDelta, PyDeltaAccess, PyTime, PyTimeAccess, - PyTzInfo, PyTzInfoAccess, + timezone_from_offset, timezone_from_offset_and_name, timezone_utc, PyDate, PyDateAccess, + PyDateTime, PyDelta, PyDeltaAccess, PyTime, PyTimeAccess, PyTzInfo, PyTzInfoAccess, }; pub use self::dict::{IntoPyDict, PyDict}; #[cfg(not(PyPy))]