Skip to content

Commit

Permalink
Conversion between chrono_tz::Tz and zoneinfo.ZoneInfo
Browse files Browse the repository at this point in the history
  • Loading branch information
Tpt committed Jan 8, 2024
1 parent 4b17287 commit 2df6533
Show file tree
Hide file tree
Showing 8 changed files with 130 additions and 4 deletions.
5 changes: 4 additions & 1 deletion Cargo.toml
Expand Up @@ -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 }
Expand All @@ -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"] }
Expand Down Expand Up @@ -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",
Expand Down
6 changes: 6 additions & 0 deletions guide/src/features.md
Expand Up @@ -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.
Expand Down
1 change: 1 addition & 0 deletions newsfragments/3730.added.md
@@ -0,0 +1 @@
`chrono-tz` feature allowing conversion between `chrono_tz::Tz` and `zoneinfo.ZoneInfo`
4 changes: 1 addition & 3 deletions src/conversions/chrono.rs
Expand Up @@ -9,16 +9,14 @@
//!
//! ```toml
//! [dependencies]
//! # change * to the latest versions
//! pyo3 = { version = "*", features = ["chrono"] }
//! 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 `PyDateTime` to chrono's `DateTime<Utc>`
//! # Example: Convert a `datetime.datetime` to chrono's `DateTime<Utc>`
//!
//! ```rust
//! use chrono::{DateTime, Duration, TimeZone, Utc};
Expand Down
113 changes: 113 additions & 0 deletions 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::<Tz>(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<Py<PyType>> = GILOnceCell::new();
ZONE_INFO
.get_or_try_init_type_ref(py, "zoneinfo", "ZoneInfo")
.unwrap()
.call1((self.name(),))
.unwrap()
.into()
}
}

impl IntoPy<PyObject> for Tz {
fn into_py(self, py: Python<'_>) -> PyObject {
self.to_object(py)
}
}

impl FromPyObject<'_> for Tz {
fn extract(ob: &PyAny) -> PyResult<Tz> {
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::<Tz>().unwrap(),
Tz::Europe__Paris
);
assert_eq!(new_zoneinfo(py, "UTC").extract::<Tz>().unwrap(), Tz::UTC);
assert_eq!(
new_zoneinfo(py, "Etc/GMT-5").extract::<Tz>().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()
}
}
1 change: 1 addition & 0 deletions src/conversions/mod.rs
Expand Up @@ -2,6 +2,7 @@

pub mod anyhow;
pub mod chrono;
pub mod chrono_tz;
pub mod either;
pub mod eyre;
pub mod hashbrown;
Expand Down
3 changes: 3 additions & 0 deletions src/lib.rs
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down
1 change: 1 addition & 0 deletions src/tests/hygiene/misc.rs
Expand Up @@ -2,6 +2,7 @@

#[derive(crate::FromPyObject)]
#[pyo3(crate = "crate")]
#[allow(dead_code)]
struct Derive1(i32); // newtype case

#[derive(crate::FromPyObject)]
Expand Down

0 comments on commit 2df6533

Please sign in to comment.