datetime: support timezone bindings

This commit is contained in:
David Hewitt 2022-07-04 21:21:36 +01:00
parent 308ffa25b0
commit 7babd13830
7 changed files with 148 additions and 76 deletions

View File

@ -14,6 +14,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
### Added
- Add `timezone_utc()`. [#1588](https://github.com/PyO3/pyo3/pull/1588)
- Implement `ToPyObject` for `[T; N]`. [#2313](https://github.com/PyO3/pyo3/pull/2313)
- Added the internal `IntoPyResult` trait to give better error messages when function return types do not implement `IntoPy`. [#2326](https://github.com/PyO3/pyo3/pull/2326)
- Add `PyDictKeys`, `PyDictValues` and `PyDictItems` Rust types to represent `dict_keys`, `dict_values` and `dict_items` types. [#2358](https://github.com/PyO3/pyo3/pull/2358)
@ -30,6 +31,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
### Changed
- 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)
- Several methods of `Py` and `PyAny` now accept `impl IntoPy<Py<PyString>>` rather than just `&str` to allow use of the `intern!` macro. [#2312](https://github.com/PyO3/pyo3/pull/2312)
- Move `PyTypeObject::type_object` method to `PyTypeInfo` trait, and deprecate `PyTypeObject` trait. [#2287](https://github.com/PyO3/pyo3/pull/2287)
- The deprecated `pyproto` feature is now disabled by default. [#2322](https://github.com/PyO3/pyo3/pull/2322)

View File

@ -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 {

View File

@ -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,
)
}

View File

@ -11,8 +11,10 @@ use libc::wchar_t;
fn test_datetime_fromtimestamp() {
Python::with_gil(|py| {
let args: Py<PyAny> = (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<PyAny> = (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<PyAny>) };
let locals = PyDict::new(py);
locals.set_item("utc_timezone", utc_timezone).unwrap();
py.run(
@ -63,6 +65,49 @@ fn test_utc_timezone() {
})
}
#[test]
#[cfg(feature = "macros")]
#[cfg_attr(target_arch = "wasm32", ignore)] // DateTime import fails on wasm for mysterious reasons
fn test_timezone_from_offset() {
use crate::types::PyDelta;
Python::with_gil(|py| {
let tz: &PyAny = unsafe {
PyDateTime_IMPORT();
py.from_borrowed_ptr(PyTimeZone_FromOffset(
PyDelta::new(py, 0, 100, 0, false).unwrap().as_ptr(),
))
};
crate::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() {
use crate::types::PyDelta;
Python::with_gil(|py| {
let tz: &PyAny = unsafe {
PyDateTime_IMPORT();
py.from_borrowed_ptr(PyTimeZone_FromOffsetAndName(
PyDelta::new(py, 0, 100, 0, false).unwrap().as_ptr(),
PyString::new(py, "testtz").as_ptr(),
))
};
crate::py_run!(
py,
tz,
"import datetime; assert tz == datetime.timezone(datetime.timedelta(seconds=100), 'testtz')"
);
})
}
#[cfg(target_endian = "little")]
#[test]
fn ascii_object_bitfield() {
@ -193,19 +238,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::<PyAny>(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 +260,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::<PyAny>(PyDateTime_TIME_GET_TZINFO(t.as_ptr())) }
.is(&utc)
.is(utc)
);
let t = PyTime::new(py, 0, 0, 0, 0, None).unwrap();

View File

@ -21,7 +21,7 @@ use crate::ffi::{
};
use crate::instance::PyNativeType;
use crate::types::PyTuple;
use crate::{AsPyPointer, PyAny, PyObject, Python, ToPyObject};
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<PyTuple> = (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.
/// This is an abstract base class and cannot be constructed directly.
/// For concrete time zone implementations, see [`timezone_utc`] and
/// the [`zoneinfo` module](https://docs.python.org/3/library/zoneinfo.html).
#[repr(transparent)]
pub struct PyTzInfo(PyAny);
pyobject_native_type!(
@ -486,6 +481,11 @@ 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) }
}
/// Bindings for `datetime.timedelta`
#[repr(transparent)]
pub struct PyDelta(PyAny);
@ -535,7 +535,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 +545,51 @@ 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_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_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};
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 +598,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 utc = timezone_utc(py);
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 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();

View File

@ -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_utc, PyDate, PyDateAccess, PyDateTime, PyDelta, PyDeltaAccess, PyTime, PyTimeAccess,
PyTzInfo, PyTzInfoAccess,
};
pub use self::dict::{IntoPyDict, PyDict};
pub use self::floatob::PyFloat;

View File

@ -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);