diff --git a/guide/src/conversions/traits.md b/guide/src/conversions/traits.md index 98bd2864809..be9d3f7e6fe 100644 --- a/guide/src/conversions/traits.md +++ b/guide/src/conversions/traits.md @@ -464,7 +464,7 @@ enum RustyEnum { ``` If the input is neither a string nor an integer, the error message will be: -`"'' cannot be converted to 'str | int'"`. +`"'' cannot be cast as 'str | int'"`. #### `#[derive(FromPyObject)]` Container Attributes - `pyo3(transparent)` diff --git a/guide/src/migration.md b/guide/src/migration.md index c2c67039c47..099c06bbe29 100644 --- a/guide/src/migration.md +++ b/guide/src/migration.md @@ -133,6 +133,16 @@ This is very similar to `serde`s [`Deserialize`] and [`DeserializeOwned`] traits [`DeserializeOwned`]: https://docs.rs/serde/latest/serde/de/trait.DeserializeOwned.html +## `.downcast()` and `DowncastError` replaced with `.cast()` and `CastError` + +The `.downcast()` family of functions were only available on `Bound`. In corner cases (particularly related to `.downcast_into()`) this would require use of `.as_any().downcast()` or `.into_any().downcast_into()` chains. Additionally, `DowncastError` produced Python exception messages which are not very Pythonic due to use of Rust type names in the error messages. + +The `.cast()` family of functions are available on all `Bound` and `Borrowed` smart pointers, whatever the type, and have error messages derived from the actual type at runtime. This produces a nicer experience for both PyO3 module authors and consumers. + +To migrate, replace `.downcast()` with `.cast()` and `DowncastError` with `CastError` (and similar with `.downcast_into()` / `DowncastIntoError` etc). + +`CastError` requires a Python `type` object (or other "classinfo" object compatible with `isinstance()`) as the second object, so in the rare case where `DowncastError` was manually constructed, small adjustments to code may apply. + ## `PyTypeCheck` is now an `unsafe trait` Because `PyTypeCheck` is the trait used to guard the `.cast()` functions to treat Python objects as specific concrete types, the trait is `unsafe` to implement. diff --git a/newsfragments/5468.added.md b/newsfragments/5468.added.md new file mode 100644 index 00000000000..73bb6cba80f --- /dev/null +++ b/newsfragments/5468.added.md @@ -0,0 +1 @@ +Add `CastError` and `CastIntoError`. diff --git a/newsfragments/5468.changed.md b/newsfragments/5468.changed.md new file mode 100644 index 00000000000..5f7cca73682 --- /dev/null +++ b/newsfragments/5468.changed.md @@ -0,0 +1 @@ +Replace `DowncastError` and `DowncastIntoError` with `CastError` and `CastIntoError`. diff --git a/pyo3-macros-backend/src/method.rs b/pyo3-macros-backend/src/method.rs index 400538ce14d..8d6f7868524 100644 --- a/pyo3-macros-backend/src/method.rs +++ b/pyo3-macros-backend/src/method.rs @@ -316,7 +316,7 @@ impl FnType { #[allow(clippy::useless_conversion)] ::std::convert::Into::into( #pyo3_path::impl_::pymethods::BoundRef::ref_from_ptr(#py, &*(&#slf as *const _ as *const *mut _)) - .downcast_unchecked::<#pyo3_path::types::PyType>() + .cast_unchecked::<#pyo3_path::types::PyType>() ) }; Some(quote! { unsafe { #ret }, }) @@ -329,7 +329,7 @@ impl FnType { #[allow(clippy::useless_conversion)] ::std::convert::Into::into( #pyo3_path::impl_::pymethods::BoundRef::ref_from_ptr(#py, &*(&#slf as *const _ as *const *mut _)) - .downcast_unchecked::<#pyo3_path::types::PyModule>() + .cast_unchecked::<#pyo3_path::types::PyModule>() ) }; Some(quote! { unsafe { #ret }, }) @@ -404,7 +404,7 @@ impl SelfType { let pyo3_path = pyo3_path.to_tokens_spanned(*span); error_mode.handle_error( quote_spanned! { *span => - #bound_ref.downcast::<#cls>() + #bound_ref.cast::<#cls>() .map_err(::std::convert::Into::<#pyo3_path::PyErr>::into) .and_then( #[allow(unknown_lints, clippy::unnecessary_fallible_conversions)] // In case slf is Py (unknown_lints can be removed when MSRV is 1.75+) diff --git a/src/conversions/bytes.rs b/src/conversions/bytes.rs index 0aba7409578..fb656579a2d 100644 --- a/src/conversions/bytes.rs +++ b/src/conversions/bytes.rs @@ -68,10 +68,10 @@ use crate::conversion::IntoPyObject; use crate::instance::Bound; use crate::pybacked::PyBackedBytes; use crate::types::PyBytes; -use crate::{Borrowed, DowncastError, FromPyObject, PyAny, PyErr, Python}; +use crate::{Borrowed, CastError, FromPyObject, PyAny, PyErr, Python}; impl<'a, 'py> FromPyObject<'a, 'py> for Bytes { - type Error = DowncastError<'a, 'py>; + type Error = CastError<'a, 'py>; fn extract(obj: Borrowed<'a, 'py, PyAny>) -> Result { Ok(Bytes::from_owner(obj.extract::()?)) diff --git a/src/conversions/chrono.rs b/src/conversions/chrono.rs index 0a2a56c3530..e9ca64d0ba7 100644 --- a/src/conversions/chrono.rs +++ b/src/conversions/chrono.rs @@ -692,11 +692,11 @@ mod tests { let none = py.None().into_bound(py); assert_eq!( none.extract::().unwrap_err().to_string(), - "TypeError: 'NoneType' object cannot be converted to 'timedelta'" + "TypeError: 'NoneType' object cannot be cast as 'timedelta'" ); assert_eq!( none.extract::().unwrap_err().to_string(), - "TypeError: 'NoneType' object cannot be converted to 'tzinfo'" + "TypeError: 'NoneType' object cannot be cast as 'tzinfo'" ); assert_eq!( none.extract::().unwrap_err().to_string(), @@ -704,25 +704,25 @@ mod tests { ); assert_eq!( none.extract::().unwrap_err().to_string(), - "TypeError: 'NoneType' object cannot be converted to 'time'" + "TypeError: 'NoneType' object cannot be cast as 'time'" ); assert_eq!( none.extract::().unwrap_err().to_string(), - "TypeError: 'NoneType' object cannot be converted to 'date'" + "TypeError: 'NoneType' object cannot be cast as 'date'" ); assert_eq!( none.extract::().unwrap_err().to_string(), - "TypeError: 'NoneType' object cannot be converted to 'datetime'" + "TypeError: 'NoneType' object cannot be cast as 'datetime'" ); assert_eq!( none.extract::>().unwrap_err().to_string(), - "TypeError: 'NoneType' object cannot be converted to 'datetime'" + "TypeError: 'NoneType' object cannot be cast as 'datetime'" ); assert_eq!( none.extract::>() .unwrap_err() .to_string(), - "TypeError: 'NoneType' object cannot be converted to 'datetime'" + "TypeError: 'NoneType' object cannot be cast as 'datetime'" ); }); } diff --git a/src/conversions/jiff.rs b/src/conversions/jiff.rs index 817b50df79d..9cc9ffd3cee 100644 --- a/src/conversions/jiff.rs +++ b/src/conversions/jiff.rs @@ -581,31 +581,31 @@ mod tests { let none = py.None().into_bound(py); assert_eq!( none.extract::().unwrap_err().to_string(), - "TypeError: 'NoneType' object cannot be converted to 'timedelta'" + "TypeError: 'NoneType' object cannot be cast as 'timedelta'" ); assert_eq!( none.extract::().unwrap_err().to_string(), - "TypeError: 'NoneType' object cannot be converted to 'tzinfo'" + "TypeError: 'NoneType' object cannot be cast as 'tzinfo'" ); assert_eq!( none.extract::().unwrap_err().to_string(), - "TypeError: 'NoneType' object cannot be converted to 'tzinfo'" + "TypeError: 'NoneType' object cannot be cast as 'tzinfo'" ); assert_eq!( none.extract::