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 Jul 13, 2022
1 parent 1cd1dbf commit 21af848
Show file tree
Hide file tree
Showing 7 changed files with 148 additions and 76 deletions.
4 changes: 3 additions & 1 deletion CHANGELOG.md
Expand Up @@ -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 `timezone_utc()`. [#1588](https://github.com/PyO3/pyo3/pull/1588)

### Changed

Expand All @@ -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

Expand Down
13 changes: 11 additions & 2 deletions pyo3-ffi/src/datetime.rs
Expand Up @@ -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 {
Expand Down
21 changes: 3 additions & 18 deletions pytests/src/datetime.rs
Expand Up @@ -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]
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 @@ -130,7 +115,7 @@ fn make_datetime<'p>(
minute,
second,
microsecond,
tzinfo.map(|o| (o.to_object(py))).as_ref(),
tzinfo,
)
}

Expand Down
77 changes: 61 additions & 16 deletions src/ffi/tests.rs
Expand Up @@ -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(
Expand All @@ -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(
Expand All @@ -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(
Expand All @@ -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() {
Expand Down Expand Up @@ -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();
Expand All @@ -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();
Expand Down

0 comments on commit 21af848

Please sign in to comment.