diff --git a/Cargo.toml b/Cargo.toml index 862ef6d0..1b6416f0 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -37,6 +37,7 @@ futures-util = "0.3" # 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 } +either = { version = "1.9", optional = true } eyre = { version = ">= 0.4, < 0.7", optional = true } hashbrown = { version = ">= 0.9, < 0.15", optional = true } indexmap = { version = ">= 1.6, < 3", optional = true } @@ -111,6 +112,7 @@ full = [ "smallvec", "serde", "indexmap", + "either", "eyre", "anyhow", "experimental-inspect", @@ -129,7 +131,7 @@ members = [ [package.metadata.docs.rs] no-default-features = true -features = ["macros", "num-bigint", "num-complex", "hashbrown", "serde", "multiple-pymethods", "indexmap", "eyre", "chrono", "rust_decimal"] +features = ["macros", "num-bigint", "num-complex", "hashbrown", "serde", "multiple-pymethods", "indexmap", "eyre", "either", "chrono", "rust_decimal"] rustdoc-args = ["--cfg", "docsrs"] [workspace.lints.clippy] diff --git a/guide/src/features.md b/guide/src/features.md index b6b61ab1..8ed2a2ed 100644 --- a/guide/src/features.md +++ b/guide/src/features.md @@ -109,6 +109,10 @@ 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) +### `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. + ### `eyre` Adds a dependency on [eyre](https://docs.rs/eyre). Enables a conversion from [eyre](https://docs.rs/eyre)’s [`Report`](https://docs.rs/eyre/latest/eyre/struct.Report.html) type to [`PyErr`]({{#PYO3_DOCS_URL}}/pyo3/struct.PyErr.html), for easy error handling. diff --git a/newsfragments/3456.added.md b/newsfragments/3456.added.md new file mode 100644 index 00000000..6e9376ba --- /dev/null +++ b/newsfragments/3456.added.md @@ -0,0 +1 @@ +Add optional conversion support for `either::Either` sum type (under "either" feature). diff --git a/src/conversions/either.rs b/src/conversions/either.rs new file mode 100644 index 00000000..4a41d2bd --- /dev/null +++ b/src/conversions/either.rs @@ -0,0 +1,146 @@ +#![cfg(feature = "either")] + +//! Conversion to/from +//! [either](https://docs.rs/either/ "A library for easy idiomatic error handling and reporting in Rust applications")’s +//! [`Either`] type to a union of two Python types. +//! +//! Use of a generic sum type like [either] is common when you want to either accept one of two possible +//! types as an argument or return one of two possible types from a function, without having to define +//! a helper type manually yourself. +//! +//! # Setup +//! +//! To use this feature, add this to your **`Cargo.toml`**: +//! +//! ```toml +//! [dependencies] +//! ## change * to the version you want to use, ideally the latest. +//! either = "*" +#![doc = concat!("pyo3 = { version = \"", env!("CARGO_PKG_VERSION"), "\", features = [\"either\"] }")] +//! ``` +//! +//! Note that you must use compatible versions of either and PyO3. +//! The required either version may vary based on the version of PyO3. +//! +//! # Example: Convert a `int | str` to `Either`. +//! +//! ```rust +//! use either::Either; +//! use pyo3::{Python, ToPyObject}; +//! +//! fn main() { +//! pyo3::prepare_freethreaded_python(); +//! Python::with_gil(|py| { +//! // Create a string and an int in Python. +//! let py_str = "crab".to_object(py); +//! let py_int = 42.to_object(py); +//! // Now convert it to an Either. +//! let either_str: Either = py_str.extract(py).unwrap(); +//! let either_int: Either = py_int.extract(py).unwrap(); +//! }); +//! } +//! ``` +//! +//! [either](https://docs.rs/either/ "A library for easy idiomatic error handling and reporting in Rust applications")’s + +use crate::{ + exceptions::PyTypeError, inspect::types::TypeInfo, FromPyObject, IntoPy, PyAny, PyObject, + PyResult, Python, ToPyObject, +}; +use either::Either; + +#[cfg_attr(docsrs, doc(cfg(feature = "either")))] +impl IntoPy for Either +where + L: IntoPy, + R: IntoPy, +{ + #[inline] + fn into_py(self, py: Python<'_>) -> PyObject { + match self { + Either::Left(l) => l.into_py(py), + Either::Right(r) => r.into_py(py), + } + } +} + +#[cfg_attr(docsrs, doc(cfg(feature = "either")))] +impl ToPyObject for Either +where + L: ToPyObject, + R: ToPyObject, +{ + #[inline] + fn to_object(&self, py: Python<'_>) -> PyObject { + match self { + Either::Left(l) => l.to_object(py), + Either::Right(r) => r.to_object(py), + } + } +} + +#[cfg_attr(docsrs, doc(cfg(feature = "either")))] +impl<'source, L, R> FromPyObject<'source> for Either +where + L: FromPyObject<'source>, + R: FromPyObject<'source>, +{ + #[inline] + fn extract(obj: &'source PyAny) -> PyResult { + if let Ok(l) = obj.extract::() { + Ok(Either::Left(l)) + } else if let Ok(r) = obj.extract::() { + Ok(Either::Right(r)) + } else { + let err_msg = format!("failed to convert the value to '{}'", Self::type_input()); + Err(PyTypeError::new_err(err_msg)) + } + } + + fn type_input() -> TypeInfo { + TypeInfo::union_of(&[L::type_input(), R::type_input()]) + } +} + +#[cfg(test)] +mod tests { + use crate::exceptions::PyTypeError; + use crate::{Python, ToPyObject}; + + use either::Either; + + #[test] + fn test_either_conversion() { + type E = Either; + type E1 = Either; + type E2 = Either; + + Python::with_gil(|py| { + let l = E::Left(42); + let obj_l = l.to_object(py); + assert_eq!(obj_l.extract::(py).unwrap(), 42); + assert_eq!(obj_l.extract::(py).unwrap(), l); + + let r = E::Right("foo".to_owned()); + let obj_r = r.to_object(py); + assert_eq!(obj_r.extract::<&str>(py).unwrap(), "foo"); + assert_eq!(obj_r.extract::(py).unwrap(), r); + + let obj_s = "foo".to_object(py); + let err = obj_s.extract::(py).unwrap_err(); + assert!(err.is_instance_of::(py)); + assert_eq!( + err.to_string(), + "TypeError: failed to convert the value to 'Union[int, float]'" + ); + + let obj_i = 42.to_object(py); + assert_eq!(obj_i.extract::(py).unwrap(), E1::Left(42)); + assert_eq!(obj_i.extract::(py).unwrap(), E2::Left(42.0)); + + let obj_f = 42.0.to_object(py); + assert_eq!(obj_f.extract::(py).unwrap(), E1::Right(42.0)); + assert_eq!(obj_f.extract::(py).unwrap(), E2::Left(42.0)); + }); + } +} diff --git a/src/conversions/mod.rs b/src/conversions/mod.rs index a9c2b0cd..680ad9be 100644 --- a/src/conversions/mod.rs +++ b/src/conversions/mod.rs @@ -2,6 +2,7 @@ pub mod anyhow; pub mod chrono; +pub mod either; pub mod eyre; pub mod hashbrown; pub mod indexmap; diff --git a/src/lib.rs b/src/lib.rs index 7cb40614..842fe705 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. +//! - [`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 //! [`HashSet`] types. @@ -257,6 +258,9 @@ //! [`Serialize`]: https://docs.rs/serde/latest/serde/trait.Serialize.html //! [chrono]: https://docs.rs/chrono/ "Date and Time for Rust." //! [`chrono`]: ./chrono/index.html "Documentation about the `chrono` 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 //! [eyre]: https://docs.rs/eyre/ "A library for easy idiomatic error handling and reporting in Rust applications." //! [`Report`]: https://docs.rs/eyre/latest/eyre/struct.Report.html //! [`eyre`]: ./eyre/index.html "Documentation about the `eyre` feature."