From 72f5cb205eb300be07bcf18ad06ceee1d2f06651 Mon Sep 17 00:00:00 2001 From: Ivan Tham Date: Mon, 8 Mar 2021 00:19:07 +0800 Subject: [PATCH] Add pyo3 integration Some parts taken from https://github.com/kangalioo/pyo3-chrono --- CHANGELOG.md | 1 + Cargo.toml | 2 + Makefile | 2 +- README.md | 2 + ci/github.sh | 2 +- src/datetime/mod.rs | 2 +- src/lib.rs | 5 ++ src/naive/date.rs | 5 +- src/naive/internals.rs | 6 +- src/naive/time/mod.rs | 2 +- src/oldtime.rs | 91 ++++++++++++++++++++++++++++++ src/pyo3/date.rs | 61 ++++++++++++++++++++ src/pyo3/datetime.rs | 108 +++++++++++++++++++++++++++++++++++ src/pyo3/mod.rs | 4 ++ src/pyo3/offset.rs | 125 +++++++++++++++++++++++++++++++++++++++++ src/pyo3/time.rs | 64 +++++++++++++++++++++ 16 files changed, 472 insertions(+), 10 deletions(-) create mode 100644 src/pyo3/date.rs create mode 100644 src/pyo3/datetime.rs create mode 100644 src/pyo3/mod.rs create mode 100644 src/pyo3/offset.rs create mode 100644 src/pyo3/time.rs diff --git a/CHANGELOG.md b/CHANGELOG.md index e596cf406e..ab16b1289c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -22,6 +22,7 @@ Versions with only mechanical changes will be omitted from the following list. * Change `Local::now()` and `Utc::now()` documentation from "current date" to "current date and time" (#647) * Fix `duration_round` panic on rounding by `Duration::zero()` (#658) * Add optional rkyv support. +* Add `pyo3` integration (@pickfire @kangalioo #542) ## 0.4.19 diff --git a/Cargo.toml b/Cargo.toml index 0a7c37cd86..97886a9698 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -40,6 +40,7 @@ serde = { version = "1.0.99", default-features = false, optional = true } pure-rust-locales = { version = "0.5.2", optional = true } criterion = { version = "0.3", optional = true } rkyv = {version = "0.7", optional = true} +pyo3 = { version = "0.16.2", optional = true } [target.'cfg(all(target_arch = "wasm32", not(any(target_os = "emscripten", target_os = "wasi"))))'.dependencies] wasm-bindgen = { version = "0.2", optional = true } @@ -54,6 +55,7 @@ serde_derive = { version = "1", default-features = false } bincode = { version = "0.8.0" } num-iter = { version = "0.1.35", default-features = false } doc-comment = { version = "0.3" } +pyo3 = { version = "0.16.2", features = ["auto-initialize"] } [target.'cfg(all(target_arch = "wasm32", not(any(target_os = "emscripten", target_os = "wasi"))))'.dev-dependencies] wasm-bindgen-test = "0.3" diff --git a/Makefile b/Makefile index 46b39dd25f..0630439ab0 100644 --- a/Makefile +++ b/Makefile @@ -26,4 +26,4 @@ test: .PHONY: doc doc: authors readme - cargo doc --features 'serde rustc-serialize bincode' + cargo doc --features 'serde rustc-serialize bincode pyo3' diff --git a/README.md b/README.md index 48af06cfdb..e28aa75021 100644 --- a/README.md +++ b/README.md @@ -62,11 +62,13 @@ Optional features: - `wasmbind`: Enable integration with [wasm-bindgen][] and its `js-sys` project - [`serde`][]: Enable serialization/deserialization via serde. +- [`pyo3`][]: Enable integration with pyo3. - `unstable-locales`: Enable localization. This adds various methods with a `_localized` suffix. The implementation and API may change or even be removed in a patch release. Feedback welcome. [`serde`]: https://github.com/serde-rs/serde +[`pyo3`]: https://github.com/PyO3/pyo3 [wasm-bindgen]: https://github.com/rustwasm/wasm-bindgen See the [cargo docs][] for examples of specifying features. diff --git a/ci/github.sh b/ci/github.sh index 207cb08a04..b10f537b5e 100755 --- a/ci/github.sh +++ b/ci/github.sh @@ -6,7 +6,7 @@ set -euo pipefail source "${BASH_SOURCE[0]%/*}/_shlib.sh" TEST_TZS=(ACST-9:30 EST4 UTC0 Asia/Katmandu) -FEATURES=(std serde clock "alloc serde" unstable-locales) +FEATURES=(std serde clock "alloc serde" unstable-locales pyo3) CHECK_FEATURES=(alloc "std unstable-locales" "serde clock" "clock unstable-locales") RUST_132_FEATURES=(rustc-serialize serde) diff --git a/src/datetime/mod.rs b/src/datetime/mod.rs index 4dd2102377..e56e0b4295 100644 --- a/src/datetime/mod.rs +++ b/src/datetime/mod.rs @@ -80,7 +80,7 @@ pub enum SecondsFormat { /// [`TimeZone`](./offset/trait.TimeZone.html) implementations. #[derive(Clone)] pub struct DateTime { - datetime: NaiveDateTime, + pub(crate) datetime: NaiveDateTime, offset: Tz::Offset, } diff --git a/src/lib.rs b/src/lib.rs index 80065c3051..0dfd32d107 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -49,11 +49,13 @@ //! //! - `wasmbind`: Enable integration with [wasm-bindgen][] and its `js-sys` project //! - [`serde`][]: Enable serialization/deserialization via serde. +//! - [`pyo3`][]: Enable integration with pyo3. //! - `unstable-locales`: Enable localization. This adds various methods with a //! `_localized` suffix. The implementation and API may change or even be //! removed in a patch release. Feedback welcome. //! //! [`serde`]: https://github.com/serde-rs/serde +//! [`pyo3`]: https://github.com/PyO3/pyo3 //! [wasm-bindgen]: https://github.com/rustwasm/wasm-bindgen //! //! See the [cargo docs][] for examples of specifying features. @@ -534,3 +536,6 @@ pub use naive::__BenchYearFlags; pub mod serde { pub use super::datetime::serde::*; } + +#[cfg(feature = "pyo3")] +mod pyo3; diff --git a/src/naive/date.rs b/src/naive/date.rs index 50ba1129a8..e64af63a94 100644 --- a/src/naive/date.rs +++ b/src/naive/date.rs @@ -173,7 +173,7 @@ impl NaiveDate { /// assert_eq!(d.weekday(), Weekday::Sat); /// assert_eq!(d.num_days_from_ce(), 735671); // days since January 1, 1 CE /// ``` - pub fn from_ymd(year: i32, month: u32, day: u32) -> NaiveDate { + pub(crate) fn from_ymd(year: i32, month: u32, day: u32) -> NaiveDate { NaiveDate::from_ymd_opt(year, month, day).expect("invalid or out-of-range date") } @@ -766,7 +766,7 @@ impl NaiveDate { /// Returns the packed month-day-flags. #[inline] - fn mdf(&self) -> Mdf { + pub(crate) fn mdf(&self) -> Mdf { self.of().to_mdf() } @@ -1899,7 +1899,6 @@ mod serde { assert_eq!(d, decoded); } } - #[cfg(test)] mod tests { use super::NaiveDate; diff --git a/src/naive/internals.rs b/src/naive/internals.rs index 43b769df39..21fc328496 100644 --- a/src/naive/internals.rs +++ b/src/naive/internals.rs @@ -371,7 +371,7 @@ impl fmt::Debug for Of { /// (month, day of month and leap flag), /// which is an index to the `MDL_TO_OL` lookup table. #[derive(PartialEq, PartialOrd, Copy, Clone)] -pub(super) struct Mdf(pub(super) u32); +pub(crate) struct Mdf(pub(super) u32); impl Mdf { #[inline] @@ -419,7 +419,7 @@ impl Mdf { } #[inline] - pub(super) fn month(&self) -> u32 { + pub(crate) fn month(&self) -> u32 { let Mdf(mdf) = *self; mdf >> 9 } @@ -432,7 +432,7 @@ impl Mdf { } #[inline] - pub(super) fn day(&self) -> u32 { + pub(crate) fn day(&self) -> u32 { let Mdf(mdf) = *self; (mdf >> 4) & 0b1_1111 } diff --git a/src/naive/time/mod.rs b/src/naive/time/mod.rs index 29b0929a06..db596ffe6a 100644 --- a/src/naive/time/mod.rs +++ b/src/naive/time/mod.rs @@ -807,7 +807,7 @@ impl NaiveTime { } /// Returns a triple of the hour, minute and second numbers. - fn hms(&self) -> (u32, u32, u32) { + pub(crate) fn hms(&self) -> (u32, u32, u32) { let (mins, sec) = div_mod_floor(self.secs, 60); let (hour, min) = div_mod_floor(mins, 60); (hour, min, sec) diff --git a/src/oldtime.rs b/src/oldtime.rs index 388b71a47d..c030ab313c 100644 --- a/src/oldtime.rs +++ b/src/oldtime.rs @@ -457,6 +457,97 @@ fn div_rem_64(this: i64, other: i64) -> (i64, i64) { (this / other, this % other) } +#[cfg(feature = "pyo3")] +mod pyo3 { + use super::{div_mod_floor_64, NANOS_PER_MICRO, SECS_PER_DAY}; + use crate::Duration; + use pyo3::conversion::{FromPyObject, IntoPy, PyTryFrom, ToPyObject}; + use pyo3::types::{PyDelta, PyDeltaAccess}; + use std::convert::TryInto; + + impl ToPyObject for Duration { + fn to_object(&self, py: pyo3::Python) -> pyo3::PyObject { + let micros = self.nanos / NANOS_PER_MICRO; + let (days, secs) = div_mod_floor_64(self.secs, SECS_PER_DAY); + // Python will check overflow so even if we reduce the size + // it will still overflow. + let days = days.try_into().unwrap_or(i32::MAX); + let secs = secs.try_into().unwrap_or(i32::MAX); + + // We do not need to check i64 to i32 cast from rust because + // python will panic with OverflowError. + // Not sure if we need normalize here. + let delta = + PyDelta::new(py, days, secs, micros, false).expect("Failed to construct delta"); + delta.into() + } + } + + impl IntoPy for Duration { + fn into_py(self, py: pyo3::Python) -> pyo3::PyObject { + ToPyObject::to_object(&self, py) + } + } + + impl FromPyObject<'_> for Duration { + fn extract(ob: &pyo3::PyAny) -> pyo3::PyResult { + let delta = ::try_from(ob)?; + // Python size are much lower than rust size so we do not need bound checks. + // 0 <= microseconds < 1000000 + // 0 <= seconds < 3600*24 + // -999999999 <= days <= 999999999 + let secs = delta.get_days() as i64 * SECS_PER_DAY + delta.get_seconds() as i64; + let nanos = delta.get_microseconds() * NANOS_PER_MICRO; + + Ok(Duration { secs, nanos }) + } + } + + #[test] + fn test_pyo3_topyobject() { + use std::cmp::Ordering; + use std::panic; + + let gil = pyo3::Python::acquire_gil(); + let py = gil.python(); + let check = |s, ns, py_d, py_s, py_ms| { + let delta = Duration { secs: s, nanos: ns }.to_object(py); + let delta: &PyDelta = delta.extract(py).unwrap(); + let py_delta = PyDelta::new(py, py_d, py_s, py_ms, true).unwrap(); + assert_eq!(delta.compare(py_delta).unwrap(), Ordering::Equal); + }; + let check_panic = |duration: Duration| { + assert!(panic::catch_unwind(|| { + let gil = pyo3::Python::acquire_gil(); + let py = gil.python(); + duration.to_object(py); + }) + .is_err()) + }; + + check(-86399999913600, 0, -999999999, 0, 0); // min + check(86399999999999, 999999000, 999999999, 86399, 999999); // max + + check_panic(super::MIN); + check_panic(super::MAX); + } + + #[test] + fn test_pyo3_frompyobject() { + let gil = pyo3::Python::acquire_gil(); + let py = gil.python(); + let check = |s, ns, py_d, py_s, py_ms| { + let py_delta = PyDelta::new(py, py_d, py_s, py_ms, false).unwrap(); + let py_delta: Duration = py_delta.extract().unwrap(); + let delta = Duration { secs: s, nanos: ns }; + assert_eq!(py_delta, delta); + }; + + check(-86399999913600, 0, -999999999, 0, 0); // min + check(86399999999999, 999999000, 999999999, 86399, 999999); // max + } +} + #[cfg(test)] mod tests { use super::{Duration, OutOfRangeError, MAX, MIN}; diff --git a/src/pyo3/date.rs b/src/pyo3/date.rs new file mode 100644 index 0000000000..3ac50dcfda --- /dev/null +++ b/src/pyo3/date.rs @@ -0,0 +1,61 @@ +use crate::{Datelike, NaiveDate}; +use pyo3::conversion::{FromPyObject, IntoPy, PyTryFrom, ToPyObject}; +use pyo3::types::{PyDate, PyDateAccess}; + +impl ToPyObject for NaiveDate { + fn to_object(&self, py: pyo3::Python) -> pyo3::PyObject { + let mdf = self.mdf(); + let date = PyDate::new(py, self.year(), mdf.month() as u8, mdf.day() as u8) + .expect("Failed to construct date"); + date.into() + } +} + +impl IntoPy for NaiveDate { + fn into_py(self, py: pyo3::Python) -> pyo3::PyObject { + ToPyObject::to_object(&self, py) + } +} + +impl FromPyObject<'_> for NaiveDate { + fn extract(ob: &pyo3::PyAny) -> pyo3::PyResult { + let date = ::try_from(ob)?; + Ok(NaiveDate::from_ymd(date.get_year(), date.get_month() as u32, date.get_day() as u32)) + } +} + +#[test] +fn test_pyo3_topyobject() { + use std::cmp::Ordering; + + let gil = pyo3::Python::acquire_gil(); + let py = gil.python(); + let eq_ymd = |y, m, d| { + let date = NaiveDate::from_ymd(y, m, d).to_object(py); + let date: &PyDate = date.extract(py).unwrap(); + let py_date = PyDate::new(py, y, m as u8, d as u8).unwrap(); + assert_eq!(date.compare(py_date).unwrap(), Ordering::Equal); + }; + + eq_ymd(2012, 2, 29); + eq_ymd(1, 1, 1); // min + eq_ymd(3000, 6, 5); // future + eq_ymd(9999, 12, 31); // max +} + +#[test] +fn test_pyo3_frompyobject() { + let gil = pyo3::Python::acquire_gil(); + let py = gil.python(); + let eq_ymd = |y, m, d| { + let py_date = PyDate::new(py, y, m as u8, d as u8).unwrap(); + let py_date: NaiveDate = py_date.extract().unwrap(); + let date = NaiveDate::from_ymd(y, m, d); + assert_eq!(py_date, date); + }; + + eq_ymd(2012, 2, 29); + eq_ymd(1, 1, 1); // min + eq_ymd(3000, 6, 5); // future + eq_ymd(9999, 12, 31); // max +} diff --git a/src/pyo3/datetime.rs b/src/pyo3/datetime.rs new file mode 100644 index 0000000000..8811e7751f --- /dev/null +++ b/src/pyo3/datetime.rs @@ -0,0 +1,108 @@ +use crate::{DateTime, Datelike, Offset, TimeZone, Timelike}; +use pyo3::conversion::{IntoPy, ToPyObject}; +use pyo3::types::PyDateTime; + +impl ToPyObject for DateTime { + fn to_object(&self, py: pyo3::Python) -> pyo3::PyObject { + let (date, time) = (self.datetime.date(), self.datetime.time()); + let mdf = date.mdf(); + let (yy, mm, dd) = (date.year(), mdf.month(), mdf.day()); + let (h, m, s) = time.hms(); + let ns = time.nanosecond(); + let (ms, fold) = match ns.checked_sub(1_000_000_000) { + Some(ns) => (ns / 1000, true), + None => (ns / 1000, false), + }; + let tz = self.offset().fix().to_object(py); + let datetime = PyDateTime::new_with_fold( + py, + yy, + mm as u8, + dd as u8, + h as u8, + m as u8, + s as u8, + ms, + Some(&tz), + fold, + ) + .expect("Failed to construct datetime"); + datetime.into() + } +} + +impl IntoPy for DateTime { + fn into_py(self, py: pyo3::Python) -> pyo3::PyObject { + ToPyObject::to_object(&self, py) + } +} + +// impl FromPyObject<'_> for DateTime { +// fn extract(ob: &pyo3::PyAny) -> pyo3::PyResult> { +// let dt = ::try_from(ob)?; +// let ms = dt.get_fold() as u32 * 1_000_000 + dt.get_microsecond(); +// let (h, m, s) = (dt.get_hour(), dt.get_minute(), dt.get_second()); +// todo!("Not sure how to extract timezone, maybe don't implement?"); +// let dt = NaiveDateTime::new( +// NaiveDate::from_ymd(dt.get_year(), dt.get_month() as u32, dt.get_day() as u32), +// NaiveTime::from_hms_micro(h as u32, m as u32, s as u32, ms), +// ); +// } +// } + +#[test] +fn test_pyo3_topyobject() { + use crate::{FixedOffset, NaiveDate, Utc}; + use std::cmp::Ordering; + + let gil = pyo3::Python::acquire_gil(); + let py = gil.python(); + let check = |y, mo, d, h, m, s, ms, py_ms, f| { + let datetime = NaiveDate::from_ymd(y, mo, d).and_hms_micro(h, m, s, ms); + let datetime = DateTime::::from_utc(datetime, Utc).to_object(py); + let datetime: &PyDateTime = datetime.extract(py).unwrap(); + let py_tz = Utc.to_object(py); + let py_datetime = PyDateTime::new_with_fold( + py, + y, + mo as u8, + d as u8, + h as u8, + m as u8, + s as u8, + py_ms, + Some(&py_tz), + f, + ) + .unwrap(); + assert_eq!(datetime.compare(py_datetime).unwrap(), Ordering::Equal); + }; + + check(2014, 5, 6, 7, 8, 9, 1_999_999, 999_999, true); + check(2014, 5, 6, 7, 8, 9, 999_999, 999_999, false); + + let check = |y, mo, d, h, m, s, ms, py_ms, f| { + let offset = FixedOffset::east(3600); + let datetime = NaiveDate::from_ymd(y, mo, d).and_hms_micro(h, m, s, ms); + let datetime = DateTime::::from_utc(datetime, offset).to_object(py); + let datetime: &PyDateTime = datetime.extract(py).unwrap(); + let py_tz = offset.to_object(py); + let py_datetime = PyDateTime::new_with_fold( + py, + y, + mo as u8, + d as u8, + h as u8, + m as u8, + s as u8, + py_ms, + Some(&py_tz), + f, + ) + .unwrap(); + assert_eq!(datetime.compare(py_datetime).unwrap(), Ordering::Equal); + }; + + check(2014, 5, 6, 7, 8, 9, 1_999_999, 999_999, true); + check(2014, 5, 6, 7, 8, 9, 999_999, 999_999, false); +} diff --git a/src/pyo3/mod.rs b/src/pyo3/mod.rs new file mode 100644 index 0000000000..11b28b4136 --- /dev/null +++ b/src/pyo3/mod.rs @@ -0,0 +1,4 @@ +mod date; +mod datetime; +mod offset; +mod time; diff --git a/src/pyo3/offset.rs b/src/pyo3/offset.rs new file mode 100644 index 0000000000..de8f27ea82 --- /dev/null +++ b/src/pyo3/offset.rs @@ -0,0 +1,125 @@ +use crate::offset::{FixedOffset, Utc}; +use pyo3::conversion::{AsPyPointer, FromPyObject, IntoPy, PyTryFrom, ToPyObject}; +use pyo3::exceptions::PyTypeError; +use pyo3::ffi::{PyDateTime_IMPORT, PyDateTime_TimeZone_UTC}; +use pyo3::types::{PyDelta, PyDeltaAccess, PyTzInfo}; +use pyo3::PyObject; + +impl ToPyObject for FixedOffset { + fn to_object(&self, py: pyo3::Python) -> pyo3::PyObject { + let dt_module = py.import("datetime").expect("Failed to import datetime"); + let dt_timezone = dt_module.getattr("timezone").expect("Failed to getattr timezone"); + let seconds_offset = self.local_minus_utc(); + let td = + PyDelta::new(py, 0, seconds_offset, 0, true).expect("Failed to contruct timedelta"); + let offset = dt_timezone.call1((td,)).expect("Failed to call timezone with timedelta"); + offset.into() + } +} + +impl IntoPy for FixedOffset { + fn into_py(self, py: pyo3::Python) -> pyo3::PyObject { + ToPyObject::to_object(&self, py) + } +} + +impl FromPyObject<'_> for FixedOffset { + /// Convert python tzinfo to rust [`FixedOffset`]. + /// + /// Note that the conversion will result in precision lost in microseconds as chrono offset + /// does not supports microseconds. + fn extract(ob: &pyo3::PyAny) -> pyo3::PyResult { + let py_tzinfo = ::try_from(ob)?; + let py_timedelta = py_tzinfo.call_method1("utcoffset", (ob.py().None(),))?; + let py_timedelta = ::try_from(py_timedelta)?; + Ok(FixedOffset::east(py_timedelta.get_seconds())) + } +} + +#[test] +fn test_fixed_pyo3_topyobject() { + let gil = pyo3::Python::acquire_gil(); + let py = gil.python(); + let py_module = py.import("datetime").unwrap(); + let py_timezone = py_module.getattr("timezone").unwrap(); + let offset = FixedOffset::east(3600).to_object(py); + let py_timedelta = PyDelta::new(py, 0, 3600, 0, true).unwrap(); + let py_timedelta = py_timezone.call1((py_timedelta,)).unwrap(); + assert!(offset.as_ref(py).eq(py_timedelta).unwrap()); + let offset = FixedOffset::east(-3600).to_object(py); + let py_timedelta = PyDelta::new(py, 0, -3600, 0, true).unwrap(); + let py_timedelta = py_timezone.call1((py_timedelta,)).unwrap(); + assert!(offset.as_ref(py).eq(py_timedelta).unwrap()); +} + +#[test] +fn test_fixed_pyo3_frompyobject() { + let gil = pyo3::Python::acquire_gil(); + let py = gil.python(); + let py_module = py.import("datetime").unwrap(); + let py_timezone = py_module.getattr("timezone").unwrap(); + let py_timedelta = PyDelta::new(py, 0, 3600, 0, true).unwrap(); + let py_tzinfo = py_timezone.call1((py_timedelta,)).unwrap(); + let offset: FixedOffset = py_tzinfo.extract().unwrap(); + assert_eq!(FixedOffset::east(3600), offset); +} + +impl ToPyObject for Utc { + fn to_object(&self, py: pyo3::Python) -> pyo3::PyObject { + unsafe { + // XXX: not sure if there is a better way to only call this once + PyDateTime_IMPORT(); + PyObject::from_owned_ptr(py, PyDateTime_TimeZone_UTC()) + } + } +} + +impl IntoPy for Utc { + fn into_py(self, py: pyo3::Python) -> pyo3::PyObject { + ToPyObject::to_object(&self, py) + } +} + +impl FromPyObject<'_> for Utc { + fn extract(ob: &pyo3::PyAny) -> pyo3::PyResult { + let py_tzinfo = ::try_from(ob)?; + let py_utc = unsafe { + PyDateTime_IMPORT(); + PyDateTime_TimeZone_UTC() + }; + if py_tzinfo.as_ptr() == py_utc { + Ok(Utc) + } else { + Err(PyTypeError::new_err("Not datetime.timezone.utc")) + } + } +} + +#[test] +fn test_utc_pyo3_topyobject() { + let gil = pyo3::Python::acquire_gil(); + let py = gil.python(); + let utc = Utc.to_object(py); + let py_module = py.import("datetime").unwrap(); + let py_timezone = py_module.getattr("timezone").unwrap(); + let py_utc = py_timezone.getattr("utc").unwrap(); + assert!(utc.as_ref(py).is(py_utc)); +} + +#[test] +fn test_utc_pyo3_frompyobject() { + let gil = pyo3::Python::acquire_gil(); + let py = gil.python(); + let py_module = py.import("datetime").unwrap(); + let py_timezone = py_module.getattr("timezone").unwrap(); + let py_utc = py_timezone.getattr("utc").unwrap(); + let py_utc: Utc = py_utc.extract().unwrap(); + assert_eq!(Utc, py_utc); + let py_timedelta = PyDelta::new(py, 0, 0, 0, false).unwrap(); + let py_timezone_utc = py_timezone.call1((py_timedelta,)).unwrap(); + let py_timezone_utc: Utc = py_timezone_utc.extract().unwrap(); + assert_eq!(Utc, py_timezone_utc); + let py_timedelta = PyDelta::new(py, 0, 3600, 0, false).unwrap(); + let py_timezone = py_timezone.call1((py_timedelta,)).unwrap(); + assert!(py_timezone.extract::().is_err()); +} diff --git a/src/pyo3/time.rs b/src/pyo3/time.rs new file mode 100644 index 0000000000..025fe981e5 --- /dev/null +++ b/src/pyo3/time.rs @@ -0,0 +1,64 @@ +use crate::{NaiveTime, Timelike}; +use pyo3::conversion::{FromPyObject, IntoPy, PyTryFrom, ToPyObject}; +use pyo3::types::{PyTime, PyTimeAccess}; + +impl ToPyObject for NaiveTime { + fn to_object(&self, py: pyo3::Python) -> pyo3::PyObject { + let (h, m, s) = self.hms(); + let ns = self.nanosecond(); + let (ms, fold) = match ns.checked_sub(1_000_000_000) { + Some(ns) => (ns / 1000, true), + None => (ns / 1000, false), + }; + let time = PyTime::new_with_fold(py, h as u8, m as u8, s as u8, ms, None, fold) + .expect("Failed to construct time"); + time.into() + } +} + +impl IntoPy for NaiveTime { + fn into_py(self, py: pyo3::Python) -> pyo3::PyObject { + ToPyObject::to_object(&self, py) + } +} + +impl FromPyObject<'_> for NaiveTime { + fn extract(ob: &pyo3::PyAny) -> pyo3::PyResult { + let time = ::try_from(ob)?; + let ms = time.get_fold() as u32 * 1_000_000 + time.get_microsecond(); + let (h, m, s) = (time.get_hour(), time.get_minute(), time.get_second()); + Ok(NaiveTime::from_hms_micro(h as u32, m as u32, s as u32, ms)) + } +} + +#[test] +fn test_pyo3_topyobject() { + use std::cmp::Ordering; + + let gil = pyo3::Python::acquire_gil(); + let py = gil.python(); + let hmsm = |h, m, s, ms, py_ms, f| { + let time = NaiveTime::from_hms_micro(h, m, s, ms).to_object(py); + let time: &PyTime = time.extract(py).unwrap(); + let py_time = PyTime::new_with_fold(py, h as u8, m as u8, s as u8, py_ms, None, f).unwrap(); + assert_eq!(time.compare(py_time).unwrap(), Ordering::Equal); + }; + + hmsm(3, 5, 7, 1_999_999, 999_999, true); + hmsm(3, 5, 7, 999_999, 999_999, false); +} + +#[test] +fn test_pyo3_frompyobject() { + let gil = pyo3::Python::acquire_gil(); + let py = gil.python(); + let hmsm = |h, m, s, ms, py_ms, f| { + let py_time = PyTime::new_with_fold(py, h as u8, m as u8, s as u8, py_ms, None, f).unwrap(); + let py_time: NaiveTime = py_time.extract().unwrap(); + let time = NaiveTime::from_hms_micro(h, m, s, ms); + assert_eq!(py_time, time); + }; + + hmsm(3, 5, 7, 1_999_999, 999_999, true); + hmsm(3, 5, 7, 999_999, 999_999, false); +}