Skip to content

Commit

Permalink
Adds timezone_from_offset and timezone_from_offset_and_name functions
Browse files Browse the repository at this point in the history
Exposes C functions provided by datetime.h

It allows to build conversions from chrono without direct access to the C API
  • Loading branch information
Tpt committed Dec 14, 2023
1 parent 79a54cf commit 8ec500b
Show file tree
Hide file tree
Showing 4 changed files with 115 additions and 23 deletions.
1 change: 1 addition & 0 deletions newsfragments/3648.added.md
@@ -0,0 +1 @@
Add `timezone_from_offset` and `timezone_from_offset_and_name` to construct `datetime.timezone` objects
29 changes: 10 additions & 19 deletions src/conversions/chrono.rs
Expand Up @@ -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 {
Expand Down Expand Up @@ -231,22 +230,14 @@ impl FromPyObject<'_> for DateTime<Utc> {
}
}

// 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()
}
}

Expand Down Expand Up @@ -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());
})
}
Expand All @@ -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);
})
Expand All @@ -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::<Utc>().is_err());
})
}
Expand Down
104 changes: 102 additions & 2 deletions src/types/datetime.rs
Expand Up @@ -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 {
Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -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);
}
}
4 changes: 2 additions & 2 deletions src/types/mod.rs
Expand Up @@ -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))]
Expand Down

0 comments on commit 8ec500b

Please sign in to comment.