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::types::TypeInfo;
49use crate::{
50    exceptions::PyTypeError, Borrowed, Bound, FromPyObject, IntoPyObject, IntoPyObjectExt, PyAny,
51    PyErr, Python,
52};
53use either::Either;
54
55#[cfg_attr(docsrs, doc(cfg(feature = "either")))]
56impl<'py, L, R> IntoPyObject<'py> for Either<L, R>
57where
58    L: IntoPyObject<'py>,
59    R: IntoPyObject<'py>,
60{
61    type Target = PyAny;
62    type Output = Bound<'py, Self::Target>;
63    type Error = PyErr;
64
65    fn into_pyobject(self, py: Python<'py>) -> Result<Self::Output, Self::Error> {
66        match self {
67            Either::Left(l) => l.into_bound_py_any(py),
68            Either::Right(r) => r.into_bound_py_any(py),
69        }
70    }
71}
72
73#[cfg_attr(docsrs, doc(cfg(feature = "either")))]
74impl<'a, 'py, L, R> IntoPyObject<'py> for &'a Either<L, R>
75where
76    &'a L: IntoPyObject<'py>,
77    &'a R: IntoPyObject<'py>,
78{
79    type Target = PyAny;
80    type Output = Bound<'py, Self::Target>;
81    type Error = PyErr;
82
83    fn into_pyobject(self, py: Python<'py>) -> Result<Self::Output, Self::Error> {
84        match self {
85            Either::Left(l) => l.into_bound_py_any(py),
86            Either::Right(r) => r.into_bound_py_any(py),
87        }
88    }
89}
90
91#[cfg_attr(docsrs, doc(cfg(feature = "either")))]
92impl<'a, 'py, L, R> FromPyObject<'a, 'py> for Either<L, R>
93where
94    L: FromPyObject<'a, 'py>,
95    R: FromPyObject<'a, 'py>,
96{
97    type Error = PyErr;
98
99    #[inline]
100    fn extract(obj: Borrowed<'a, 'py, PyAny>) -> Result<Self, Self::Error> {
101        if let Ok(l) = obj.extract::<L>() {
102            Ok(Either::Left(l))
103        } else if let Ok(r) = obj.extract::<R>() {
104            Ok(Either::Right(r))
105        } else {
106            // TODO: it might be nice to use the `type_input()` name here once `type_input`
107            // is not experimental, rather than the Rust type names.
108            let err_msg = format!(
109                "failed to convert the value to 'Union[{}, {}]'",
110                std::any::type_name::<L>(),
111                std::any::type_name::<R>()
112            );
113            Err(PyTypeError::new_err(err_msg))
114        }
115    }
116
117    #[cfg(feature = "experimental-inspect")]
118    fn type_input() -> TypeInfo {
119        TypeInfo::union_of(&[L::type_input(), R::type_input()])
120    }
121}
122
123#[cfg(test)]
124mod tests {
125    use std::borrow::Cow;
126
127    use crate::exceptions::PyTypeError;
128    use crate::{IntoPyObject, Python};
129
130    use crate::types::PyAnyMethods;
131    use either::Either;
132
133    #[test]
134    fn test_either_conversion() {
135        type E = Either<i32, String>;
136        type E1 = Either<i32, f32>;
137        type E2 = Either<f32, i32>;
138
139        Python::attach(|py| {
140            let l = E::Left(42);
141            let obj_l = (&l).into_pyobject(py).unwrap();
142            assert_eq!(obj_l.extract::<i32>().unwrap(), 42);
143            assert_eq!(obj_l.extract::<E>().unwrap(), l);
144
145            let r = E::Right("foo".to_owned());
146            let obj_r = (&r).into_pyobject(py).unwrap();
147            assert_eq!(obj_r.extract::<Cow<'_, str>>().unwrap(), "foo");
148            assert_eq!(obj_r.extract::<E>().unwrap(), r);
149
150            let obj_s = "foo".into_pyobject(py).unwrap();
151            let err = obj_s.extract::<E1>().unwrap_err();
152            assert!(err.is_instance_of::<PyTypeError>(py));
153            assert_eq!(
154                err.to_string(),
155                "TypeError: failed to convert the value to 'Union[i32, f32]'"
156            );
157
158            let obj_i = 42i32.into_pyobject(py).unwrap();
159            assert_eq!(obj_i.extract::<E1>().unwrap(), E1::Left(42));
160            assert_eq!(obj_i.extract::<E2>().unwrap(), E2::Left(42.0));
161
162            let obj_f = 42.0f64.into_pyobject(py).unwrap();
163            assert_eq!(obj_f.extract::<E1>().unwrap(), E1::Right(42.0));
164            assert_eq!(obj_f.extract::<E2>().unwrap(), E2::Left(42.0));
165        });
166    }
167}