Skip to content

Commit

Permalink
Add pyo3 integration
Browse files Browse the repository at this point in the history
  • Loading branch information
pickfire committed Mar 27, 2022
1 parent c2e9f61 commit 72f5cb2
Show file tree
Hide file tree
Showing 16 changed files with 472 additions and 10 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Expand Up @@ -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

Expand Down
2 changes: 2 additions & 0 deletions Cargo.toml
Expand Up @@ -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 }
Expand All @@ -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"
Expand Down
2 changes: 1 addition & 1 deletion Makefile
Expand Up @@ -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'
2 changes: 2 additions & 0 deletions README.md
Expand Up @@ -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.
Expand Down
2 changes: 1 addition & 1 deletion ci/github.sh
Expand Up @@ -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)

Expand Down
2 changes: 1 addition & 1 deletion src/datetime/mod.rs
Expand Up @@ -80,7 +80,7 @@ pub enum SecondsFormat {
/// [`TimeZone`](./offset/trait.TimeZone.html) implementations.
#[derive(Clone)]
pub struct DateTime<Tz: TimeZone> {
datetime: NaiveDateTime,
pub(crate) datetime: NaiveDateTime,
offset: Tz::Offset,
}

Expand Down
5 changes: 5 additions & 0 deletions src/lib.rs
Expand Up @@ -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.
Expand Down Expand Up @@ -534,3 +536,6 @@ pub use naive::__BenchYearFlags;
pub mod serde {
pub use super::datetime::serde::*;
}

#[cfg(feature = "pyo3")]
mod pyo3;
5 changes: 2 additions & 3 deletions src/naive/date.rs
Expand Up @@ -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")
}

Expand Down Expand Up @@ -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()
}

Expand Down Expand Up @@ -1899,7 +1899,6 @@ mod serde {
assert_eq!(d, decoded);
}
}

#[cfg(test)]
mod tests {
use super::NaiveDate;
Expand Down
6 changes: 3 additions & 3 deletions src/naive/internals.rs
Expand Up @@ -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]
Expand Down Expand Up @@ -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
}
Expand All @@ -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
}
Expand Down
2 changes: 1 addition & 1 deletion src/naive/time/mod.rs
Expand Up @@ -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)
Expand Down
91 changes: 91 additions & 0 deletions src/oldtime.rs
Expand Up @@ -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<pyo3::PyObject> 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<Duration> {
let delta = <PyDelta as PyTryFrom>::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};
Expand Down
61 changes: 61 additions & 0 deletions 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<pyo3::PyObject> 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<NaiveDate> {
let date = <PyDate as PyTryFrom>::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
}

0 comments on commit 72f5cb2

Please sign in to comment.