Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add pyo3 integration #542

Closed
wants to merge 1 commit into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Expand Up @@ -33,6 +33,7 @@ Versions with only mechanical changes will be omitted from the following list.
* Add support for getting week bounds based on a specific `NaiveDate` and a `Weekday` (#666)
* Remove libc dependency from Cargo.toml.
* Add the `and_local_timezone` method to `NaiveDateTime`
* 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.4", 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 = "1.3.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 @@ -84,7 +84,7 @@ pub enum SecondsFormat {
#[derive(Clone)]
#[cfg_attr(feature = "rkyv", derive(Archive, Deserialize, Serialize))]
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 @@ -527,6 +529,9 @@ pub mod serde {
pub use super::datetime::serde::*;
}

#[cfg(feature = "pyo3")]
mod pyo3;

/// MSRV 1.42
#[cfg(test)]
#[macro_export]
Expand Down
2 changes: 1 addition & 1 deletion src/naive/date.rs
Expand Up @@ -832,7 +832,7 @@ impl NaiveDate {

/// Returns the packed month-day-flags.
#[inline]
fn mdf(&self) -> Mdf {
pub(crate) fn mdf(&self) -> Mdf {
pickfire marked this conversation as resolved.
Show resolved Hide resolved
self.of().to_mdf()
}

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);
Copy link
Author

@pickfire pickfire Jul 6, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The thing I see is that Mdf needs to be exposed, so I am not sure if it is a good tradeoff since the internals are being exposed. As well as mdf function.


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 @@ -795,7 +795,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) {
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

And these to get internal values.

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 @@ -475,6 +475,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!(delta.eq(py_delta).unwrap());
};
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
}