Pyerr value bound (#3820)

* Implement PyErr::value_bound

* Use PyErr::value_bound in conversions

* Implement PyErr::from_value_bound

* Remove unnecessary clone

* Return a reference from PyErr::value_bound

* Avoid clone in PyErr::from_value_bound

* Use PyErr::from_value_bound instead of from_value

* Remove unnecessary .as_borrowed() calls

* Remove unused import

* Simplify UnraisableCapture.hook

* Use Bound::from_owned_ptr_or_opt in fn cause

* Update PyErrState::lazy to take Py<PyAny>

This is easier to work with elsewhere than `&PyAny` or
`Bound<'py, PyAny>`.

* Add Bound PyUnicodeDecodeError constructors

* Update PyErr::from_value_bound to take Bound

* Simplify PyErr::from_value

* Simplify PyErr::value

* Remove unnecessary reference

* Simplify Pyerr::cause implementation

* Simplify PyUnicodeDecodeError::new_bound
This commit is contained in:
Lily Foote 2024-02-17 00:27:45 +00:00 committed by GitHub
parent c24478e8bc
commit 940804fe0d
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
14 changed files with 165 additions and 80 deletions

View File

@ -479,7 +479,7 @@ class House(object):
}
Err(e) => {
house
.call_method1("__exit__", (e.get_type_bound(py), e.value(py), e.traceback_bound(py)))
.call_method1("__exit__", (e.get_type_bound(py), e.value_bound(py), e.traceback_bound(py)))
.unwrap();
}
}

View File

@ -149,7 +149,7 @@ mod test_anyhow {
Python::with_gil(|py| {
let locals = [("err", pyerr)].into_py_dict_bound(py);
let pyerr = py.run_bound("raise err", None, Some(&locals)).unwrap_err();
assert_eq!(pyerr.value(py).to_string(), expected_contents);
assert_eq!(pyerr.value_bound(py).to_string(), expected_contents);
})
}
@ -166,7 +166,7 @@ mod test_anyhow {
Python::with_gil(|py| {
let locals = [("err", pyerr)].into_py_dict_bound(py);
let pyerr = py.run_bound("raise err", None, Some(&locals)).unwrap_err();
assert_eq!(pyerr.value(py).to_string(), expected_contents);
assert_eq!(pyerr.value_bound(py).to_string(), expected_contents);
})
}

View File

@ -590,7 +590,7 @@ mod tests {
assert!(result.is_err());
let res = result.err().unwrap();
// Also check the error message is what we expect
let msg = res.value(py).repr().unwrap().to_string();
let msg = res.value_bound(py).repr().unwrap().to_string();
assert_eq!(msg, "TypeError(\"zoneinfo.ZoneInfo(key='Europe/London') is not a fixed offset timezone\")");
});
}
@ -605,7 +605,7 @@ mod tests {
// Now test that converting a PyDateTime with tzinfo to a NaiveDateTime fails
let res: PyResult<NaiveDateTime> = py_datetime.extract();
assert_eq!(
res.unwrap_err().value(py).repr().unwrap().to_string(),
res.unwrap_err().value_bound(py).repr().unwrap().to_string(),
"TypeError('expected a datetime without tzinfo')"
);
});
@ -620,14 +620,14 @@ mod tests {
// Now test that converting a PyDateTime with tzinfo to a NaiveDateTime fails
let res: PyResult<DateTime<Utc>> = py_datetime.extract();
assert_eq!(
res.unwrap_err().value(py).repr().unwrap().to_string(),
res.unwrap_err().value_bound(py).repr().unwrap().to_string(),
"TypeError('expected a datetime with non-None tzinfo')"
);
// Now test that converting a PyDateTime with tzinfo to a NaiveDateTime fails
let res: PyResult<DateTime<FixedOffset>> = py_datetime.extract();
assert_eq!(
res.unwrap_err().value(py).repr().unwrap().to_string(),
res.unwrap_err().value_bound(py).repr().unwrap().to_string(),
"TypeError('expected a datetime with non-None tzinfo')"
);
});

View File

@ -154,7 +154,7 @@ mod tests {
Python::with_gil(|py| {
let locals = [("err", pyerr)].into_py_dict_bound(py);
let pyerr = py.run_bound("raise err", None, Some(&locals)).unwrap_err();
assert_eq!(pyerr.value(py).to_string(), expected_contents);
assert_eq!(pyerr.value_bound(py).to_string(), expected_contents);
})
}
@ -171,7 +171,7 @@ mod tests {
Python::with_gil(|py| {
let locals = [("err", pyerr)].into_py_dict_bound(py);
let pyerr = py.run_bound("raise err", None, Some(&locals)).unwrap_err();
assert_eq!(pyerr.value(py).to_string(), expected_contents);
assert_eq!(pyerr.value_bound(py).to_string(), expected_contents);
})
}

View File

@ -78,7 +78,7 @@ impl Coroutine {
(Some(exc), Some(cb)) => cb.throw(exc.as_ref(py)),
(Some(exc), None) => {
self.close();
return Err(PyErr::from_value(exc.as_ref(py)));
return Err(PyErr::from_value_bound(exc.into_bound(py)));
}
(None, _) => {}
}

View File

@ -101,19 +101,20 @@ where
}
impl PyErrState {
pub(crate) fn lazy(ptype: &PyAny, args: impl PyErrArguments + 'static) -> Self {
let ptype = ptype.into();
pub(crate) fn lazy(ptype: Py<PyAny>, args: impl PyErrArguments + 'static) -> Self {
PyErrState::Lazy(Box::new(move |py| PyErrStateLazyFnOutput {
ptype,
pvalue: args.arguments(py),
}))
}
pub(crate) fn normalized(pvalue: &PyBaseException) -> Self {
pub(crate) fn normalized(pvalue: Bound<'_, PyBaseException>) -> Self {
#[cfg(not(Py_3_12))]
use crate::types::any::PyAnyMethods;
Self::Normalized(PyErrStateNormalized {
#[cfg(not(Py_3_12))]
ptype: pvalue.get_type().into(),
pvalue: pvalue.into(),
#[cfg(not(Py_3_12))]
ptraceback: unsafe {
Py::from_owned_ptr_or_opt(
@ -121,6 +122,7 @@ impl PyErrState {
ffi::PyException_GetTraceback(pvalue.as_ptr()),
)
},
pvalue: pvalue.into(),
})
}

View File

@ -124,6 +124,8 @@ mod tests {
#[test]
fn io_errors() {
use crate::types::any::PyAnyMethods;
let check_err = |kind, expected_ty| {
Python::with_gil(|py| {
let rust_err = io::Error::new(kind, "some error msg");
@ -139,8 +141,8 @@ mod tests {
let py_err_recovered_from_rust_err: PyErr = rust_err_from_py_err.into();
assert!(py_err_recovered_from_rust_err
.value(py)
.is(py_error_clone.value(py))); // It should be the same exception
.value_bound(py)
.is(py_error_clone.value_bound(py))); // It should be the same exception
})
};

View File

@ -187,7 +187,19 @@ impl PyErr {
where
A: PyErrArguments + Send + Sync + 'static,
{
PyErr::from_state(PyErrState::lazy(ty, args))
PyErr::from_state(PyErrState::lazy(ty.into(), args))
}
/// Deprecated form of [`PyErr::from_value_bound`].
#[cfg_attr(
not(feature = "gil-refs"),
deprecated(
since = "0.21.0",
note = "`PyErr::from_value` will be replaced by `PyErr::from_value_bound` in a future PyO3 version"
)
)]
pub fn from_value(obj: &PyAny) -> PyErr {
PyErr::from_value_bound(obj.as_borrowed().to_owned())
}
/// Creates a new PyErr.
@ -201,33 +213,38 @@ impl PyErr {
/// # Examples
/// ```rust
/// use pyo3::prelude::*;
/// use pyo3::PyTypeInfo;
/// use pyo3::exceptions::PyTypeError;
/// use pyo3::types::{PyType, PyString};
/// use pyo3::types::PyString;
///
/// Python::with_gil(|py| {
/// // Case #1: Exception object
/// let err = PyErr::from_value(PyTypeError::new_err("some type error").value(py));
/// let err = PyErr::from_value_bound(PyTypeError::new_err("some type error")
/// .value_bound(py).clone().into_any());
/// assert_eq!(err.to_string(), "TypeError: some type error");
///
/// // Case #2: Exception type
/// let err = PyErr::from_value(PyType::new::<PyTypeError>(py));
/// let err = PyErr::from_value_bound(PyTypeError::type_object_bound(py).into_any());
/// assert_eq!(err.to_string(), "TypeError: ");
///
/// // Case #3: Invalid exception value
/// let err = PyErr::from_value(PyString::new_bound(py, "foo").as_gil_ref());
/// let err = PyErr::from_value_bound(PyString::new_bound(py, "foo").into_any());
/// assert_eq!(
/// err.to_string(),
/// "TypeError: exceptions must derive from BaseException"
/// );
/// });
/// ```
pub fn from_value(obj: &PyAny) -> PyErr {
let state = if let Ok(obj) = obj.downcast::<PyBaseException>() {
PyErrState::normalized(obj)
} else {
// Assume obj is Type[Exception]; let later normalization handle if this
// is not the case
PyErrState::lazy(obj, obj.py().None())
pub fn from_value_bound(obj: Bound<'_, PyAny>) -> PyErr {
let state = match obj.downcast_into::<PyBaseException>() {
Ok(obj) => PyErrState::normalized(obj),
Err(err) => {
// Assume obj is Type[Exception]; let later normalization handle if this
// is not the case
let obj = err.into_inner();
let py = obj.py();
PyErrState::lazy(obj.into_py(py), py.None())
}
};
PyErr::from_state(state)
@ -260,6 +277,18 @@ impl PyErr {
self.normalized(py).ptype(py)
}
/// Deprecated form of [`PyErr::value_bound`].
#[cfg_attr(
not(feature = "gil-refs"),
deprecated(
since = "0.21.0",
note = "`PyErr::value` will be replaced by `PyErr::value_bound` in a future PyO3 version"
)
)]
pub fn value<'py>(&'py self, py: Python<'py>) -> &'py PyBaseException {
self.value_bound(py).as_gil_ref()
}
/// Returns the value of this exception.
///
/// # Examples
@ -270,11 +299,11 @@ impl PyErr {
/// Python::with_gil(|py| {
/// let err: PyErr = PyTypeError::new_err(("some type error",));
/// assert!(err.is_instance_of::<PyTypeError>(py));
/// assert_eq!(err.value(py).to_string(), "some type error");
/// assert_eq!(err.value_bound(py).to_string(), "some type error");
/// });
/// ```
pub fn value<'py>(&'py self, py: Python<'py>) -> &'py PyBaseException {
self.normalized(py).pvalue.as_ref(py)
pub fn value_bound<'py>(&self, py: Python<'py>) -> &Bound<'py, PyBaseException> {
self.normalized(py).pvalue.bind(py)
}
/// Consumes self to take ownership of the exception value contained in this error.
@ -527,7 +556,7 @@ impl PyErr {
pub fn display(&self, py: Python<'_>) {
#[cfg(Py_3_12)]
unsafe {
ffi::PyErr_DisplayException(self.value(py).as_ptr())
ffi::PyErr_DisplayException(self.value_bound(py).as_ptr())
}
#[cfg(not(Py_3_12))]
@ -540,7 +569,7 @@ impl PyErr {
let type_bound = self.get_type_bound(py);
ffi::PyErr_Display(
type_bound.as_ptr(),
self.value(py).as_ptr(),
self.value_bound(py).as_ptr(),
traceback
.as_ref()
.map_or(std::ptr::null_mut(), |traceback| traceback.as_ptr()),
@ -785,7 +814,7 @@ impl PyErr {
/// let err: PyErr = PyTypeError::new_err(("some type error",));
/// let err_clone = err.clone_ref(py);
/// assert!(err.get_type_bound(py).is(&err_clone.get_type_bound(py)));
/// assert!(err.value(py).is(err_clone.value(py)));
/// assert!(err.value_bound(py).is(err_clone.value_bound(py)));
/// match err.traceback_bound(py) {
/// None => assert!(err_clone.traceback_bound(py).is_none()),
/// Some(tb) => assert!(err_clone.traceback_bound(py).unwrap().is(&tb)),
@ -800,15 +829,14 @@ impl PyErr {
/// Return the cause (either an exception instance, or None, set by `raise ... from ...`)
/// associated with the exception, as accessible from Python through `__cause__`.
pub fn cause(&self, py: Python<'_>) -> Option<PyErr> {
let value = self.value(py);
let obj =
unsafe { py.from_owned_ptr_or_opt::<PyAny>(ffi::PyException_GetCause(value.as_ptr())) };
obj.map(Self::from_value)
use crate::ffi_ptr_ext::FfiPtrExt;
unsafe { ffi::PyException_GetCause(self.value_bound(py).as_ptr()).assume_owned_or_opt(py) }
.map(Self::from_value_bound)
}
/// Set the cause associated with the exception, pass `None` to clear it.
pub fn set_cause(&self, py: Python<'_>, cause: Option<Self>) {
let value = self.value(py);
let value = self.value_bound(py);
let cause = cause.map(|err| err.into_value(py));
unsafe {
// PyException_SetCause _steals_ a reference to cause, so must use .into_ptr()
@ -868,7 +896,7 @@ impl std::fmt::Debug for PyErr {
Python::with_gil(|py| {
f.debug_struct("PyErr")
.field("type", &self.get_type_bound(py))
.field("value", self.value(py))
.field("value", self.value_bound(py))
.field("traceback", &self.traceback_bound(py))
.finish()
})
@ -877,8 +905,9 @@ impl std::fmt::Debug for PyErr {
impl std::fmt::Display for PyErr {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
use crate::types::string::PyStringMethods;
Python::with_gil(|py| {
let value = self.value(py);
let value = self.value_bound(py);
let type_name = value.get_type().qualname().map_err(|_| std::fmt::Error)?;
write!(f, "{}", type_name)?;
if let Ok(s) = value.str() {
@ -1043,7 +1072,6 @@ impl_signed_integer!(isize);
mod tests {
use super::PyErrState;
use crate::exceptions::{self, PyTypeError, PyValueError};
use crate::types::any::PyAnyMethods;
use crate::{PyErr, PyNativeType, PyTypeInfo, Python};
#[test]
@ -1237,6 +1265,7 @@ mod tests {
#[test]
fn warnings() {
use crate::types::any::PyAnyMethods;
// Note: although the warning filter is interpreter global, keeping the
// GIL locked should prevent effects to be visible to other testing
// threads.
@ -1284,7 +1313,7 @@ mod tests {
)
.unwrap_err();
assert!(err
.value(py)
.value_bound(py)
.getattr("args")
.unwrap()
.get_item(0)

View File

@ -9,7 +9,7 @@
//! yourself to import Python classes that are ultimately derived from
//! `BaseException`.
use crate::{ffi, PyResult, Python};
use crate::{ffi, Bound, PyResult, Python};
use std::ffi::CStr;
use std::ops;
use std::os::raw::c_char;
@ -22,6 +22,7 @@ macro_rules! impl_exception_boilerplate {
impl ::std::convert::From<&$name> for $crate::PyErr {
#[inline]
fn from(err: &$name) -> $crate::PyErr {
#[allow(deprecated)]
$crate::PyErr::from_value(err)
}
}
@ -613,7 +614,14 @@ impl_windows_native_exception!(
);
impl PyUnicodeDecodeError {
/// Creates a Python `UnicodeDecodeError`.
/// Deprecated form of [`PyUnicodeDecodeError::new_bound`].
#[cfg_attr(
not(feature = "gil-refs"),
deprecated(
since = "0.21.0",
note = "`PyUnicodeDecodeError::new` will be replaced by `PyUnicodeDecodeError::new_bound` in a future PyO3 version"
)
)]
pub fn new<'p>(
py: Python<'p>,
encoding: &CStr,
@ -621,16 +629,47 @@ impl PyUnicodeDecodeError {
range: ops::Range<usize>,
reason: &CStr,
) -> PyResult<&'p PyUnicodeDecodeError> {
Ok(PyUnicodeDecodeError::new_bound(py, encoding, input, range, reason)?.into_gil_ref())
}
/// Creates a Python `UnicodeDecodeError`.
pub fn new_bound<'p>(
py: Python<'p>,
encoding: &CStr,
input: &[u8],
range: ops::Range<usize>,
reason: &CStr,
) -> PyResult<Bound<'p, PyUnicodeDecodeError>> {
use crate::ffi_ptr_ext::FfiPtrExt;
use crate::py_result_ext::PyResultExt;
unsafe {
py.from_owned_ptr_or_err(ffi::PyUnicodeDecodeError_Create(
ffi::PyUnicodeDecodeError_Create(
encoding.as_ptr(),
input.as_ptr() as *const c_char,
input.len() as ffi::Py_ssize_t,
range.start as ffi::Py_ssize_t,
range.end as ffi::Py_ssize_t,
reason.as_ptr(),
))
)
.assume_owned_or_err(py)
}
.downcast_into()
}
/// Deprecated form of [`PyUnicodeDecodeError::new_utf8_bound`].
#[cfg_attr(
not(feature = "gil-refs"),
deprecated(
since = "0.21.0",
note = "`PyUnicodeDecodeError::new_utf8` will be replaced by `PyUnicodeDecodeError::new_utf8_bound` in a future PyO3 version"
)
)]
pub fn new_utf8<'p>(
py: Python<'p>,
input: &[u8],
err: std::str::Utf8Error,
) -> PyResult<&'p PyUnicodeDecodeError> {
Ok(PyUnicodeDecodeError::new_utf8_bound(py, input, err)?.into_gil_ref())
}
/// Creates a Python `UnicodeDecodeError` from a Rust UTF-8 decoding error.
@ -646,7 +685,7 @@ impl PyUnicodeDecodeError {
/// Python::with_gil(|py| {
/// let invalid_utf8 = b"fo\xd8o";
/// let err = std::str::from_utf8(invalid_utf8).expect_err("should be invalid utf8");
/// let decode_err = PyUnicodeDecodeError::new_utf8(py, invalid_utf8, err)?;
/// let decode_err = PyUnicodeDecodeError::new_utf8_bound(py, invalid_utf8, err)?;
/// assert_eq!(
/// decode_err.to_string(),
/// "'utf-8' codec can't decode byte 0xd8 in position 2: invalid utf-8"
@ -654,13 +693,13 @@ impl PyUnicodeDecodeError {
/// Ok(())
/// })
/// # }
pub fn new_utf8<'p>(
pub fn new_utf8_bound<'p>(
py: Python<'p>,
input: &[u8],
err: std::str::Utf8Error,
) -> PyResult<&'p PyUnicodeDecodeError> {
) -> PyResult<Bound<'p, PyUnicodeDecodeError>> {
let pos = err.valid_up_to();
PyUnicodeDecodeError::new(
PyUnicodeDecodeError::new_bound(
py,
CStr::from_bytes_with_nul(b"utf-8\0").unwrap(),
input,
@ -745,7 +784,7 @@ macro_rules! test_exception {
assert!(err.is_instance_of::<$exc_ty>(py));
let value: &$exc_ty = err.value(py).downcast().unwrap();
let value: &$exc_ty = err.value_bound(py).clone().into_gil_ref().downcast().unwrap();
assert!(value.source().is_none());
err.set_cause(py, Some($crate::exceptions::PyValueError::new_err("a cause")));
@ -1025,14 +1064,14 @@ mod tests {
#[cfg_attr(invalid_from_utf8_lint, allow(invalid_from_utf8))]
let err = std::str::from_utf8(invalid_utf8).expect_err("should be invalid utf8");
Python::with_gil(|py| {
let decode_err = PyUnicodeDecodeError::new_utf8(py, invalid_utf8, err).unwrap();
let decode_err = PyUnicodeDecodeError::new_utf8_bound(py, invalid_utf8, err).unwrap();
assert_eq!(
format!("{:?}", decode_err),
"UnicodeDecodeError('utf-8', b'fo\\xd8o', 2, 3, 'invalid utf-8')"
);
// Restoring should preserve the same error
let e: PyErr = decode_err.into();
let e = PyErr::from_value_bound(decode_err.into_any());
e.restore(py);
assert_eq!(
@ -1081,7 +1120,11 @@ mod tests {
let invalid_utf8 = b"fo\xd8o";
#[cfg_attr(invalid_from_utf8_lint, allow(invalid_from_utf8))]
let err = std::str::from_utf8(invalid_utf8).expect_err("should be invalid utf8");
PyErr::from_value(PyUnicodeDecodeError::new_utf8(py, invalid_utf8, err).unwrap())
PyErr::from_value_bound(
PyUnicodeDecodeError::new_utf8_bound(py, invalid_utf8, err)
.unwrap()
.into_any(),
)
});
test_exception!(PyUnicodeEncodeError, |py| py
.eval_bound("chr(40960).encode('ascii')", None, None)

View File

@ -167,8 +167,11 @@ pub fn from_py_with_with_default<'a, 'py, T>(
pub fn argument_extraction_error(py: Python<'_>, arg_name: &str, error: PyErr) -> PyErr {
use crate::types::any::PyAnyMethods;
if error.get_type_bound(py).is(py.get_type::<PyTypeError>()) {
let remapped_error =
PyTypeError::new_err(format!("argument '{}': {}", arg_name, error.value(py)));
let remapped_error = PyTypeError::new_err(format!(
"argument '{}': {}",
arg_name,
error.value_bound(py)
));
remapped_error.set_cause(py, error.cause(py));
remapped_error
} else {

View File

@ -73,8 +73,8 @@ mod inner {
#[cfg(all(feature = "macros", Py_3_8))]
#[pymethods(crate = "pyo3")]
impl UnraisableCapture {
pub fn hook(&mut self, unraisable: &PyAny) {
let err = PyErr::from_value(unraisable.getattr("exc_value").unwrap());
pub fn hook(&mut self, unraisable: Bound<'_, PyAny>) {
let err = PyErr::from_value_bound(unraisable.getattr("exc_value").unwrap());
let instance = unraisable.getattr("object").unwrap();
self.capture = Some((err, instance.into()));
}

View File

@ -73,9 +73,9 @@ impl<'a> PyStringData<'a> {
match self {
Self::Ucs1(data) => match str::from_utf8(data) {
Ok(s) => Ok(Cow::Borrowed(s)),
Err(e) => Err(crate::PyErr::from_value(PyUnicodeDecodeError::new_utf8(
py, data, e,
)?)),
Err(e) => Err(crate::PyErr::from_value_bound(
PyUnicodeDecodeError::new_utf8_bound(py, data, e)?.into_any(),
)),
},
Self::Ucs2(data) => match String::from_utf16(data) {
Ok(s) => Ok(Cow::Owned(s)),
@ -83,24 +83,30 @@ impl<'a> PyStringData<'a> {
let mut message = e.to_string().as_bytes().to_vec();
message.push(0);
Err(crate::PyErr::from_value(PyUnicodeDecodeError::new(
py,
CStr::from_bytes_with_nul(b"utf-16\0").unwrap(),
self.as_bytes(),
0..self.as_bytes().len(),
CStr::from_bytes_with_nul(&message).unwrap(),
)?))
Err(crate::PyErr::from_value_bound(
PyUnicodeDecodeError::new_bound(
py,
CStr::from_bytes_with_nul(b"utf-16\0").unwrap(),
self.as_bytes(),
0..self.as_bytes().len(),
CStr::from_bytes_with_nul(&message).unwrap(),
)?
.into_any(),
))
}
},
Self::Ucs4(data) => match data.iter().map(|&c| std::char::from_u32(c)).collect() {
Some(s) => Ok(Cow::Owned(s)),
None => Err(crate::PyErr::from_value(PyUnicodeDecodeError::new(
py,
CStr::from_bytes_with_nul(b"utf-32\0").unwrap(),
self.as_bytes(),
0..self.as_bytes().len(),
CStr::from_bytes_with_nul(b"error converting utf-32\0").unwrap(),
)?)),
None => Err(crate::PyErr::from_value_bound(
PyUnicodeDecodeError::new_bound(
py,
CStr::from_bytes_with_nul(b"utf-32\0").unwrap(),
self.as_bytes(),
0..self.as_bytes().len(),
CStr::from_bytes_with_nul(b"error converting utf-32\0").unwrap(),
)?
.into_any(),
)),
},
}
}

View File

@ -150,9 +150,9 @@ except Exception as e:
Some(&locals),
)
.unwrap();
let err = PyErr::from_value(locals.get_item("err").unwrap().unwrap().into_gil_ref());
let traceback = err.value(py).getattr("__traceback__").unwrap();
assert!(err.traceback_bound(py).unwrap().is(traceback));
let err = PyErr::from_value_bound(locals.get_item("err").unwrap().unwrap());
let traceback = err.value_bound(py).getattr("__traceback__").unwrap();
assert!(err.traceback_bound(py).unwrap().is(&traceback));
})
}

View File

@ -137,7 +137,7 @@ fn cancelled_coroutine() {
)
.unwrap_err();
assert_eq!(
err.value(gil).get_type().qualname().unwrap(),
err.value_bound(gil).get_type().qualname().unwrap(),
"CancelledError"
);
})