From 9d6946148416e27d0233fba30d86ebbe366e2231 Mon Sep 17 00:00:00 2001 From: avhz Date: Fri, 1 Nov 2024 00:57:23 +0100 Subject: [PATCH 1/2] feat: support `time-rs` crate --- Cargo.toml | 34 +- src/conversions/mod.rs | 1 + src/conversions/time.rs | 1584 +++++++++++++++++++++++++++++++++++++++ 3 files changed, 1613 insertions(+), 6 deletions(-) create mode 100644 src/conversions/time.rs diff --git a/Cargo.toml b/Cargo.toml index 9e931ed00b6..099ded93271 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -10,11 +10,23 @@ repository = "https://github.com/pyo3/pyo3" documentation = "https://docs.rs/crate/pyo3/" categories = ["api-bindings", "development-tools::ffi"] license = "MIT OR Apache-2.0" -exclude = ["/.gitignore", ".cargo/config", "/codecov.yml", "/Makefile", "/pyproject.toml", "/noxfile.py", "/.github", "/tests/test_compile_error.rs", "/tests/ui"] +exclude = [ + "/.gitignore", + ".cargo/config", + "/codecov.yml", + "/Makefile", + "/pyproject.toml", + "/noxfile.py", + "/.github", + "/tests/test_compile_error.rs", + "/tests/ui", +] edition = "2021" rust-version = "1.63" [dependencies] +time = "0.3.20" + cfg-if = "1.0" libc = "0.2.62" memoffset = "0.9" @@ -41,7 +53,7 @@ hashbrown = { version = ">= 0.14.5, < 0.16", optional = true } indexmap = { version = ">= 2.5.0, < 3", optional = true } num-bigint = { version = "0.4.2", optional = true } num-complex = { version = ">= 0.4.6, < 0.5", optional = true } -num-rational = {version = "0.4.1", optional = true } +num-rational = { version = "0.4.1", optional = true } rust_decimal = { version = "1.15", default-features = false, optional = true } serde = { version = "1.0", optional = true } smallvec = { version = "1.0", optional = true } @@ -63,10 +75,12 @@ rayon = "1.6.1" futures = "0.3.28" tempfile = "3.12.0" static_assertions = "1.1.0" -uuid = {version = "1.10.0", features = ["v4"] } +uuid = { version = "1.10.0", features = ["v4"] } [build-dependencies] -pyo3-build-config = { path = "pyo3-build-config", version = "=0.23.0-dev", features = ["resolve-config"] } +pyo3-build-config = { path = "pyo3-build-config", version = "=0.23.0-dev", features = [ + "resolve-config", +] } [features] default = ["macros"] @@ -96,8 +110,16 @@ abi3 = ["pyo3-build-config/abi3", "pyo3-ffi/abi3"] abi3-py37 = ["abi3-py38", "pyo3-build-config/abi3-py37", "pyo3-ffi/abi3-py37"] abi3-py38 = ["abi3-py39", "pyo3-build-config/abi3-py38", "pyo3-ffi/abi3-py38"] abi3-py39 = ["abi3-py310", "pyo3-build-config/abi3-py39", "pyo3-ffi/abi3-py39"] -abi3-py310 = ["abi3-py311", "pyo3-build-config/abi3-py310", "pyo3-ffi/abi3-py310"] -abi3-py311 = ["abi3-py312", "pyo3-build-config/abi3-py311", "pyo3-ffi/abi3-py311"] +abi3-py310 = [ + "abi3-py311", + "pyo3-build-config/abi3-py310", + "pyo3-ffi/abi3-py310", +] +abi3-py311 = [ + "abi3-py312", + "pyo3-build-config/abi3-py311", + "pyo3-ffi/abi3-py311", +] abi3-py312 = ["abi3", "pyo3-build-config/abi3-py312", "pyo3-ffi/abi3-py312"] # Automatically generates `python3.dll` import libraries for Windows targets. diff --git a/src/conversions/mod.rs b/src/conversions/mod.rs index 53ecf849c07..5da61231592 100644 --- a/src/conversions/mod.rs +++ b/src/conversions/mod.rs @@ -14,3 +14,4 @@ pub mod rust_decimal; pub mod serde; pub mod smallvec; mod std; +pub mod time; diff --git a/src/conversions/time.rs b/src/conversions/time.rs new file mode 100644 index 00000000000..8d723982efd --- /dev/null +++ b/src/conversions/time.rs @@ -0,0 +1,1584 @@ +// #![cfg(feature = "chrono")] + +// //! Conversions to and from [chrono](https://docs.rs/chrono/)’s `Duration`, +// //! `Date`, `Time`, `OffsetDateTime`, `FixedOffset`, and `Utc`. +// //! +// //! # Setup +// //! +// //! To use this feature, add this to your **`Cargo.toml`**: +// //! +// //! ```toml +// //! [dependencies] +// //! chrono = "0.4" +// #![doc = concat!("pyo3 = { version = \"", env!("CARGO_PKG_VERSION"), "\", features = [\"chrono\"] }")] +// //! ``` +// //! +// //! Note that you must use compatible versions of chrono and PyO3. +// //! The required chrono version may vary based on the version of PyO3. +// //! +// //! # Example: Convert a `datetime.datetime` to chrono's `OffsetDateTime` +// //! +// //! ```rust +// //! use chrono::{OffsetDateTime, Duration, TimeZone, Utc}; +// //! use pyo3::{Python, PyResult, IntoPyObject, types::PyAnyMethods}; +// //! +// //! fn main() -> PyResult<()> { +// //! pyo3::prepare_freethreaded_python(); +// //! Python::with_gil(|py| { +// //! // Build some chrono values +// //! let chrono_datetime = Utc.with_ymd_and_hms(2022, 1, 1, 12, 0, 0).unwrap(); +// //! let chrono_duration = Duration::seconds(1); +// //! // Convert them to Python +// //! let py_datetime = chrono_datetime.into_pyobject(py)?; +// //! let py_timedelta = chrono_duration.into_pyobject(py)?; +// //! // Do an operation in Python +// //! let py_sum = py_datetime.call_method1("__add__", (py_timedelta,))?; +// //! // Convert back to Rust +// //! let chrono_sum: OffsetDateTime = py_sum.extract()?; +// //! println!("OffsetDateTime: {}", chrono_datetime); +// //! Ok(()) +// //! }) +// //! } +// //! ``` + +use crate::conversion::IntoPyObject; +use crate::exceptions::{PyTypeError, PyUserWarning, PyValueError}; +#[cfg(Py_LIMITED_API)] +use crate::sync::GILOnceCell; +use crate::types::any::PyAnyMethods; +#[cfg(not(Py_LIMITED_API))] +use crate::types::datetime::timezone_from_offset; +use crate::types::PyNone; +#[cfg(not(Py_LIMITED_API))] +use crate::types::{ + PyDate, PyDateAccess, PyDateTime, PyDelta, PyDeltaAccess, PyTime, PyTimeAccess, PyTzInfo, + PyTzInfoAccess, +}; +use crate::{ffi, Bound, FromPyObject, PyAny, PyErr, PyObject, PyResult, Python}; +#[cfg(Py_LIMITED_API)] +use crate::{intern, DowncastError}; +#[allow(deprecated)] +use crate::{IntoPy, ToPyObject}; + +// use chrono::{ +// offset::{FixedOffset, Utc}, +// DateTime, Datelike, Duration, NaiveDate, NaiveDateTime, NaiveTime, Offset, TimeZone, Timelike, +// }; + +use time::OffsetDateTime; +use time::PrimitiveDateTime; +use time::Time; +use time::{Date, UtcOffset}; +use time::{Duration, Month}; + +#[allow(deprecated)] +impl ToPyObject for Duration { + #[inline] + fn to_object(&self, py: Python<'_>) -> PyObject { + self.into_pyobject(py).unwrap().into_any().unbind() + } +} + +#[allow(deprecated)] +impl IntoPy for Duration { + #[inline] + fn into_py(self, py: Python<'_>) -> PyObject { + self.into_pyobject(py).unwrap().into_any().unbind() + } +} + +impl<'py> IntoPyObject<'py> for Duration { + #[cfg(Py_LIMITED_API)] + type Target = PyAny; + #[cfg(not(Py_LIMITED_API))] + type Target = PyDelta; + type Output = Bound<'py, Self::Target>; + type Error = PyErr; + + fn into_pyobject(self, py: Python<'py>) -> Result { + // Total number of days + let days = self.whole_days(); + // Remainder of seconds + let secs_dur = self - Duration::days(days); + let secs = secs_dur.whole_seconds(); + // Fractional part of the microseconds + let micros = (secs_dur - Duration::seconds(secs_dur.whole_seconds())).whole_microseconds(); + // This should never panic since we are just getting the fractional + // part of the total microseconds, which should never overflow. + // .unwrap(); + + #[cfg(not(Py_LIMITED_API))] + { + // We do not need to check the days i64 to i32 cast from rust because + // python will panic with OverflowError. + // We pass true as the `normalize` parameter since we'd need to do several checks here to + // avoid that, and it shouldn't have a big performance impact. + // The seconds and microseconds cast should never overflow since it's at most the number of seconds per day + PyDelta::new( + py, + days.try_into().unwrap_or(i32::MAX), + secs.try_into()?, + micros.try_into()?, + true, + ) + } + + #[cfg(Py_LIMITED_API)] + { + DatetimeTypes::try_get(py) + .and_then(|dt| dt.timedelta.bind(py).call1((days, secs, micros))) + } + } +} + +impl<'py> IntoPyObject<'py> for &Duration { + #[cfg(Py_LIMITED_API)] + type Target = PyAny; + #[cfg(not(Py_LIMITED_API))] + type Target = PyDelta; + type Output = Bound<'py, Self::Target>; + type Error = PyErr; + + #[inline] + fn into_pyobject(self, py: Python<'py>) -> Result { + (*self).into_pyobject(py) + } +} + +impl FromPyObject<'_> for Month { + fn extract_bound(ob: &Bound<'_, PyAny>) -> PyResult { + ob.extract::()? // 1-based month + .saturating_sub(1) + .try_into() + .or_else(|_| Err(PyValueError::new_err("invalid month"))) + } +} + +impl FromPyObject<'_> for Duration { + fn extract_bound(ob: &Bound<'_, PyAny>) -> PyResult { + // 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 + #[cfg(not(Py_LIMITED_API))] + let (days, seconds, microseconds) = { + let delta = ob.downcast::()?; + ( + delta.get_days().into(), + delta.get_seconds().into(), + delta.get_microseconds().into(), + ) + }; + #[cfg(Py_LIMITED_API)] + let (days, seconds, microseconds) = { + check_type(ob, &DatetimeTypes::get(ob.py()).timedelta, "PyDelta")?; + ( + ob.getattr(intern!(ob.py(), "days"))?.extract()?, + ob.getattr(intern!(ob.py(), "seconds"))?.extract()?, + ob.getattr(intern!(ob.py(), "microseconds"))?.extract()?, + ) + }; + Ok( + Duration::days(days) + + Duration::seconds(seconds) + + Duration::microseconds(microseconds), + ) + } +} + +#[allow(deprecated)] +impl ToPyObject for Date { + #[inline] + fn to_object(&self, py: Python<'_>) -> PyObject { + self.into_pyobject(py).unwrap().into_any().unbind() + } +} + +#[allow(deprecated)] +impl IntoPy for Date { + #[inline] + fn into_py(self, py: Python<'_>) -> PyObject { + self.into_pyobject(py).unwrap().into_any().unbind() + } +} + +impl<'py> IntoPyObject<'py> for Date { + #[cfg(Py_LIMITED_API)] + type Target = PyAny; + #[cfg(not(Py_LIMITED_API))] + type Target = PyDate; + type Output = Bound<'py, Self::Target>; + type Error = PyErr; + + fn into_pyobject(self, py: Python<'py>) -> Result { + let DateArgs { year, month, day } = (&self).into(); + #[cfg(not(Py_LIMITED_API))] + { + PyDate::new(py, year, month, day) + } + + #[cfg(Py_LIMITED_API)] + { + DatetimeTypes::try_get(py).and_then(|dt| dt.date.bind(py).call1((year, month, day))) + } + } +} + +impl<'py> IntoPyObject<'py> for &Date { + #[cfg(Py_LIMITED_API)] + type Target = PyAny; + #[cfg(not(Py_LIMITED_API))] + type Target = PyDate; + type Output = Bound<'py, Self::Target>; + type Error = PyErr; + + #[inline] + fn into_pyobject(self, py: Python<'py>) -> Result { + (*self).into_pyobject(py) + } +} + +impl FromPyObject<'_> for Date { + fn extract_bound(ob: &Bound<'_, PyAny>) -> PyResult { + #[cfg(not(Py_LIMITED_API))] + { + let date = ob.downcast::()?; + py_date_to_naive_date(date) + } + #[cfg(Py_LIMITED_API)] + { + check_type(ob, &DatetimeTypes::get(ob.py()).date, "PyDate")?; + py_date_to_naive_date(ob) + } + } +} + +#[allow(deprecated)] +impl ToPyObject for Time { + #[inline] + fn to_object(&self, py: Python<'_>) -> PyObject { + self.into_pyobject(py).unwrap().into_any().unbind() + } +} + +#[allow(deprecated)] +impl IntoPy for Time { + #[inline] + fn into_py(self, py: Python<'_>) -> PyObject { + self.into_pyobject(py).unwrap().into_any().unbind() + } +} + +impl<'py> IntoPyObject<'py> for Time { + #[cfg(Py_LIMITED_API)] + type Target = PyAny; + #[cfg(not(Py_LIMITED_API))] + type Target = PyTime; + type Output = Bound<'py, Self::Target>; + type Error = PyErr; + + fn into_pyobject(self, py: Python<'py>) -> Result { + let TimeArgs { + hour, + min, + sec, + micro, + truncated_leap_second, + } = (&self).into(); + + #[cfg(not(Py_LIMITED_API))] + let time = PyTime::new(py, hour, min, sec, micro, None)?; + + #[cfg(Py_LIMITED_API)] + let time = DatetimeTypes::try_get(py) + .and_then(|dt| dt.time.bind(py).call1((hour, min, sec, micro)))?; + + if truncated_leap_second { + warn_truncated_leap_second(&time); + } + + Ok(time) + } +} + +impl<'py> IntoPyObject<'py> for &Time { + #[cfg(Py_LIMITED_API)] + type Target = PyAny; + #[cfg(not(Py_LIMITED_API))] + type Target = PyTime; + type Output = Bound<'py, Self::Target>; + type Error = PyErr; + + #[inline] + fn into_pyobject(self, py: Python<'py>) -> Result { + (*self).into_pyobject(py) + } +} + +impl FromPyObject<'_> for Time { + fn extract_bound(ob: &Bound<'_, PyAny>) -> PyResult