Skip to content

Commit

Permalink
datetime: support timezone bindings
Browse files Browse the repository at this point in the history
  • Loading branch information
davidhewitt committed Apr 30, 2021
1 parent 1937e21 commit bd6ecde
Show file tree
Hide file tree
Showing 7 changed files with 145 additions and 74 deletions.
2 changes: 2 additions & 0 deletions CHANGELOG.md
Expand Up @@ -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)
Expand All @@ -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)
Expand Down
21 changes: 3 additions & 18 deletions examples/pyo3-pytests/src/datetime.rs
Expand Up @@ -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))]
Expand All @@ -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]
Expand Down Expand Up @@ -131,7 +116,7 @@ fn make_datetime<'p>(
minute,
second,
microsecond,
tzinfo.map(|o| (o.to_object(py))).as_ref(),
tzinfo,
)
}

Expand Down
1 change: 1 addition & 0 deletions guide/src/building_and_distribution/pypy.md
Expand Up @@ -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.
31 changes: 19 additions & 12 deletions src/ffi/datetime.rs
Expand Up @@ -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,
Expand Down Expand Up @@ -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 {
Expand Down Expand Up @@ -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
}
}

Expand Down Expand Up @@ -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,
Expand Down
158 changes: 116 additions & 42 deletions src/types/datetime.rs
Expand Up @@ -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};
Expand All @@ -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

Expand Down Expand Up @@ -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<PyTuple> = (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)
}
}
Expand Down Expand Up @@ -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)(
Expand Down Expand Up @@ -169,7 +160,7 @@ impl PyDateTime {
minute: u8,
second: u8,
microsecond: u32,
tzinfo: Option<&PyObject>,
tzinfo: Option<&PyTzInfo>,
fold: bool,
) -> PyResult<&'p PyDateTime> {
unsafe {
Expand All @@ -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<PyTuple> = (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)
}
}
Expand Down Expand Up @@ -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)(
Expand All @@ -302,7 +275,7 @@ impl PyTime {
minute: u8,
second: u8,
microsecond: u32,
tzinfo: Option<&PyObject>,
tzinfo: Option<&PyTzInfo>,
fold: bool,
) -> PyResult<&'p PyTime> {
unsafe {
Expand Down Expand Up @@ -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!(
Expand All @@ -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<PyTzInfo>)
}
.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);
Expand Down Expand Up @@ -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(),
Expand All @@ -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);

Expand Down
2 changes: 2 additions & 0 deletions src/types/mod.rs
Expand Up @@ -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,
Expand Down

0 comments on commit bd6ecde

Please sign in to comment.