Skip to content

Commit 254b29f

Browse files
authored
replace DowncastError with CastError (#5468)
* replace `DowncastError` with `CastError` * newsfragment, CI * fixups * migration guide entry * fixup docs
1 parent d9e2c50 commit 254b29f

File tree

28 files changed

+545
-296
lines changed

28 files changed

+545
-296
lines changed

guide/src/conversions/traits.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -464,7 +464,7 @@ enum RustyEnum {
464464
```
465465

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

469469
#### `#[derive(FromPyObject)]` Container Attributes
470470
- `pyo3(transparent)`

guide/src/migration.md

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -133,6 +133,16 @@ This is very similar to `serde`s [`Deserialize`] and [`DeserializeOwned`] traits
133133
[`DeserializeOwned`]: https://docs.rs/serde/latest/serde/de/trait.DeserializeOwned.html
134134
</details>
135135

136+
## `.downcast()` and `DowncastError` replaced with `.cast()` and `CastError`
137+
138+
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.
139+
140+
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.
141+
142+
To migrate, replace `.downcast()` with `.cast()` and `DowncastError` with `CastError` (and similar with `.downcast_into()` / `DowncastIntoError` etc).
143+
144+
`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.
145+
136146
## `PyTypeCheck` is now an `unsafe trait`
137147

138148
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.

newsfragments/5468.added.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Add `CastError` and `CastIntoError`.

newsfragments/5468.changed.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Replace `DowncastError` and `DowncastIntoError` with `CastError` and `CastIntoError`.

pyo3-macros-backend/src/method.rs

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -316,7 +316,7 @@ impl FnType {
316316
#[allow(clippy::useless_conversion)]
317317
::std::convert::Into::into(
318318
#pyo3_path::impl_::pymethods::BoundRef::ref_from_ptr(#py, &*(&#slf as *const _ as *const *mut _))
319-
.downcast_unchecked::<#pyo3_path::types::PyType>()
319+
.cast_unchecked::<#pyo3_path::types::PyType>()
320320
)
321321
};
322322
Some(quote! { unsafe { #ret }, })
@@ -329,7 +329,7 @@ impl FnType {
329329
#[allow(clippy::useless_conversion)]
330330
::std::convert::Into::into(
331331
#pyo3_path::impl_::pymethods::BoundRef::ref_from_ptr(#py, &*(&#slf as *const _ as *const *mut _))
332-
.downcast_unchecked::<#pyo3_path::types::PyModule>()
332+
.cast_unchecked::<#pyo3_path::types::PyModule>()
333333
)
334334
};
335335
Some(quote! { unsafe { #ret }, })
@@ -404,7 +404,7 @@ impl SelfType {
404404
let pyo3_path = pyo3_path.to_tokens_spanned(*span);
405405
error_mode.handle_error(
406406
quote_spanned! { *span =>
407-
#bound_ref.downcast::<#cls>()
407+
#bound_ref.cast::<#cls>()
408408
.map_err(::std::convert::Into::<#pyo3_path::PyErr>::into)
409409
.and_then(
410410
#[allow(unknown_lints, clippy::unnecessary_fallible_conversions)] // In case slf is Py<Self> (unknown_lints can be removed when MSRV is 1.75+)

src/conversions/bytes.rs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -68,10 +68,10 @@ use crate::conversion::IntoPyObject;
6868
use crate::instance::Bound;
6969
use crate::pybacked::PyBackedBytes;
7070
use crate::types::PyBytes;
71-
use crate::{Borrowed, DowncastError, FromPyObject, PyAny, PyErr, Python};
71+
use crate::{Borrowed, CastError, FromPyObject, PyAny, PyErr, Python};
7272

7373
impl<'a, 'py> FromPyObject<'a, 'py> for Bytes {
74-
type Error = DowncastError<'a, 'py>;
74+
type Error = CastError<'a, 'py>;
7575

7676
fn extract(obj: Borrowed<'a, 'py, PyAny>) -> Result<Self, Self::Error> {
7777
Ok(Bytes::from_owner(obj.extract::<PyBackedBytes>()?))

src/conversions/chrono.rs

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -692,37 +692,37 @@ mod tests {
692692
let none = py.None().into_bound(py);
693693
assert_eq!(
694694
none.extract::<Duration>().unwrap_err().to_string(),
695-
"TypeError: 'NoneType' object cannot be converted to 'timedelta'"
695+
"TypeError: 'NoneType' object cannot be cast as 'timedelta'"
696696
);
697697
assert_eq!(
698698
none.extract::<FixedOffset>().unwrap_err().to_string(),
699-
"TypeError: 'NoneType' object cannot be converted to 'tzinfo'"
699+
"TypeError: 'NoneType' object cannot be cast as 'tzinfo'"
700700
);
701701
assert_eq!(
702702
none.extract::<Utc>().unwrap_err().to_string(),
703703
"ValueError: expected datetime.timezone.utc"
704704
);
705705
assert_eq!(
706706
none.extract::<NaiveTime>().unwrap_err().to_string(),
707-
"TypeError: 'NoneType' object cannot be converted to 'time'"
707+
"TypeError: 'NoneType' object cannot be cast as 'time'"
708708
);
709709
assert_eq!(
710710
none.extract::<NaiveDate>().unwrap_err().to_string(),
711-
"TypeError: 'NoneType' object cannot be converted to 'date'"
711+
"TypeError: 'NoneType' object cannot be cast as 'date'"
712712
);
713713
assert_eq!(
714714
none.extract::<NaiveDateTime>().unwrap_err().to_string(),
715-
"TypeError: 'NoneType' object cannot be converted to 'datetime'"
715+
"TypeError: 'NoneType' object cannot be cast as 'datetime'"
716716
);
717717
assert_eq!(
718718
none.extract::<DateTime<Utc>>().unwrap_err().to_string(),
719-
"TypeError: 'NoneType' object cannot be converted to 'datetime'"
719+
"TypeError: 'NoneType' object cannot be cast as 'datetime'"
720720
);
721721
assert_eq!(
722722
none.extract::<DateTime<FixedOffset>>()
723723
.unwrap_err()
724724
.to_string(),
725-
"TypeError: 'NoneType' object cannot be converted to 'datetime'"
725+
"TypeError: 'NoneType' object cannot be cast as 'datetime'"
726726
);
727727
});
728728
}

src/conversions/jiff.rs

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -581,31 +581,31 @@ mod tests {
581581
let none = py.None().into_bound(py);
582582
assert_eq!(
583583
none.extract::<Span>().unwrap_err().to_string(),
584-
"TypeError: 'NoneType' object cannot be converted to 'timedelta'"
584+
"TypeError: 'NoneType' object cannot be cast as 'timedelta'"
585585
);
586586
assert_eq!(
587587
none.extract::<Offset>().unwrap_err().to_string(),
588-
"TypeError: 'NoneType' object cannot be converted to 'tzinfo'"
588+
"TypeError: 'NoneType' object cannot be cast as 'tzinfo'"
589589
);
590590
assert_eq!(
591591
none.extract::<TimeZone>().unwrap_err().to_string(),
592-
"TypeError: 'NoneType' object cannot be converted to 'tzinfo'"
592+
"TypeError: 'NoneType' object cannot be cast as 'tzinfo'"
593593
);
594594
assert_eq!(
595595
none.extract::<Time>().unwrap_err().to_string(),
596-
"TypeError: 'NoneType' object cannot be converted to 'time'"
596+
"TypeError: 'NoneType' object cannot be cast as 'time'"
597597
);
598598
assert_eq!(
599599
none.extract::<Date>().unwrap_err().to_string(),
600-
"TypeError: 'NoneType' object cannot be converted to 'date'"
600+
"TypeError: 'NoneType' object cannot be cast as 'date'"
601601
);
602602
assert_eq!(
603603
none.extract::<DateTime>().unwrap_err().to_string(),
604-
"TypeError: 'NoneType' object cannot be converted to 'datetime'"
604+
"TypeError: 'NoneType' object cannot be cast as 'datetime'"
605605
);
606606
assert_eq!(
607607
none.extract::<Zoned>().unwrap_err().to_string(),
608-
"TypeError: 'NoneType' object cannot be converted to 'datetime'"
608+
"TypeError: 'NoneType' object cannot be cast as 'datetime'"
609609
);
610610
});
611611
}

src/conversions/smallvec.rs

Lines changed: 3 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -22,8 +22,7 @@ use crate::inspect::types::TypeInfo;
2222
use crate::types::any::PyAnyMethods;
2323
use crate::types::{PySequence, PyString};
2424
use crate::{
25-
err::DowncastError, ffi, Borrowed, Bound, FromPyObject, PyAny, PyErr, PyResult, PyTypeInfo,
26-
Python,
25+
err::CastError, ffi, Borrowed, Bound, FromPyObject, PyAny, PyErr, PyResult, PyTypeInfo, Python,
2726
};
2827
use smallvec::{Array, SmallVec};
2928

@@ -103,11 +102,7 @@ where
103102
if ffi::PySequence_Check(obj.as_ptr()) != 0 {
104103
obj.cast_unchecked::<PySequence>()
105104
} else {
106-
return Err(DowncastError::new_from_type(
107-
obj,
108-
PySequence::type_object(obj.py()).into_any(),
109-
)
110-
.into());
105+
return Err(CastError::new(obj, PySequence::type_object(obj.py()).into_any()).into());
111106
}
112107
};
113108

@@ -139,7 +134,7 @@ mod tests {
139134
let sv: PyResult<SmallVec<[u64; 8]>> = dict.extract();
140135
assert_eq!(
141136
sv.unwrap_err().to_string(),
142-
"TypeError: 'dict' object cannot be converted to 'Sequence'"
137+
"TypeError: 'dict' object cannot be cast as 'Sequence'"
143138
);
144139
});
145140
}

src/conversions/std/array.rs

Lines changed: 2 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
use crate::conversion::{FromPyObjectOwned, IntoPyObject};
22
use crate::types::any::PyAnyMethods;
33
use crate::types::PySequence;
4-
use crate::{err::DowncastError, ffi, FromPyObject, PyAny, PyResult, PyTypeInfo, Python};
4+
use crate::{err::CastError, ffi, FromPyObject, PyAny, PyResult, PyTypeInfo, Python};
55
use crate::{exceptions, Borrowed, Bound, PyErr};
66

77
impl<'py, T, const N: usize> IntoPyObject<'py> for [T; N]
@@ -75,11 +75,7 @@ where
7575
if ffi::PySequence_Check(obj.as_ptr()) != 0 {
7676
obj.cast_unchecked::<PySequence>()
7777
} else {
78-
return Err(DowncastError::new_from_type(
79-
obj,
80-
PySequence::type_object(obj.py()).into_any(),
81-
)
82-
.into());
78+
return Err(CastError::new(obj, PySequence::type_object(obj.py()).into_any()).into());
8379
}
8480
};
8581
let seq_len = seq.len()?;

0 commit comments

Comments
 (0)