Skip to main content

pyo3/conversions/
either.rs

1#![cfg(feature = "either")]
2
3//! Conversion to/from
4//! [either](https://docs.rs/either/ "A library for easy idiomatic error handling and reporting in Rust applications")’s
5//! [`Either`] type to a union of two Python types.
6//!
7//! Use of a generic sum type like [either] is common when you want to either accept one of two possible
8//! types as an argument or return one of two possible types from a function, without having to define
9//! a helper type manually yourself.
10//!
11//! # Setup
12//!
13//! To use this feature, add this to your **`Cargo.toml`**:
14//!
15//! ```toml
16//! [dependencies]
17//! ## change * to the version you want to use, ideally the latest.
18//! either = "*"
19#![doc = concat!("pyo3 = { version = \"", env!("CARGO_PKG_VERSION"),  "\", features = [\"either\"] }")]
20//! ```
21//!
22//! Note that you must use compatible versions of either and PyO3.
23//! The required either version may vary based on the version of PyO3.
24//!
25//! # Example: Convert a `int | str` to `Either<i32, String>`.
26//!
27//! ```rust
28//! use either::Either;
29//! use pyo3::{Python, PyResult, IntoPyObject, types::PyAnyMethods};
30//!
31//! fn main() -> PyResult<()> {
32//!     Python::initialize();
33//!     Python::attach(|py| {
34//!         // Create a string and an int in Python.
35//!         let py_str = "crab".into_pyobject(py)?;
36//!         let py_int = 42i32.into_pyobject(py)?;
37//!         // Now convert it to an Either<i32, String>.
38//!         let either_str: Either<i32, String> = py_str.extract()?;
39//!         let either_int: Either<i32, String> = py_int.extract()?;
40//!         Ok(())
41//!     })
42//! }
43//! ```
44//!
45//! [either](https://docs.rs/either/ "A library for easy idiomatic error handling and reporting in Rust applications")’s
46
47#[cfg(feature = "experimental-inspect")]
48use crate::inspect::PyStaticExpr;
49#[cfg(feature = "experimental-inspect")]
50use crate::type_hint_union;
51use crate::{
52    exceptions::PyTypeError, Borrowed, Bound, FromPyObject, IntoPyObject, IntoPyObjectExt, PyAny,
53    PyErr, Python,
54};
55use either::Either;
56
57#[cfg_attr(docsrs, doc(cfg(feature = "either")))]
58impl<'py, L, R> IntoPyObject<'py> for Either<L, R>
59where
60    L: IntoPyObject<'py>,
61    R: IntoPyObject<'py>,
62{
63    type Target = PyAny;
64    type Output = Bound<'py, Self::Target>;
65    type Error = PyErr;
66
67    #[cfg(feature = "experimental-inspect")]
68    const OUTPUT_TYPE: PyStaticExpr = type_hint_union!(L::OUTPUT_TYPE, R::OUTPUT_TYPE);
69
70    fn into_pyobject(self, py: Python<'py>) -> Result<Self::Output, Self::Error> {
71        match self {
72            Either::Left(l) => l.into_bound_py_any(py),
73            Either::Right(r) => r.into_bound_py_any(py),
74        }
75    }
76}
77
78#[cfg_attr(docsrs, doc(cfg(feature = "either")))]
79impl<'a, 'py, L, R> IntoPyObject<'py> for &'a Either<L, R>
80where
81    &'a L: IntoPyObject<'py>,
82    &'a R: IntoPyObject<'py>,
83{
84    type Target = PyAny;
85    type Output = Bound<'py, Self::Target>;
86    type Error = PyErr;
87
88    #[cfg(feature = "experimental-inspect")]
89    const OUTPUT_TYPE: PyStaticExpr = type_hint_union!(<&L>::OUTPUT_TYPE, <&R>::OUTPUT_TYPE);
90
91    fn into_pyobject(self, py: Python<'py>) -> Result<Self::Output, Self::Error> {
92        match self {
93            Either::Left(l) => l.into_bound_py_any(py),
94            Either::Right(r) => r.into_bound_py_any(py),
95        }
96    }
97}
98
99#[cfg_attr(docsrs, doc(cfg(feature = "either")))]
100impl<'a, 'py, L, R> FromPyObject<'a, 'py> for Either<L, R>
101where
102    L: FromPyObject<'a, 'py>,
103    R: FromPyObject<'a, 'py>,
104{
105    type Error = PyErr;
106
107    #[cfg(feature = "experimental-inspect")]
108    const INPUT_TYPE: PyStaticExpr = type_hint_union!(L::INPUT_TYPE, R::INPUT_TYPE);
109
110    #[inline]
111    fn extract(obj: Borrowed<'a, 'py, PyAny>) -> Result<Self, Self::Error> {
112        if let Ok(l) = obj.extract::<L>() {
113            Ok(Either::Left(l))
114        } else if let Ok(r) = obj.extract::<R>() {
115            Ok(Either::Right(r))
116        } else {
117            // TODO: it might be nice to use the `type_input()` name here once `type_input`
118            // is not experimental, rather than the Rust type names.
119            let err_msg = format!(
120                "failed to convert the value to 'Union[{}, {}]'",
121                std::any::type_name::<L>(),
122                std::any::type_name::<R>()
123            );
124            Err(PyTypeError::new_err(err_msg))
125        }
126    }
127}
128
129#[cfg(test)]
130mod tests {
131    use std::borrow::Cow;
132
133    use crate::exceptions::PyTypeError;
134    use crate::{IntoPyObject, Python};
135
136    use crate::types::PyAnyMethods;
137    use either::Either;
138
139    #[test]
140    fn test_either_conversion() {
141        type E = Either<i32, String>;
142        type E1 = Either<i32, f32>;
143        type E2 = Either<f32, i32>;
144
145        Python::attach(|py| {
146            let l = E::Left(42);
147            let obj_l = (&l).into_pyobject(py).unwrap();
148            assert_eq!(obj_l.extract::<i32>().unwrap(), 42);
149            assert_eq!(obj_l.extract::<E>().unwrap(), l);
150
151            let r = E::Right("foo".to_owned());
152            let obj_r = (&r).into_pyobject(py).unwrap();
153            assert_eq!(obj_r.extract::<Cow<'_, str>>().unwrap(), "foo");
154            assert_eq!(obj_r.extract::<E>().unwrap(), r);
155
156            let obj_s = "foo".into_pyobject(py).unwrap();
157            let err = obj_s.extract::<E1>().unwrap_err();
158            assert!(err.is_instance_of::<PyTypeError>(py));
159            assert_eq!(
160                err.to_string(),
161                "TypeError: failed to convert the value to 'Union[i32, f32]'"
162            );
163
164            let obj_i = 42i32.into_pyobject(py).unwrap();
165            assert_eq!(obj_i.extract::<E1>().unwrap(), E1::Left(42));
166            assert_eq!(obj_i.extract::<E2>().unwrap(), E2::Left(42.0));
167
168            let obj_f = 42.0f64.into_pyobject(py).unwrap();
169            assert_eq!(obj_f.extract::<E1>().unwrap(), E1::Right(42.0));
170            assert_eq!(obj_f.extract::<E2>().unwrap(), E2::Left(42.0));
171        });
172    }
173}