From 0cdf99b5f65a2397ab97205080053b8029fdb62b Mon Sep 17 00:00:00 2001 From: David Hewitt <1939362+davidhewitt@users.noreply.github.com> Date: Mon, 4 Jul 2022 21:21:36 +0100 Subject: [PATCH] datetime: support timezone bindings --- CHANGELOG.md | 4 +- pyo3-ffi/src/datetime.rs | 13 +++- pytests/src/datetime.rs | 21 +----- src/ffi/tests.rs | 34 +++++---- src/types/datetime.rs | 154 ++++++++++++++++++++++++++++++--------- src/types/mod.rs | 4 +- tests/test_datetime.rs | 8 +- 7 files changed, 161 insertions(+), 77 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 7809147df61..fef7ac764e2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -22,7 +22,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Add `CompareOp::matches` to easily implement `__richcmp__` as the result of a Rust `std::cmp::Ordering` comparison. [#2460](https://github.com/PyO3/pyo3/pull/2460) - Supprt `#[pyo3(name)]` on enum variants [#2457](https://github.com/PyO3/pyo3/pull/2457) -- Add `PySuper` object [#2049](https://github.com/PyO3/pyo3/issues/2049) +- Add `PySuper` object [#2049](https://github.com/PyO3/pyo3/issues/2049) +- Add support for constructing `datetime.timezone` objects: `timezone_utc()`, `timezone_from_offset()`, and `timezone_from_offset_and_name()`. [#1588](https://github.com/PyO3/pyo3/pull/1588) ### Changed @@ -36,6 +37,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Prevent multiple `#[pymethods]` with the same name for a single `#[pyclass]`. [#2399](https://github.com/PyO3/pyo3/pull/2399) - Fixup `lib_name` when using `PYO3_CONFIG_FILE`. [#2404](https://github.com/PyO3/pyo3/pull/2404) - Iterators over `PySet` and `PyDict` will now panic if the underlying collection is mutated during the iteration. [#2380](https://github.com/PyO3/pyo3/pull/2380) +- Change datetime constructors taking a `tzinfo` to take `Option<&PyTzInfo>` instead of `Option<&PyObject>`: `PyDateTime::new()`, `PyDateTime::new_with_fold()`, `PyTime::new()`, and `PyTime::new_with_fold()`. [#1588](https://github.com/PyO3/pyo3/pull/1588) ### Fixed diff --git a/pyo3-ffi/src/datetime.rs b/pyo3-ffi/src/datetime.rs index 8f552831f2e..7e5a250990f 100644 --- a/pyo3-ffi/src/datetime.rs +++ b/pyo3-ffi/src/datetime.rs @@ -574,8 +574,17 @@ pub unsafe fn PyTZInfo_CheckExact(op: *mut PyObject) -> c_int { // skipped non-limited PyTime_FromTime // skipped non-limited PyTime_FromTimeAndFold // skipped non-limited PyDelta_FromDSU -// skipped non-limited PyTimeZone_FromOffset -// skipped non-limited PyTimeZone_FromOffsetAndName + +pub unsafe fn PyTimeZone_FromOffset(offset: *mut PyObject) -> *mut PyObject { + ((*PyDateTimeAPI()).TimeZone_FromTimeZone)(offset, std::ptr::null_mut()) +} + +pub unsafe fn PyTimeZone_FromOffsetAndName( + offset: *mut PyObject, + name: *mut PyObject, +) -> *mut PyObject { + ((*PyDateTimeAPI()).TimeZone_FromTimeZone)(offset, name) +} #[cfg(not(PyPy))] pub unsafe fn PyDateTime_FromTimestamp(args: *mut PyObject) -> *mut PyObject { diff --git a/pytests/src/datetime.rs b/pytests/src/datetime.rs index b0a306ec821..ba48705dc78 100644 --- a/pytests/src/datetime.rs +++ b/pytests/src/datetime.rs @@ -33,14 +33,7 @@ fn make_time<'p>( microsecond: u32, tzinfo: Option<&PyTzInfo>, ) -> PyResult<&'p PyTime> { - PyTime::new( - py, - hour, - minute, - second, - microsecond, - tzinfo.map(|o| o.to_object(py)).as_ref(), - ) + PyTime::new(py, hour, minute, second, microsecond, tzinfo) } #[pyfunction] @@ -53,15 +46,7 @@ fn time_with_fold<'p>( tzinfo: Option<&PyTzInfo>, fold: bool, ) -> PyResult<&'p PyTime> { - PyTime::new_with_fold( - py, - hour, - minute, - second, - microsecond, - tzinfo.map(|o| o.to_object(py)).as_ref(), - fold, - ) + PyTime::new_with_fold(py, hour, minute, second, microsecond, tzinfo, fold) } #[pyfunction] @@ -130,7 +115,7 @@ fn make_datetime<'p>( minute, second, microsecond, - tzinfo.map(|o| (o.to_object(py))).as_ref(), + tzinfo, ) } diff --git a/src/ffi/tests.rs b/src/ffi/tests.rs index a1789366133..4d9c56d86e4 100644 --- a/src/ffi/tests.rs +++ b/src/ffi/tests.rs @@ -11,8 +11,10 @@ use libc::wchar_t; fn test_datetime_fromtimestamp() { Python::with_gil(|py| { let args: Py = (100,).into_py(py); - unsafe { PyDateTime_IMPORT() }; - let dt: &PyAny = unsafe { py.from_owned_ptr(PyDateTime_FromTimestamp(args.as_ptr())) }; + let dt: &PyAny = unsafe { + PyDateTime_IMPORT(); + py.from_owned_ptr(PyDateTime_FromTimestamp(args.as_ptr())) + }; let locals = PyDict::new(py); locals.set_item("dt", dt).unwrap(); py.run( @@ -29,8 +31,10 @@ fn test_datetime_fromtimestamp() { fn test_date_fromtimestamp() { Python::with_gil(|py| { let args: Py = (100,).into_py(py); - unsafe { PyDateTime_IMPORT() }; - let dt: &PyAny = unsafe { py.from_owned_ptr(PyDate_FromTimestamp(args.as_ptr())) }; + let dt: &PyAny = unsafe { + PyDateTime_IMPORT(); + py.from_owned_ptr(PyDate_FromTimestamp(args.as_ptr())) + }; let locals = PyDict::new(py); locals.set_item("dt", dt).unwrap(); py.run( @@ -46,12 +50,10 @@ fn test_date_fromtimestamp() { #[test] fn test_utc_timezone() { Python::with_gil(|py| { - let utc_timezone = unsafe { + let utc_timezone: &PyAny = unsafe { PyDateTime_IMPORT(); - PyDateTime_TimeZone_UTC() + py.from_borrowed_ptr(PyDateTime_TimeZone_UTC()) }; - let utc_timezone = - unsafe { &*((&utc_timezone) as *const *mut PyObject as *const Py) }; let locals = PyDict::new(py); locals.set_item("utc_timezone", utc_timezone).unwrap(); py.run( @@ -193,19 +195,19 @@ fn ucs4() { #[cfg_attr(target_arch = "wasm32", ignore)] // DateTime import fails on wasm for mysterious reasons #[cfg(not(PyPy))] fn test_get_tzinfo() { + use crate::types::timezone_utc; + crate::Python::with_gil(|py| { use crate::types::{PyDateTime, PyTime}; - use crate::{AsPyPointer, PyAny, ToPyObject}; + use crate::{AsPyPointer, PyAny}; - let datetime = py.import("datetime").map_err(|e| e.print(py)).unwrap(); - let timezone = datetime.getattr("timezone").unwrap(); - let utc = timezone.getattr("utc").unwrap().to_object(py); + let utc = timezone_utc(py); - let dt = PyDateTime::new(py, 2018, 1, 1, 0, 0, 0, 0, Some(&utc)).unwrap(); + let dt = PyDateTime::new(py, 2018, 1, 1, 0, 0, 0, 0, Some(utc)).unwrap(); assert!( unsafe { py.from_borrowed_ptr::(PyDateTime_DATE_GET_TZINFO(dt.as_ptr())) } - .is(&utc) + .is(utc) ); let dt = PyDateTime::new(py, 2018, 1, 1, 0, 0, 0, 0, None).unwrap(); @@ -215,11 +217,11 @@ fn test_get_tzinfo() { .is_none() ); - let t = PyTime::new(py, 0, 0, 0, 0, Some(&utc)).unwrap(); + let t = PyTime::new(py, 0, 0, 0, 0, Some(utc)).unwrap(); assert!( unsafe { py.from_borrowed_ptr::(PyDateTime_TIME_GET_TZINFO(t.as_ptr())) } - .is(&utc) + .is(utc) ); let t = PyTime::new(py, 0, 0, 0, 0, None).unwrap(); diff --git a/src/types/datetime.rs b/src/types/datetime.rs index 810f19a4821..1fcadcd5351 100644 --- a/src/types/datetime.rs +++ b/src/types/datetime.rs @@ -20,8 +20,8 @@ use crate::ffi::{ PyDateTime_TIME_GET_MINUTE, PyDateTime_TIME_GET_SECOND, }; use crate::instance::PyNativeType; -use crate::types::PyTuple; -use crate::{AsPyPointer, PyAny, PyObject, Python, ToPyObject}; +use crate::types::{PyString, PyTuple}; +use crate::{AsPyPointer, IntoPy, Py, PyAny, Python}; use std::os::raw::c_int; fn ensure_datetime_api(_py: Python<'_>) -> &'static PyDateTime_CAPI { @@ -244,7 +244,7 @@ impl PyDateTime { minute: u8, second: u8, microsecond: u32, - tzinfo: Option<&PyObject>, + tzinfo: Option<&PyTzInfo>, ) -> PyResult<&'p PyDateTime> { let api = ensure_datetime_api(py); unsafe { @@ -280,7 +280,7 @@ impl PyDateTime { minute: u8, second: u8, microsecond: u32, - tzinfo: Option<&PyObject>, + tzinfo: Option<&PyTzInfo>, fold: bool, ) -> PyResult<&'p PyDateTime> { let api = ensure_datetime_api(py); @@ -303,20 +303,13 @@ impl PyDateTime { /// Construct a `datetime` object from a POSIX timestamp /// - /// This is equivalent to `datetime.datetime.from_timestamp` + /// This is equivalent to `datetime.datetime.fromtimestamp` pub fn from_timestamp<'p>( py: Python<'p>, timestamp: f64, - time_zone_info: Option<&PyTzInfo>, + tzinfo: Option<&PyTzInfo>, ) -> PyResult<&'p PyDateTime> { - let timestamp: PyObject = timestamp.to_object(py); - - let time_zone_info: PyObject = match time_zone_info { - Some(time_zone_info) => time_zone_info.to_object(py), - None => py.None(), - }; - - let args = PyTuple::new(py, &[timestamp, time_zone_info]); + let args: Py = (timestamp, tzinfo).into_py(py); // safety ensure API is loaded let _api = ensure_datetime_api(py); @@ -396,7 +389,7 @@ impl PyTime { minute: u8, second: u8, microsecond: u32, - tzinfo: Option<&PyObject>, + tzinfo: Option<&PyTzInfo>, ) -> PyResult<&'p PyTime> { let api = ensure_datetime_api(py); unsafe { @@ -419,7 +412,7 @@ impl PyTime { minute: u8, second: u8, microsecond: u32, - tzinfo: Option<&PyObject>, + tzinfo: Option<&PyTzInfo>, fold: bool, ) -> PyResult<&'p PyTime> { let api = ensure_datetime_api(py); @@ -473,9 +466,11 @@ impl PyTzInfoAccess for PyTime { } } -/// Bindings for `datetime.tzinfo` +/// Bindings for `datetime.tzinfo`. /// -/// This is an abstract base class and should not be constructed directly. +/// While `tzinfo` is an abstract base class, the `datetime` module provides one concrete +/// implementation: `datetime.timezone`. See [`timezone_utc`], +/// [`timezone_from_offset`], and [`timezone_from_offset_and_name`]. #[repr(transparent)] pub struct PyTzInfo(PyAny); pyobject_native_type!( @@ -486,6 +481,33 @@ pyobject_native_type!( #checkfunction=PyTZInfo_Check ); +/// Equivalent to `datetime.timezone.utc` +pub fn timezone_utc(py: Python<'_>) -> &PyTzInfo { + unsafe { &*(ensure_datetime_api(py).TimeZone_UTC as *const PyTzInfo) } +} + +/// Equivalent to `datetime.timezone(offset)` +pub fn timezone_from_offset<'py>(py: Python<'py>, offset: &PyDelta) -> PyResult<&'py PyTzInfo> { + ensure_datetime_api(py); + unsafe { py.from_owned_ptr_or_err(ffi::PyTimeZone_FromOffset(offset.as_ptr())) } +} + +/// Equivalent to `datetime.timezone(offset, name)` +pub fn timezone_from_offset_and_name<'py, N: IntoPy>>( + py: Python<'py>, + offset: &PyDelta, + name: N, +) -> PyResult<&'py PyTzInfo> { + let name = name.into_py(py); + ensure_datetime_api(py); + unsafe { + py.from_owned_ptr_or_err(ffi::PyTimeZone_FromOffsetAndName( + offset.as_ptr(), + name.as_ptr(), + )) + } +} + /// Bindings for `datetime.timedelta` #[repr(transparent)] pub struct PyDelta(PyAny); @@ -535,7 +557,7 @@ impl PyDeltaAccess for PyDelta { } // Utility function -fn opt_to_pyobj(py: Python<'_>, opt: Option<&PyObject>) -> *mut ffi::PyObject { +fn opt_to_pyobj(py: Python<'_>, opt: Option<&PyTzInfo>) -> *mut ffi::PyObject { // Convenience function for unpacking Options to either an Object or None match opt { Some(tzi) => tzi.as_ptr(), @@ -545,12 +567,84 @@ fn opt_to_pyobj(py: Python<'_>, opt: Option<&PyObject>) -> *mut ffi::PyObject { #[cfg(test)] mod tests { + use super::*; + #[cfg(feature = "macros")] + use crate::py_run; + #[test] + #[cfg(feature = "macros")] #[cfg_attr(target_arch = "wasm32", ignore)] // DateTime import fails on wasm for mysterious reasons - fn test_new_with_fold() { - crate::Python::with_gil(|py| { - use crate::types::{PyDateTime, PyTimeAccess}; + fn test_datetime_fromtimestamp() { + Python::with_gil(|py| { + let dt = PyDateTime::from_timestamp(py, 100.0, None).unwrap(); + py_run!( + py, + dt, + "import datetime; assert dt == datetime.datetime.fromtimestamp(100)" + ); + { + let dt = PyDateTime::from_timestamp(py, 100.0, Some(timezone_utc(py))).unwrap(); + py_run!( + py, + dt, + "import datetime; assert dt == datetime.datetime.fromtimestamp(100, datetime.timezone.utc)" + ); + } + }) + } + + #[test] + #[cfg(feature = "macros")] + #[cfg_attr(target_arch = "wasm32", ignore)] // DateTime import fails on wasm for mysterious reasons + fn test_date_fromtimestamp() { + Python::with_gil(|py| { + let dt = PyDate::from_timestamp(py, 100).unwrap(); + py_run!( + py, + dt, + "import datetime; assert dt == datetime.date.fromtimestamp(100)" + ); + }) + } + + #[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| { + let tz = timezone_from_offset(py, PyDelta::new(py, 0, 100, 0, false).unwrap()).unwrap(); + py_run!( + py, + tz, + "import datetime; assert tz == datetime.timezone(datetime.timedelta(seconds=100))" + ); + }) + } + + #[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 tz = timezone_from_offset_and_name( + py, + PyDelta::new(py, 0, 100, 0, false).unwrap(), + "testtz", + ) + .unwrap(); + py_run!( + py, + tz, + "import datetime; assert tz == datetime.timezone(datetime.timedelta(seconds=100), 'testtz')" + ); + }) + } + + #[test] + #[cfg_attr(target_arch = "wasm32", ignore)] // DateTime import fails on wasm for mysterious reasons + fn test_new_with_fold() { + Python::with_gil(|py| { let a = PyDateTime::new_with_fold(py, 2021, 1, 23, 20, 32, 40, 341516, None, false); let b = PyDateTime::new_with_fold(py, 2021, 1, 23, 20, 32, 40, 341516, None, true); @@ -559,29 +653,23 @@ mod tests { }); } - #[cfg(not(PyPy))] #[test] #[cfg_attr(target_arch = "wasm32", ignore)] // DateTime import fails on wasm for mysterious reasons fn test_get_tzinfo() { crate::Python::with_gil(|py| { - use crate::conversion::ToPyObject; - use crate::types::{PyDateTime, PyTime, PyTzInfoAccess}; - - let datetime = py.import("datetime").map_err(|e| e.print(py)).unwrap(); - let timezone = datetime.getattr("timezone").unwrap(); - let utc = timezone.getattr("utc").unwrap().to_object(py); + let utc = timezone_utc(py); - let dt = PyDateTime::new(py, 2018, 1, 1, 0, 0, 0, 0, Some(&utc)).unwrap(); + let dt = PyDateTime::new(py, 2018, 1, 1, 0, 0, 0, 0, Some(utc)).unwrap(); - assert!(dt.get_tzinfo().unwrap().eq(&utc).unwrap()); + assert!(dt.get_tzinfo().unwrap().eq(utc).unwrap()); let dt = PyDateTime::new(py, 2018, 1, 1, 0, 0, 0, 0, None).unwrap(); assert!(dt.get_tzinfo().is_none()); - let t = PyTime::new(py, 0, 0, 0, 0, Some(&utc)).unwrap(); + let t = PyTime::new(py, 0, 0, 0, 0, Some(utc)).unwrap(); - assert!(t.get_tzinfo().unwrap().eq(&utc).unwrap()); + assert!(t.get_tzinfo().unwrap().eq(utc).unwrap()); let t = PyTime::new(py, 0, 0, 0, 0, None).unwrap(); diff --git a/src/types/mod.rs b/src/types/mod.rs index 36530ef2642..9ae01987f28 100644 --- a/src/types/mod.rs +++ b/src/types/mod.rs @@ -12,8 +12,8 @@ pub use self::code::PyCode; pub use self::complex::PyComplex; #[cfg(not(Py_LIMITED_API))] pub use self::datetime::{ - 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}; pub use self::floatob::PyFloat; diff --git a/tests/test_datetime.rs b/tests/test_datetime.rs index 458e15d3535..03cd29d5179 100644 --- a/tests/test_datetime.rs +++ b/tests/test_datetime.rs @@ -1,7 +1,7 @@ #![cfg(not(Py_LIMITED_API))] use pyo3::prelude::*; -use pyo3::types::IntoPyDict; +use pyo3::types::{timezone_utc, IntoPyDict}; use pyo3_ffi::PyDateTime_IMPORT; fn _get_subclasses<'p>( @@ -110,11 +110,9 @@ fn test_datetime_utc() { let gil = Python::acquire_gil(); let py = gil.python(); - let datetime = py.import("datetime").map_err(|e| e.print(py)).unwrap(); - let timezone = datetime.getattr("timezone").unwrap(); - let utc = timezone.getattr("utc").unwrap().to_object(py); + let utc = timezone_utc(py); - let dt = PyDateTime::new(py, 2018, 1, 1, 0, 0, 0, 0, Some(&utc)).unwrap(); + let dt = PyDateTime::new(py, 2018, 1, 1, 0, 0, 0, 0, Some(utc)).unwrap(); let locals = [("dt", dt)].into_py_dict(py);