diff --git a/CHANGELOG.md b/CHANGELOG.md index 0b062d64748..cf8535f2d3a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -21,6 +21,7 @@ and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0. - Add FFI definitions from `cpython/import.h`.[#1475](https://github.com/PyO3/pyo3/pull/1475) - Add tuple and unit struct support for `#[pyclass]` macro. [#1504](https://github.com/PyO3/pyo3/pull/1504) - Add FFI definition `PyDateTime_TimeZone_UTC`. [#1572](https://github.com/PyO3/pyo3/pull/1572) +- 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 - Change `PyTimeAcces::get_fold()` to return a `bool` instead of a `u8`. [#1397](https://github.com/PyO3/pyo3/pull/1397) @@ -38,6 +39,7 @@ and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0. - `PyMappingProtocol::__reversed__` - `PyNumberProtocol::__complex__` and `PyNumberProtocol::__round__` - `PyAsyncProtocol::__aenter__` and `PyAsyncProtocol::__aexit__` +- 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) ### Removed - Remove deprecated exception names `BaseException` etc. [#1426](https://github.com/PyO3/pyo3/pull/1426) diff --git a/examples/pyo3-pytests/src/datetime.rs b/examples/pyo3-pytests/src/datetime.rs index b7231841c2f..5b5493238da 100644 --- a/examples/pyo3-pytests/src/datetime.rs +++ b/examples/pyo3-pytests/src/datetime.rs @@ -32,14 +32,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) } #[cfg(not(PyPy))] @@ -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] @@ -131,7 +116,7 @@ fn make_datetime<'p>( minute, second, microsecond, - tzinfo.map(|o| (o.to_object(py))).as_ref(), + tzinfo, ) } diff --git a/guide/src/building_and_distribution/pypy.md b/guide/src/building_and_distribution/pypy.md index 111cf43076c..2cff88bcf95 100644 --- a/guide/src/building_and_distribution/pypy.md +++ b/guide/src/building_and_distribution/pypy.md @@ -19,3 +19,4 @@ These are features currently supported by PyO3, but not yet implemented in cpyex - Conversion to rust's i128, u128 types. - `PySequence_Count` (which is used to count number of element in array) - `PyDict_MergeFromSeq2` (used in `PyDict::from_sequence`) +- Support for `datetime.timezone` construction. diff --git a/src/ffi/datetime.rs b/src/ffi/datetime.rs index 5f4915df4b5..6a0f5e8a388 100644 --- a/src/ffi/datetime.rs +++ b/src/ffi/datetime.rs @@ -445,12 +445,11 @@ pub static PyDateTimeAPI: _PyDateTimeAPI_impl = _PyDateTimeAPI_impl { inner: GILOnceCell::new(), }; -/// Safe wrapper around the Python C-API global `PyDateTime_TimeZone_UTC`. This follows a similar +/// Wrapper around the Python C-API global `PyDateTime_TimeZone_UTC`. This follows a similar /// strategy as [`PyDateTimeAPI`]: the Python datetime C-API will automatically be imported if this /// type is deferenced. /// -/// The type obtained by dereferencing this object is `&'static PyObject`. This may change in the -/// future to be a more specific type representing that this is a `datetime.timezone` object. +/// The type obtained by dereferencing this object is `&'static *mut ffi::PyObject`. #[cfg(all(Py_3_7, not(PyPy)))] pub static PyDateTime_TimeZone_UTC: _PyDateTime_TimeZone_UTC_impl = _PyDateTime_TimeZone_UTC_impl { inner: &PyDateTimeAPI, @@ -565,8 +564,19 @@ 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 + +#[cfg(all(Py_3_7, not(PyPy)))] +pub unsafe fn PyTimeZone_FromOffset(offset: *mut PyObject) -> *mut PyObject { + (PyDateTimeAPI.TimeZone_FromTimeZone)(offset, std::ptr::null_mut()) +} + +#[cfg(all(Py_3_7, not(PyPy)))] +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 { @@ -616,14 +626,11 @@ pub struct _PyDateTime_TimeZone_UTC_impl { #[cfg(all(Py_3_7, not(PyPy)))] impl Deref for _PyDateTime_TimeZone_UTC_impl { - type Target = crate::PyObject; + type Target = *mut PyObject; #[inline] - fn deref(&self) -> &crate::PyObject { - unsafe { - &*((&self.inner.TimeZone_UTC) as *const *mut crate::ffi::PyObject - as *const crate::PyObject) - } + fn deref(&self) -> &'static *mut PyObject { + &self.inner.TimeZone_UTC } } @@ -665,7 +672,7 @@ mod tests { #[cfg(all(Py_3_7, not(PyPy)))] fn test_utc_timezone() { Python::with_gil(|py| { - let utc_timezone = PyDateTime_TimeZone_UTC.as_ref(py); + let utc_timezone: &PyAny = unsafe { py.from_borrowed_ptr(*PyDateTime_TimeZone_UTC) }; py_run!( py, utc_timezone, diff --git a/src/types/datetime.rs b/src/types/datetime.rs index 77bb22f1b94..de4d8b0f484 100644 --- a/src/types/datetime.rs +++ b/src/types/datetime.rs @@ -5,7 +5,6 @@ use crate::err::PyResult; use crate::ffi; -#[cfg(PyPy)] use crate::ffi::datetime::{PyDateTime_FromTimestamp, PyDate_FromTimestamp}; use crate::ffi::PyDateTimeAPI; use crate::ffi::{PyDateTime_Check, PyDate_Check, PyDelta_Check, PyTZInfo_Check, PyTime_Check}; @@ -24,10 +23,8 @@ use crate::ffi::{ PyDateTime_TIME_GET_SECOND, }; use crate::types::PyTuple; -use crate::{AsPyPointer, PyAny, PyObject, Python, ToPyObject}; +use crate::{AsPyPointer, IntoPy, Py, PyAny, Python}; use std::os::raw::c_int; -#[cfg(not(PyPy))] -use std::ptr; /// Access traits @@ -87,16 +84,10 @@ impl PyDate { /// /// This is equivalent to `datetime.date.fromtimestamp` pub fn from_timestamp(py: Python, timestamp: i64) -> PyResult<&PyDate> { - let time_tuple = PyTuple::new(py, &[timestamp]); + let time_tuple: Py = (timestamp,).into_py(py); unsafe { - #[cfg(PyPy)] let ptr = PyDate_FromTimestamp(time_tuple.as_ptr()); - - #[cfg(not(PyPy))] - let ptr = - (PyDateTimeAPI.Date_FromTimestamp)(PyDateTimeAPI.DateType, time_tuple.as_ptr()); - py.from_owned_ptr_or_err(ptr) } } @@ -138,7 +129,7 @@ impl PyDateTime { minute: u8, second: u8, microsecond: u32, - tzinfo: Option<&PyObject>, + tzinfo: Option<&PyTzInfo>, ) -> PyResult<&'p PyDateTime> { unsafe { let ptr = (PyDateTimeAPI.DateTime_FromDateAndTime)( @@ -169,7 +160,7 @@ impl PyDateTime { minute: u8, second: u8, microsecond: u32, - tzinfo: Option<&PyObject>, + tzinfo: Option<&PyTzInfo>, fold: bool, ) -> PyResult<&'p PyDateTime> { unsafe { @@ -191,34 +182,16 @@ 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); unsafe { - #[cfg(PyPy)] let ptr = PyDateTime_FromTimestamp(args.as_ptr()); - - #[cfg(not(PyPy))] - let ptr = { - (PyDateTimeAPI.DateTime_FromTimestamp)( - PyDateTimeAPI.DateTimeType, - args.as_ptr(), - ptr::null_mut(), - ) - }; - py.from_owned_ptr_or_err(ptr) } } @@ -279,7 +252,7 @@ impl PyTime { minute: u8, second: u8, microsecond: u32, - tzinfo: Option<&PyObject>, + tzinfo: Option<&PyTzInfo>, ) -> PyResult<&'p PyTime> { unsafe { let ptr = (PyDateTimeAPI.Time_FromTime)( @@ -302,7 +275,7 @@ impl PyTime { minute: u8, second: u8, microsecond: u32, - tzinfo: Option<&PyObject>, + tzinfo: Option<&PyTzInfo>, fold: bool, ) -> PyResult<&'p PyTime> { unsafe { @@ -343,9 +316,12 @@ impl PyTimeAccess 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`](fn.timezone_utc.html), +/// [`timezone_from_offset`](fn.timezone_from_offset.html), and +/// [`timezone_from_offset_and_name`](fn.timezone_from_offset_and_name.html). #[repr(transparent)] pub struct PyTzInfo(PyAny); pyobject_native_type!( @@ -356,6 +332,38 @@ pyobject_native_type!( PyTZInfo_Check ); +/// Equivalent to `datetime.timezone.utc` +#[cfg(all(Py_3_7, not(PyPy)))] +pub fn timezone_utc(py: Python) -> &PyTzInfo { + unsafe { + &*(&*ffi::PyDateTime_TimeZone_UTC as *const *mut ffi::PyObject + as *const crate::Py) + } + .as_ref(py) +} + +/// Equivalent to `datetime.timezone(offset)` +#[cfg(all(Py_3_7, not(PyPy)))] +pub fn timezone_from_offset<'py>(py: Python<'py>, offset: &PyDelta) -> PyResult<&'py PyTzInfo> { + unsafe { py.from_owned_ptr_or_err(ffi::PyTimeZone_FromOffset(offset.as_ptr())) } +} + +/// Equivalent to `datetime.timezone(offset, name)` +#[cfg(all(Py_3_7, not(PyPy)))] +pub fn timezone_from_offset_and_name<'py>( + py: Python<'py>, + offset: &PyDelta, + name: &str, +) -> PyResult<&'py PyTzInfo> { + let name = name.into_py(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); @@ -403,7 +411,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(), @@ -413,12 +421,78 @@ fn opt_to_pyobj(py: Python, opt: Option<&PyObject>) -> *mut ffi::PyObject { #[cfg(test)] mod tests { + use super::*; + use crate::py_run; + + #[test] + 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)" + ); + + #[cfg(all(Py_3_7, not(PyPy)))] + { + 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] + 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(all(Py_3_7, not(PyPy)))] + 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(all(Py_3_7, not(PyPy)))] + 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')" + ); + }) + } + #[cfg(not(PyPy))] #[test] fn test_new_with_fold() { - pyo3::Python::with_gil(|py| { - use pyo3::types::{PyDateTime, PyTimeAccess}; - + 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); diff --git a/src/types/mod.rs b/src/types/mod.rs index 96459f37dc2..07c9c71b25b 100644 --- a/src/types/mod.rs +++ b/src/types/mod.rs @@ -7,6 +7,8 @@ pub use self::boolobject::PyBool; pub use self::bytearray::PyByteArray; pub use self::bytes::PyBytes; pub use self::complex::PyComplex; +#[cfg(all(Py_3_7, not(Py_LIMITED_API), not(PyPy)))] +pub use self::datetime::{timezone_from_offset, timezone_from_offset_and_name, timezone_utc}; #[cfg(not(Py_LIMITED_API))] pub use self::datetime::{ PyDate, PyDateAccess, PyDateTime, PyDelta, PyDeltaAccess, PyTime, PyTimeAccess, PyTzInfo, diff --git a/tests/test_datetime.rs b/tests/test_datetime.rs index 0fbc4df04db..80febe3d46a 100644 --- a/tests/test_datetime.rs +++ b/tests/test_datetime.rs @@ -108,9 +108,9 @@ fn test_datetime_utc() { 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.getattr("utc").unwrap().extract().unwrap(); - 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);