pyo3/err/
cast_error.rs

1use std::borrow::Cow;
2
3use crate::{
4    exceptions,
5    types::{PyAnyMethods, PyStringMethods, PyTuple, PyTupleMethods, PyType, PyTypeMethods},
6    Borrowed, Bound, IntoPyObjectExt, Py, PyAny, PyErr, PyErrArguments, Python,
7};
8
9/// Error that indicates a failure to convert a PyAny to a more specific Python type.
10#[derive(Debug)]
11pub struct CastError<'a, 'py> {
12    /// The original object that failed to convert.
13    from: Borrowed<'a, 'py, PyAny>,
14    /// The type we tried (and failed) to convert to.
15    /// (see `PyTypeCheck::classinfo_object`)
16    to: Bound<'py, PyAny>,
17}
18
19impl<'a, 'py> CastError<'a, 'py> {
20    /// Create a new `CastError` representing a failure to convert the object
21    /// `from` into the type `to`.
22    ///
23    /// As with [`PyTypeCheck::classinfo_object`][crate::PyTypeCheck::classinfo_object],
24    /// valid `to` values are those which can be used with `isinstance`, such as `type`
25    /// objects, tuples of `type` objects, or `typing.Union` instances.
26    #[inline]
27    pub fn new(from: Borrowed<'a, 'py, PyAny>, to: Bound<'py, PyAny>) -> Self {
28        Self { from, to }
29    }
30}
31
32/// Error that indicates a failure to convert a PyAny to a more specific Python type.
33#[derive(Debug)]
34pub struct CastIntoError<'py> {
35    from: Bound<'py, PyAny>,
36    to: Bound<'py, PyAny>,
37}
38
39impl<'py> CastIntoError<'py> {
40    /// Create a new `CastIntoError` representing a failure to convert the object
41    /// `from` into the type `to`.
42    ///
43    /// Equivalent to [`CastError::new`] for owned objects.
44    #[inline]
45    pub fn new(from: Bound<'py, PyAny>, to: Bound<'py, PyAny>) -> Self {
46        Self { from, to }
47    }
48
49    /// Consumes this `CastIntoError` and returns the original object, allowing continued
50    /// use of it after a failed conversion.
51    ///
52    /// See [`cast_into`][Bound::cast_into] for an example.
53    pub fn into_inner(self) -> Bound<'py, PyAny> {
54        self.from
55    }
56}
57
58struct CastErrorArguments {
59    from: Py<PyType>,
60    to: Py<PyAny>,
61}
62
63impl PyErrArguments for CastErrorArguments {
64    fn arguments(self, py: Python<'_>) -> Py<PyAny> {
65        format!(
66            "{}",
67            DisplayDowncastError {
68                from: &self.from.into_bound(py),
69                to: &self.to.into_bound(py),
70            }
71        )
72        .into_py_any(py)
73        .expect("failed to create Python string")
74    }
75}
76
77/// Convert `CastError` to Python `TypeError`.
78impl std::convert::From<CastError<'_, '_>> for PyErr {
79    fn from(err: CastError<'_, '_>) -> PyErr {
80        let args = CastErrorArguments {
81            from: err.from.get_type().unbind(),
82            to: err.to.unbind(),
83        };
84
85        exceptions::PyTypeError::new_err(args)
86    }
87}
88
89impl std::error::Error for CastError<'_, '_> {}
90
91impl std::fmt::Display for CastError<'_, '_> {
92    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> Result<(), std::fmt::Error> {
93        DisplayDowncastError {
94            from: &self.from.get_type(),
95            to: &self.to,
96        }
97        .fmt(f)
98    }
99}
100
101/// Convert `CastIntoError` to Python `TypeError`.
102impl std::convert::From<CastIntoError<'_>> for PyErr {
103    fn from(err: CastIntoError<'_>) -> PyErr {
104        let args = CastErrorArguments {
105            from: err.from.get_type().unbind(),
106            to: err.to.unbind(),
107        };
108
109        exceptions::PyTypeError::new_err(args)
110    }
111}
112
113impl std::error::Error for CastIntoError<'_> {}
114
115impl std::fmt::Display for CastIntoError<'_> {
116    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> Result<(), std::fmt::Error> {
117        DisplayDowncastError {
118            from: &self.from.get_type(),
119            to: &self.to,
120        }
121        .fmt(f)
122    }
123}
124
125struct DisplayDowncastError<'a, 'py> {
126    from: &'a Bound<'py, PyType>,
127    to: &'a Bound<'py, PyAny>,
128}
129
130impl std::fmt::Display for DisplayDowncastError<'_, '_> {
131    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
132        let from = self.from.qualname();
133        let from = from
134            .as_ref()
135            .map(|name| name.to_string_lossy())
136            .unwrap_or(Cow::Borrowed("<failed to extract type name>"));
137        let to = DisplayClassInfo(self.to);
138        write!(f, "'{from}' object cannot be cast as '{to}'")
139    }
140}
141
142struct DisplayClassInfo<'a, 'py>(&'a Bound<'py, PyAny>);
143
144impl std::fmt::Display for DisplayClassInfo<'_, '_> {
145    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
146        if let Ok(t) = self.0.cast::<PyType>() {
147            t.qualname()
148                .map_err(|_| std::fmt::Error)?
149                .to_string_lossy()
150                .fmt(f)
151        } else if let Ok(t) = self.0.cast::<PyTuple>() {
152            for (i, t) in t.iter().enumerate() {
153                if i > 0 {
154                    f.write_str(" | ")?;
155                }
156                write!(f, "{}", DisplayClassInfo(&t))?;
157            }
158            Ok(())
159        } else {
160            self.0.fmt(f)
161        }
162    }
163}
164
165#[cfg(test)]
166mod tests {
167    use crate::PyTypeInfo;
168
169    use super::*;
170
171    #[test]
172    fn test_display_cast_error() {
173        Python::attach(|py| {
174            let obj = py.None().into_bound(py);
175            let to_type = py.get_type::<crate::types::PyInt>().into_any();
176            let err = CastError::new(obj.as_borrowed(), to_type);
177            assert_eq!(err.to_string(), "'NoneType' object cannot be cast as 'int'");
178        })
179    }
180
181    #[test]
182    fn test_display_cast_error_with_tuple() {
183        Python::attach(|py| {
184            let obj = py.None().into_bound(py);
185            let to_type = PyTuple::new(
186                py,
187                &[
188                    py.get_type::<crate::types::PyInt>().into_any(),
189                    crate::types::PyNone::type_object(py).into_any(),
190                ],
191            )
192            .unwrap()
193            .into_any();
194            let err = CastError::new(obj.as_borrowed(), to_type);
195            assert_eq!(
196                err.to_string(),
197                "'NoneType' object cannot be cast as 'int | NoneType'"
198            );
199        })
200    }
201
202    #[test]
203    fn test_display_cast_into_error() {
204        Python::attach(|py| {
205            let obj = py.None().into_bound(py);
206            let to_type = py.get_type::<crate::types::PyInt>().into_any();
207            let err = CastIntoError::new(obj, to_type);
208            assert_eq!(err.to_string(), "'NoneType' object cannot be cast as 'int'");
209        })
210    }
211
212    #[test]
213    fn test_pyerr_from_cast_error() {
214        Python::attach(|py| {
215            let obj = py.None().into_bound(py);
216            let to_type = py.get_type::<crate::types::PyInt>().into_any();
217            let err = CastError::new(obj.as_borrowed(), to_type);
218            let py_err: PyErr = err.into();
219            assert_eq!(
220                py_err.to_string(),
221                "TypeError: 'NoneType' object cannot be cast as 'int'"
222            );
223        })
224    }
225
226    #[test]
227    fn test_pyerr_from_cast_into_error() {
228        Python::attach(|py| {
229            let obj = py.None().into_bound(py);
230            let to_type = py.get_type::<crate::types::PyInt>().into_any();
231            let err = CastIntoError::new(obj, to_type);
232            let py_err: PyErr = err.into();
233            assert_eq!(
234                py_err.to_string(),
235                "TypeError: 'NoneType' object cannot be cast as 'int'"
236            );
237        })
238    }
239}