diff --git a/Cargo.toml b/Cargo.toml index fd1daeb1451..31179e59a7c 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -35,6 +35,7 @@ inventory = { version = "0.3.0", optional = true } # crate integrations that can be added using the eponymous features anyhow = { version = "1.0", optional = true } chrono = { version = "0.4.25", default-features = false, optional = true } +chrono-tz = { version = ">= 0.6, < 0.9", default-features = false, optional = true } either = { version = "1.9", optional = true } eyre = { version = ">= 0.4, < 0.7", optional = true } hashbrown = { version = ">= 0.9, < 0.15", optional = true } @@ -47,7 +48,8 @@ smallvec = { version = "1.0", optional = true } [dev-dependencies] assert_approx_eq = "1.1.0" -chrono = { version = "0.4.25" } +chrono = "0.4.25" +chrono-tz = ">= 0.6, < 0.9" # Required for "and $N others" normalization trybuild = ">=1.0.70" proptest = { version = "1.0", default-features = false, features = ["std"] } @@ -108,6 +110,7 @@ full = [ "macros", # "multiple-pymethods", # TODO re-add this when MSRV is greater than 1.62 "chrono", + "chrono-tz", "num-bigint", "num-complex", "hashbrown", diff --git a/guide/src/features.md b/guide/src/features.md index edcf3772b26..43124e0076e 100644 --- a/guide/src/features.md +++ b/guide/src/features.md @@ -115,6 +115,12 @@ Adds a dependency on [chrono](https://docs.rs/chrono). Enables a conversion from - [NaiveTime](https://docs.rs/chrono/latest/chrono/naive/struct.NaiveTime.html) -> [`PyTime`]({{#PYO3_DOCS_URL}}/pyo3/types/struct.PyTime.html) - [DateTime](https://docs.rs/chrono/latest/chrono/struct.DateTime.html) -> [`PyDateTime`]({{#PYO3_DOCS_URL}}/pyo3/types/struct.PyDateTime.html) +### `chrono-tz` + +Adds a dependency on [chrono-tz](https://docs.rs/chrono-tz). +Enables conversion from and to [`Tz`](https://docs.rs/chrono-tz/latest/chrono_tz/enum.Tz.html). +It requires at least Python 3.9. + ### `either` Adds a dependency on [either](https://docs.rs/either). Enables a conversions into [either](https://docs.rs/either)’s [`Either`](https://docs.rs/either/latest/either/struct.Report.html) type. diff --git a/newsfragments/3730.added.md b/newsfragments/3730.added.md new file mode 100644 index 00000000000..7e287245eb1 --- /dev/null +++ b/newsfragments/3730.added.md @@ -0,0 +1 @@ +`chrono-tz` feature allowing conversion between `chrono_tz::Tz` and `zoneinfo.ZoneInfo` \ No newline at end of file diff --git a/src/conversions/chrono.rs b/src/conversions/chrono.rs index cc0eb73a8ef..0e95375b5bd 100644 --- a/src/conversions/chrono.rs +++ b/src/conversions/chrono.rs @@ -9,8 +9,6 @@ //! //! ```toml //! [dependencies] -//! # change * to the latest versions -//! pyo3 = { version = "*", features = ["chrono"] } //! chrono = "0.4" #![doc = concat!("pyo3 = { version = \"", env!("CARGO_PKG_VERSION"), "\", features = [\"chrono\"] }")] //! ``` @@ -18,7 +16,7 @@ //! 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 `PyDateTime` to chrono's `DateTime` +//! # Example: Convert a `datetime.datetime` to chrono's `DateTime` //! //! ```rust //! use chrono::{DateTime, Duration, TimeZone, Utc}; diff --git a/src/conversions/chrono_tz.rs b/src/conversions/chrono_tz.rs new file mode 100644 index 00000000000..041114b5054 --- /dev/null +++ b/src/conversions/chrono_tz.rs @@ -0,0 +1,113 @@ +#![cfg(all(Py_3_9, feature = "chrono-tz"))] + +//! Conversions to and from [chrono-tz](https://docs.rs/chrono-tz/)’s `Tz`. +//! +//! This feature requires at least Python 3.9. +//! +//! # Setup +//! +//! To use this feature, add this to your **`Cargo.toml`**: +//! +//! ```toml +//! [dependencies] +//! chrono-tz = "0.8" +#![doc = concat!("pyo3 = { version = \"", env!("CARGO_PKG_VERSION"), "\", features = [\"chrono-tz\"] }")] +//! ``` +//! +//! Note that you must use compatible versions of chrono, chrono-tz and PyO3. +//! The required chrono version may vary based on the version of PyO3. +//! +//! # Example: Convert a `zoneinfo.ZoneInfo` to chrono-tz's `Tz` +//! +//! ```rust +//! use chrono_tz::Tz; +//! use pyo3::{Python, ToPyObject}; +//! +//! fn main() { +//! pyo3::prepare_freethreaded_python(); +//! Python::with_gil(|py| { +//! // Convert to Python +//! let py_tzinfo = Tz::Europe__Paris.to_object(py); +//! // Convert back to Rust +//! assert_eq!(py_tzinfo.extract::(py).unwrap(), Tz::Europe__Paris); +//! }); +//! } +//! ``` +use crate::exceptions::PyValueError; +use crate::sync::GILOnceCell; +use crate::types::PyType; +use crate::{intern, FromPyObject, IntoPy, Py, PyAny, PyObject, PyResult, Python, ToPyObject}; +use chrono_tz::Tz; +use std::str::FromStr; + +impl ToPyObject for Tz { + fn to_object(&self, py: Python<'_>) -> PyObject { + static ZONE_INFO: GILOnceCell> = GILOnceCell::new(); + ZONE_INFO + .get_or_try_init_type_ref(py, "zoneinfo", "ZoneInfo") + .unwrap() + .call1((self.name(),)) + .unwrap() + .into() + } +} + +impl IntoPy for Tz { + fn into_py(self, py: Python<'_>) -> PyObject { + self.to_object(py) + } +} + +impl FromPyObject<'_> for Tz { + fn extract(ob: &PyAny) -> PyResult { + Tz::from_str(ob.getattr(intern!(ob.py(), "key"))?.extract()?) + .map_err(|e| PyValueError::new_err(e.to_string())) + } +} + +#[cfg(all(test, not(windows)))] // Troubles loading timezones on Windows +mod tests { + use super::*; + + #[test] + fn test_frompyobject() { + Python::with_gil(|py| { + assert_eq!( + new_zoneinfo(py, "Europe/Paris").extract::().unwrap(), + Tz::Europe__Paris + ); + assert_eq!(new_zoneinfo(py, "UTC").extract::().unwrap(), Tz::UTC); + assert_eq!( + new_zoneinfo(py, "Etc/GMT-5").extract::().unwrap(), + Tz::Etc__GMTMinus5 + ); + }); + } + + #[test] + fn test_topyobject() { + Python::with_gil(|py| { + let assert_eq = |l: PyObject, r: &PyAny| { + assert!(l.as_ref(py).eq(r).unwrap()); + }; + + assert_eq( + Tz::Europe__Paris.to_object(py), + new_zoneinfo(py, "Europe/Paris"), + ); + assert_eq(Tz::UTC.to_object(py), new_zoneinfo(py, "UTC")); + assert_eq( + Tz::Etc__GMTMinus5.to_object(py), + new_zoneinfo(py, "Etc/GMT-5"), + ); + }); + } + + fn new_zoneinfo<'a>(py: Python<'a>, name: &str) -> &'a PyAny { + zoneinfo_class(py).call1((name,)).unwrap() + } + + fn zoneinfo_class(py: Python<'_>) -> &PyAny { + py.import("zoneinfo").unwrap().getattr("ZoneInfo").unwrap() + } +} diff --git a/src/conversions/mod.rs b/src/conversions/mod.rs index 680ad9be6d5..3d785c02381 100644 --- a/src/conversions/mod.rs +++ b/src/conversions/mod.rs @@ -2,6 +2,7 @@ pub mod anyhow; pub mod chrono; +pub mod chrono_tz; pub mod either; pub mod eyre; pub mod hashbrown; diff --git a/src/lib.rs b/src/lib.rs index 4802cbc2712..3f91eb56913 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -82,6 +82,7 @@ //! The following features enable interactions with other crates in the Rust ecosystem: //! - [`anyhow`]: Enables a conversion from [anyhow]’s [`Error`][anyhow_error] type to [`PyErr`]. //! - [`chrono`]: Enables a conversion from [chrono]'s structures to the equivalent Python ones. +//! - [`chrono-tz`]: Enables a conversion from [chrono-tz]'s `Tz` enum. Requires Python 3.9+. //! - [`either`]: Enables conversions between Python objects and [either]'s [`Either`] type. //! - [`eyre`]: Enables a conversion from [eyre]’s [`Report`] type to [`PyErr`]. //! - [`hashbrown`]: Enables conversions between Python objects and [hashbrown]'s [`HashMap`] and @@ -257,7 +258,9 @@ //! [`Deserialize`]: https://docs.rs/serde/latest/serde/trait.Deserialize.html //! [`Serialize`]: https://docs.rs/serde/latest/serde/trait.Serialize.html //! [chrono]: https://docs.rs/chrono/ "Date and Time for Rust." +//! [chrono-tz]: https://docs.rs/chrono-tz/ "TimeZone implementations for chrono from the IANA database." //! [`chrono`]: ./chrono/index.html "Documentation about the `chrono` feature." +//! [`chrono-tz`]: ./chrono-tz/index.html "Documentation about the `chrono-tz` feature." //! [either]: https://docs.rs/either/ "A type that represents one of two alternatives." //! [`either`]: ./either/index.html "Documentation about the `either` feature." //! [`Either`]: https://docs.rs/either/latest/either/enum.Either.html diff --git a/src/tests/hygiene/misc.rs b/src/tests/hygiene/misc.rs index 2e7d3e6f9ee..9da8793ed4c 100644 --- a/src/tests/hygiene/misc.rs +++ b/src/tests/hygiene/misc.rs @@ -2,6 +2,7 @@ #[derive(crate::FromPyObject)] #[pyo3(crate = "crate")] +#[allow(dead_code)] struct Derive1(i32); // newtype case #[derive(crate::FromPyObject)]