Skip to main content

pyo3/err/
cast_error.rs

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