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
1 change: 1 addition & 0 deletions newsfragments/5387.added.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Add `PyTypeCheck::classinfo_object` that returns an object that can be used as parameter in `isinstance` or `issubclass`
2 changes: 2 additions & 0 deletions newsfragments/5387.changed.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
- Fetch type name dynamically on cast errors instead of using `PyTypeCheck::NAME`
- Deprecate `PyTypeCheck::NAME`, please `TypeTypeCheck::classinfo_object` to get the expected type and format it at runtime.
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 'PyDelta'"
"TypeError: 'NoneType' object cannot be converted to 'timedelta'"
);
assert_eq!(
none.extract::<FixedOffset>().unwrap_err().to_string(),
"TypeError: 'NoneType' object cannot be converted to 'PyTzInfo'"
"TypeError: 'NoneType' object cannot be converted to '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 'PyTime'"
"TypeError: 'NoneType' object cannot be converted to 'time'"
);
assert_eq!(
none.extract::<NaiveDate>().unwrap_err().to_string(),
"TypeError: 'NoneType' object cannot be converted to 'PyDate'"
"TypeError: 'NoneType' object cannot be converted to 'date'"
);
assert_eq!(
none.extract::<NaiveDateTime>().unwrap_err().to_string(),
"TypeError: 'NoneType' object cannot be converted to 'PyDateTime'"
"TypeError: 'NoneType' object cannot be converted to 'datetime'"
);
assert_eq!(
none.extract::<DateTime<Utc>>().unwrap_err().to_string(),
"TypeError: 'NoneType' object cannot be converted to 'PyDateTime'"
"TypeError: 'NoneType' object cannot be converted to 'datetime'"
);
assert_eq!(
none.extract::<DateTime<FixedOffset>>()
.unwrap_err()
.to_string(),
"TypeError: 'NoneType' object cannot be converted to 'PyDateTime'"
"TypeError: 'NoneType' object cannot be converted to '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 @@ -553,31 +553,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 'PyDelta'"
"TypeError: 'NoneType' object cannot be converted to 'timedelta'"
);
assert_eq!(
none.extract::<Offset>().unwrap_err().to_string(),
"TypeError: 'NoneType' object cannot be converted to 'PyTzInfo'"
"TypeError: 'NoneType' object cannot be converted to 'tzinfo'"
);
assert_eq!(
none.extract::<TimeZone>().unwrap_err().to_string(),
"TypeError: 'NoneType' object cannot be converted to 'PyTzInfo'"
"TypeError: 'NoneType' object cannot be converted to 'tzinfo'"
);
assert_eq!(
none.extract::<Time>().unwrap_err().to_string(),
"TypeError: 'NoneType' object cannot be converted to 'PyTime'"
"TypeError: 'NoneType' object cannot be converted to 'time'"
);
assert_eq!(
none.extract::<Date>().unwrap_err().to_string(),
"TypeError: 'NoneType' object cannot be converted to 'PyDate'"
"TypeError: 'NoneType' object cannot be converted to 'date'"
);
assert_eq!(
none.extract::<DateTime>().unwrap_err().to_string(),
"TypeError: 'NoneType' object cannot be converted to 'PyDateTime'"
"TypeError: 'NoneType' object cannot be converted to 'datetime'"
);
assert_eq!(
none.extract::<Zoned>().unwrap_err().to_string(),
"TypeError: 'NoneType' object cannot be converted to 'PyDateTime'"
"TypeError: 'NoneType' object cannot be converted to 'datetime'"
);
});
}
Expand Down
9 changes: 7 additions & 2 deletions src/conversions/smallvec.rs
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,8 @@ 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, Python,
err::DowncastError, ffi, Borrowed, Bound, FromPyObject, PyAny, PyErr, PyResult, PyTypeInfo,
Python,
};
use smallvec::{Array, SmallVec};

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

Expand Down
8 changes: 6 additions & 2 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, Python};
use crate::{err::DowncastError, 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,7 +75,11 @@ where
if ffi::PySequence_Check(obj.as_ptr()) != 0 {
obj.cast_unchecked::<PySequence>()
} else {
return Err(DowncastError::new_from_borrowed(obj, "Sequence").into());
return Err(DowncastError::new_from_type(
obj,
PySequence::type_object(obj.py()).into_any(),
)
.into());
}
};
let seq_len = seq.len()?;
Expand Down
8 changes: 6 additions & 2 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,
Borrowed, DowncastError, PyResult, PyTypeInfo,
};
use crate::{Bound, PyAny, PyErr, Python};

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

Expand Down
95 changes: 73 additions & 22 deletions src/err/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ use crate::type_object::PyTypeInfo;
use crate::types::any::PyAnyMethods;
use crate::types::{
string::PyStringMethods, traceback::PyTracebackMethods, typeobject::PyTypeMethods, PyTraceback,
PyType,
PyTuple, PyTupleMethods, PyType,
};
use crate::{
exceptions::{self, PyBaseException},
Expand Down Expand Up @@ -46,25 +46,23 @@ pub type PyResult<T> = Result<T, PyErr>;
#[derive(Debug)]
pub struct DowncastError<'a, 'py> {
from: Borrowed<'a, 'py, PyAny>,
to: Cow<'static, str>,
to: TypeNameOrValue<'py>,
}

impl<'a, 'py> DowncastError<'a, 'py> {
/// Create a new `PyDowncastError` representing a failure to convert the object
/// `from` into the type named in `to`.
pub fn new(from: &'a Bound<'py, PyAny>, to: impl Into<Cow<'static, str>>) -> Self {
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

DowncastError::new and DowncastIntoError::new are unused now. Do we want to replace the second parameter with a Bound<'py, PyAny>?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think we should leave this for the moment, as public API. We might want to re-name / re-introduce this as CastError, and if so, we could "fix" the API and deprecate this one at the same time.

DowncastError {
Self {
from: from.as_borrowed(),
to: to.into(),
to: TypeNameOrValue::Name(to.into()),
}
}
pub(crate) fn new_from_borrowed(
from: Borrowed<'a, 'py, PyAny>,
to: impl Into<Cow<'static, str>>,
) -> Self {
DowncastError {

pub(crate) fn new_from_type(from: Borrowed<'a, 'py, PyAny>, to: Bound<'py, PyAny>) -> Self {
Self {
from,
to: to.into(),
to: TypeNameOrValue::Value(to),
}
}
}
Expand All @@ -73,16 +71,23 @@ impl<'a, 'py> DowncastError<'a, 'py> {
#[derive(Debug)]
pub struct DowncastIntoError<'py> {
from: Bound<'py, PyAny>,
to: Cow<'static, str>,
to: TypeNameOrValue<'py>,
}

impl<'py> DowncastIntoError<'py> {
/// Create a new `DowncastIntoError` representing a failure to convert the object
/// `from` into the type named in `to`.
pub fn new(from: Bound<'py, PyAny>, to: impl Into<Cow<'static, str>>) -> Self {
DowncastIntoError {
Self {
from,
to: TypeNameOrValue::Name(to.into()),
}
}

pub(crate) fn new_from_type(from: Bound<'py, PyAny>, to: Bound<'py, PyAny>) -> Self {
Self {
from,
to: to.into(),
to: TypeNameOrValue::Value(to),
}
}

Expand All @@ -95,6 +100,12 @@ impl<'py> DowncastIntoError<'py> {
}
}

// Helper to store either a concrete type or a type name
#[derive(Debug)]
enum TypeNameOrValue<'py> {
Name(Cow<'static, str>),
Value(Bound<'py, PyAny>),
}
/// Helper conversion trait that allows to use custom arguments for lazy exception construction.
pub trait PyErrArguments: Send + Sync {
/// Arguments for exception
Expand Down Expand Up @@ -721,25 +732,33 @@ impl<'py> IntoPyObject<'py> for &PyErr {

struct PyDowncastErrorArguments {
from: Py<PyType>,
to: Cow<'static, str>,
to: OwnedTypeNameOrValue,
}

impl PyErrArguments for PyDowncastErrorArguments {
fn arguments(self, py: Python<'_>) -> Py<PyAny> {
const FAILED_TO_EXTRACT: Cow<'_, str> = Cow::Borrowed("<failed to extract type name>");
let from = self.from.bind(py).qualname();
let from = match &from {
Ok(qn) => qn.to_cow().unwrap_or(FAILED_TO_EXTRACT),
Err(_) => FAILED_TO_EXTRACT,
let from = from
.as_ref()
.map(|name| name.to_string_lossy())
.unwrap_or(Cow::Borrowed("<failed to extract type name>"));
let to = match self.to {
OwnedTypeNameOrValue::Name(name) => TypeNameOrValue::Name(name),
OwnedTypeNameOrValue::Value(t) => TypeNameOrValue::Value(t.into_bound(py)),
};
format!("'{}' object cannot be converted to '{}'", from, self.to)
format!("'{}' object cannot be converted to '{}'", from, to)
.into_pyobject(py)
.unwrap()
.into_any()
.unbind()
}
}

enum OwnedTypeNameOrValue {
Name(Cow<'static, str>),
Value(Py<PyAny>),
}

/// Python exceptions that can be converted to [`PyErr`].
///
/// This is used to implement [`From<Bound<'_, T>> for PyErr`].
Expand All @@ -763,7 +782,10 @@ impl std::convert::From<DowncastError<'_, '_>> for PyErr {
fn from(err: DowncastError<'_, '_>) -> PyErr {
let args = PyDowncastErrorArguments {
from: err.from.get_type().into(),
to: err.to,
to: match err.to {
TypeNameOrValue::Name(name) => OwnedTypeNameOrValue::Name(name),
TypeNameOrValue::Value(t) => OwnedTypeNameOrValue::Value(t.into()),
},
};

exceptions::PyTypeError::new_err(args)
Expand All @@ -783,7 +805,10 @@ impl std::convert::From<DowncastIntoError<'_>> for PyErr {
fn from(err: DowncastIntoError<'_>) -> PyErr {
let args = PyDowncastErrorArguments {
from: err.from.get_type().into(),
to: err.to,
to: match err.to {
TypeNameOrValue::Name(name) => OwnedTypeNameOrValue::Name(name),
TypeNameOrValue::Value(t) => OwnedTypeNameOrValue::Value(t.into()),
},
};

exceptions::PyTypeError::new_err(args)
Expand All @@ -801,7 +826,7 @@ impl std::fmt::Display for DowncastIntoError<'_> {
fn display_downcast_error(
f: &mut std::fmt::Formatter<'_>,
from: &Bound<'_, PyAny>,
to: &str,
to: &TypeNameOrValue<'_>,
) -> std::fmt::Result {
write!(
f,
Expand All @@ -811,6 +836,32 @@ fn display_downcast_error(
)
}

impl std::fmt::Display for TypeNameOrValue<'_> {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::Name(name) => name.fmt(f),
Self::Value(t) => {
if let Ok(t) = t.downcast::<PyType>() {
t.qualname()
.map_err(|_| std::fmt::Error)?
.to_string_lossy()
.fmt(f)
} else if let Ok(t) = t.downcast::<PyTuple>() {
for (i, t) in t.iter().enumerate() {
if i > 0 {
f.write_str(" | ")?;
}
TypeNameOrValue::Value(t).fmt(f)?;
}
Ok(())
} else {
t.fmt(f)
}
}
}
}
}

#[track_caller]
pub fn panic_after_error(_py: Python<'_>) -> ! {
unsafe {
Expand Down
Loading
Loading