diff --git a/src/err/err_state.rs b/src/err/err_state.rs index 6b7ef0df9d4..06b7a6ad6f5 100644 --- a/src/err/err_state.rs +++ b/src/err/err_state.rs @@ -13,6 +13,11 @@ use crate::{ types::{PyAnyMethods, PyTraceback, PyType}, Bound, Py, PyAny, PyErrArguments, PyTypeInfo, Python, }; +#[cfg(Py_3_12)] +use { + crate::types::{PyString, PyTuple}, + std::ptr::NonNull, +}; pub(crate) struct PyErrState { // Safety: can only hand out references when in the "normalized" state. Will never change @@ -383,29 +388,82 @@ fn lazy_into_normalized_ffi_tuple( } /// Raises a "lazy" exception state into the Python interpreter. -/// -/// In principle this could be split in two; first a function to create an exception -/// in a normalized state, and then a call to `PyErr_SetRaisedException` to raise it. -/// -/// This would require either moving some logic from C to Rust, or requesting a new -/// API in CPython. fn raise_lazy(py: Python<'_>, lazy: Box) { let PyErrStateLazyFnOutput { ptype, pvalue } = lazy(py); + unsafe { + #[cfg(not(Py_3_12))] if ffi::PyExceptionClass_Check(ptype.as_ptr()) == 0 { ffi::PyErr_SetString( PyTypeError::type_object_raw(py).cast(), c"exceptions must derive from BaseException".as_ptr(), - ) + ); } else { - ffi::PyErr_SetObject(ptype.as_ptr(), pvalue.as_ptr()) + ffi::PyErr_SetObject(ptype.as_ptr(), pvalue.as_ptr()); + } + + #[cfg(Py_3_12)] + { + let exc = create_normalized_exception(ptype.bind(py), pvalue.into_bound(py)); + + ffi::PyErr_SetRaisedException(exc.into_ptr()); + } + } +} + +#[cfg(Py_3_12)] +fn create_normalized_exception<'py>( + ptype: &Bound<'py, PyAny>, + mut pvalue: Bound<'py, PyAny>, +) -> Bound<'py, PyBaseException> { + let py = ptype.py(); + + // 1: check type is a subclass of BaseException + let ptype: Bound<'py, PyType> = if unsafe { ffi::PyExceptionClass_Check(ptype.as_ptr()) } == 0 { + pvalue = PyString::new(py, "exceptions must derive from BaseException").into_any(); + PyTypeError::type_object(py) + } else { + // Safety: PyExceptionClass_Check guarantees that ptype is a subclass of BaseException + unsafe { ptype.cast_unchecked() }.clone() + }; + + let pvalue = if pvalue.is_exact_instance(&ptype) { + return unsafe { + ffi::PyErr_SetObject(ptype.as_ptr(), pvalue.as_ptr()); + ffi::PyErr_GetRaisedException() + .assume_owned_or_opt(py) + .expect("exception missing after just setting it") + .cast_into_unchecked() + }; + } else if pvalue.is_none() { + // None -> no arguments + ptype.call0().and_then(|pvalue| Ok(pvalue.cast_into()?)) + } else if let Ok(tup) = pvalue.cast::() { + // Tuple -> use as tuple of arguments + ptype.call1(tup).and_then(|pvalue| Ok(pvalue.cast_into()?)) + } else { + // Anything else -> use as single argument + ptype + .call1((pvalue,)) + .and_then(|pvalue| Ok(pvalue.cast_into()?)) + }; + + match pvalue { + Ok(pvalue) => { + // Implicitly set the context of the new exception to the currently handled exception, if any. + unsafe { + if let Some(context) = NonNull::new(ffi::PyErr_GetHandledException()) { + ffi::PyException_SetContext(pvalue.as_ptr(), context.as_ptr()); + } + } + pvalue } + Err(e) => e.value(py).clone(), } } #[cfg(test)] mod tests { - use crate::{ exceptions::PyValueError, sync::PyOnceLock, Py, PyAny, PyErr, PyErrArguments, Python, }; @@ -478,4 +536,35 @@ mod tests { .is_instance_of::(py)) }); } + + #[test] + #[cfg(feature = "macros")] + fn test_new_exception_context() { + use crate::{ + exceptions::{PyRuntimeError, PyValueError}, + pyfunction, + types::{PyDict, PyDictMethods}, + wrap_pyfunction, PyResult, + }; + #[pyfunction(crate = "crate")] + fn throw_exception() -> PyResult<()> { + Err(PyValueError::new_err("error happened")) + } + + Python::attach(|py| { + let globals = PyDict::new(py); + let f = wrap_pyfunction!(throw_exception, py).unwrap(); + globals.set_item("throw_exception", f).unwrap(); + let err = py + .run( + c"try:\n raise RuntimeError()\nexcept:\n throw_exception()\n", + Some(&globals), + None, + ) + .unwrap_err(); + + let context = err.context(py).unwrap(); + assert!(context.is_instance_of::(py)) + }) + } }