Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion guide/src/conversions/traits.md
Original file line number Diff line number Diff line change
Expand Up @@ -464,7 +464,7 @@ enum RustyEnum {
```

If the input is neither a string nor an integer, the error message will be:
`"'<INPUT_TYPE>' cannot be converted to 'str | int'"`.
`"'<INPUT_TYPE>' cannot be cast as 'str | int'"`.

#### `#[derive(FromPyObject)]` Container Attributes
- `pyo3(transparent)`
Expand Down
10 changes: 10 additions & 0 deletions guide/src/migration.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
</details>

## `.downcast()` and `DowncastError` replaced with `.cast()` and `CastError`

The `.downcast()` family of functions were only available on `Bound<PyAny>`. 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.
Expand Down
1 change: 1 addition & 0 deletions newsfragments/5468.added.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Add `CastError` and `CastIntoError`.
1 change: 1 addition & 0 deletions newsfragments/5468.changed.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Replace `DowncastError` and `DowncastIntoError` with `CastError` and `CastIntoError`.
6 changes: 3 additions & 3 deletions pyo3-macros-backend/src/method.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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 }, })
Expand All @@ -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 }, })
Expand Down Expand Up @@ -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<Self> (unknown_lints can be removed when MSRV is 1.75+)
Expand Down
4 changes: 2 additions & 2 deletions src/conversions/bytes.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<Self, Self::Error> {
Ok(Bytes::from_owner(obj.extract::<PyBackedBytes>()?))
Expand Down
14 changes: 7 additions & 7 deletions src/conversions/chrono.rs
Original file line number Diff line number Diff line change
Expand Up @@ -692,37 +692,37 @@ mod tests {
let none = py.None().into_bound(py);
assert_eq!(
none.extract::<Duration>().unwrap_err().to_string(),
"TypeError: 'NoneType' object cannot be converted to 'timedelta'"
"TypeError: 'NoneType' object cannot be cast as 'timedelta'"
);
assert_eq!(
none.extract::<FixedOffset>().unwrap_err().to_string(),
"TypeError: 'NoneType' object cannot be converted to 'tzinfo'"
"TypeError: 'NoneType' object cannot be cast as 'tzinfo'"
);
assert_eq!(
none.extract::<Utc>().unwrap_err().to_string(),
"ValueError: expected datetime.timezone.utc"
);
assert_eq!(
none.extract::<NaiveTime>().unwrap_err().to_string(),
"TypeError: 'NoneType' object cannot be converted to 'time'"
"TypeError: 'NoneType' object cannot be cast as 'time'"
);
assert_eq!(
none.extract::<NaiveDate>().unwrap_err().to_string(),
"TypeError: 'NoneType' object cannot be converted to 'date'"
"TypeError: 'NoneType' object cannot be cast as 'date'"
);
assert_eq!(
none.extract::<NaiveDateTime>().unwrap_err().to_string(),
"TypeError: 'NoneType' object cannot be converted to 'datetime'"
"TypeError: 'NoneType' object cannot be cast as 'datetime'"
);
assert_eq!(
none.extract::<DateTime<Utc>>().unwrap_err().to_string(),
"TypeError: 'NoneType' object cannot be converted to 'datetime'"
"TypeError: 'NoneType' object cannot be cast as 'datetime'"
);
assert_eq!(
none.extract::<DateTime<FixedOffset>>()
.unwrap_err()
.to_string(),
"TypeError: 'NoneType' object cannot be converted to 'datetime'"
"TypeError: 'NoneType' object cannot be cast as 'datetime'"
);
});
}
Expand Down
14 changes: 7 additions & 7 deletions src/conversions/jiff.rs
Original file line number Diff line number Diff line change
Expand Up @@ -581,31 +581,31 @@ mod tests {
let none = py.None().into_bound(py);
assert_eq!(
none.extract::<Span>().unwrap_err().to_string(),
"TypeError: 'NoneType' object cannot be converted to 'timedelta'"
"TypeError: 'NoneType' object cannot be cast as 'timedelta'"
);
assert_eq!(
none.extract::<Offset>().unwrap_err().to_string(),
"TypeError: 'NoneType' object cannot be converted to 'tzinfo'"
"TypeError: 'NoneType' object cannot be cast as 'tzinfo'"
);
assert_eq!(
none.extract::<TimeZone>().unwrap_err().to_string(),
"TypeError: 'NoneType' object cannot be converted to 'tzinfo'"
"TypeError: 'NoneType' object cannot be cast as 'tzinfo'"
);
assert_eq!(
none.extract::<Time>().unwrap_err().to_string(),
"TypeError: 'NoneType' object cannot be converted to 'time'"
"TypeError: 'NoneType' object cannot be cast as 'time'"
);
assert_eq!(
none.extract::<Date>().unwrap_err().to_string(),
"TypeError: 'NoneType' object cannot be converted to 'date'"
"TypeError: 'NoneType' object cannot be cast as 'date'"
);
assert_eq!(
none.extract::<DateTime>().unwrap_err().to_string(),
"TypeError: 'NoneType' object cannot be converted to 'datetime'"
"TypeError: 'NoneType' object cannot be cast as 'datetime'"
);
assert_eq!(
none.extract::<Zoned>().unwrap_err().to_string(),
"TypeError: 'NoneType' object cannot be converted to 'datetime'"
"TypeError: 'NoneType' object cannot be cast as 'datetime'"
);
});
}
Expand Down
11 changes: 3 additions & 8 deletions src/conversions/smallvec.rs
Original file line number Diff line number Diff line change
Expand Up @@ -22,8 +22,7 @@ use crate::inspect::types::TypeInfo;
use crate::types::any::PyAnyMethods;
use crate::types::{PySequence, PyString};
use crate::{
err::DowncastError, ffi, Borrowed, Bound, FromPyObject, PyAny, PyErr, PyResult, PyTypeInfo,
Python,
err::CastError, ffi, Borrowed, Bound, FromPyObject, PyAny, PyErr, PyResult, PyTypeInfo, Python,
};
use smallvec::{Array, SmallVec};

Expand Down Expand Up @@ -103,11 +102,7 @@ where
if ffi::PySequence_Check(obj.as_ptr()) != 0 {
obj.cast_unchecked::<PySequence>()
} else {
return Err(DowncastError::new_from_type(
obj,
PySequence::type_object(obj.py()).into_any(),
)
.into());
return Err(CastError::new(obj, PySequence::type_object(obj.py()).into_any()).into());
}
};

Expand Down Expand Up @@ -139,7 +134,7 @@ mod tests {
let sv: PyResult<SmallVec<[u64; 8]>> = dict.extract();
assert_eq!(
sv.unwrap_err().to_string(),
"TypeError: 'dict' object cannot be converted to 'Sequence'"
"TypeError: 'dict' object cannot be cast as 'Sequence'"
);
});
}
Expand Down
8 changes: 2 additions & 6 deletions src/conversions/std/array.rs
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
use crate::conversion::{FromPyObjectOwned, IntoPyObject};
use crate::types::any::PyAnyMethods;
use crate::types::PySequence;
use crate::{err::DowncastError, ffi, FromPyObject, PyAny, PyResult, PyTypeInfo, Python};
use crate::{err::CastError, ffi, FromPyObject, PyAny, PyResult, PyTypeInfo, Python};
use crate::{exceptions, Borrowed, Bound, PyErr};

impl<'py, T, const N: usize> IntoPyObject<'py> for [T; N]
Expand Down Expand Up @@ -75,11 +75,7 @@ where
if ffi::PySequence_Check(obj.as_ptr()) != 0 {
obj.cast_unchecked::<PySequence>()
} else {
return Err(DowncastError::new_from_type(
obj,
PySequence::type_object(obj.py()).into_any(),
)
.into());
return Err(CastError::new(obj, PySequence::type_object(obj.py()).into_any()).into());
}
};
let seq_len = seq.len()?;
Expand Down
6 changes: 3 additions & 3 deletions src/conversions/std/slice.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ use crate::inspect::types::TypeInfo;
use crate::{
conversion::IntoPyObject,
types::{PyByteArray, PyByteArrayMethods, PyBytes},
Bound, DowncastError, PyAny, PyErr, Python,
Bound, CastError, PyAny, PyErr, Python,
};

impl<'a, 'py, T> IntoPyObject<'py> for &'a [T]
Expand Down Expand Up @@ -36,7 +36,7 @@ where
}

impl<'a, 'py> crate::conversion::FromPyObject<'a, 'py> for &'a [u8] {
type Error = DowncastError<'a, 'py>;
type Error = CastError<'a, 'py>;

fn extract(obj: crate::Borrowed<'a, 'py, PyAny>) -> Result<Self, Self::Error> {
Ok(obj.cast::<PyBytes>()?.as_bytes())
Expand All @@ -54,7 +54,7 @@ impl<'a, 'py> crate::conversion::FromPyObject<'a, 'py> for &'a [u8] {
/// pointing into the source object, and no copying or heap allocations will happen.
/// If it is a `bytearray`, its contents will be copied to an owned `Cow`.
impl<'a, 'py> crate::conversion::FromPyObject<'a, 'py> for Cow<'a, [u8]> {
type Error = DowncastError<'a, 'py>;
type Error = CastError<'a, 'py>;

fn extract(ob: crate::Borrowed<'a, 'py, PyAny>) -> Result<Self, Self::Error> {
if let Ok(bytes) = ob.cast::<PyBytes>() {
Expand Down
8 changes: 2 additions & 6 deletions src/conversions/std/vec.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ use crate::{
exceptions::PyTypeError,
ffi,
types::{PyAnyMethods, PySequence, PyString},
Borrowed, DowncastError, PyResult, PyTypeInfo,
Borrowed, CastError, PyResult, PyTypeInfo,
};
use crate::{Bound, PyAny, PyErr, Python};

Expand Down Expand Up @@ -91,11 +91,7 @@ where
if ffi::PySequence_Check(obj.as_ptr()) != 0 {
obj.cast_unchecked::<PySequence>()
} else {
return Err(DowncastError::new_from_type(
obj,
PySequence::type_object(obj.py()).into_any(),
)
.into());
return Err(CastError::new(obj, PySequence::type_object(obj.py()).into_any()).into());
}
};

Expand Down
Loading
Loading