From 16be081f01efc1238ca23a0de16aa05dc2f08501 Mon Sep 17 00:00:00 2001 From: Ivan Smirnov Date: Sat, 16 Sep 2023 12:07:33 +0100 Subject: [PATCH] add conversion support for `either::Either` --- Cargo.toml | 4 +- src/conversions/either.rs | 146 ++++++++++++++++++++++++++++++++++++++ src/conversions/mod.rs | 1 + 3 files changed, 150 insertions(+), 1 deletion(-) create mode 100644 src/conversions/either.rs diff --git a/Cargo.toml b/Cargo.toml index 2871677754e..653a4d96f9f 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -34,6 +34,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", 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 } @@ -106,6 +107,7 @@ full = [ "hashbrown", "serde", "indexmap", + "either", "eyre", "anyhow", "experimental-inspect", @@ -124,5 +126,5 @@ 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"] diff --git a/src/conversions/either.rs b/src/conversions/either.rs new file mode 100644 index 00000000000..a665c1b2fc5 --- /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 eyre 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().unwrap(); +//! let either_int: Either = py_int.extract().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 5544dc23532..f9a65ba4f3f 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;