fix(py): a panic in the library raises clawhdf5.InternalError, not PanicException
PanicException derives from BaseException, so `except Exception` let a library bug through. Every call from the bindings into the library now runs under catch_unwind and a panic becomes InternalError (RuntimeError) naming the object. Tests: a hidden hook panics inside the guard; and the v4 chunk indexes are compared with h5py from Python — with the library fix reverted, ds[0:30] of the implicit-index dataset now raises InternalError instead of aborting the test run. Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
This commit is contained in:
@@ -24,6 +24,42 @@ pub(crate) use dataset::PyDataset;
|
||||
pub(crate) use file::PyFile;
|
||||
pub(crate) use group::PyGroup;
|
||||
|
||||
pyo3::create_exception!(
|
||||
clawhdf5,
|
||||
InternalError,
|
||||
pyo3::exceptions::PyRuntimeError,
|
||||
"A bug in clawhdf5 met while reading or writing a file (a Rust panic, \
|
||||
caught). Derived from RuntimeError, so `except Exception` handles it."
|
||||
);
|
||||
|
||||
/// The text of a caught panic.
|
||||
pub(crate) fn panic_text(payload: &(dyn std::any::Any + Send)) -> String {
|
||||
payload
|
||||
.downcast_ref::<&str>()
|
||||
.map(|s| (*s).to_string())
|
||||
.or_else(|| payload.downcast_ref::<String>().cloned())
|
||||
.unwrap_or_else(|| "unknown panic".to_string())
|
||||
}
|
||||
|
||||
/// Run `f`, turning a panic in the library into [`InternalError`] instead of
|
||||
/// PyO3's `PanicException` (a `BaseException`, which `except Exception`
|
||||
/// does not catch). Wraps every call into the library.
|
||||
pub(crate) fn no_panic<T>(f: impl FnOnce() -> PyResult<T>) -> PyResult<T> {
|
||||
std::panic::catch_unwind(std::panic::AssertUnwindSafe(f)).unwrap_or_else(|p| {
|
||||
Err(InternalError::new_err(format!(
|
||||
"clawhdf5 internal error (please report it): {}",
|
||||
panic_text(&*p)
|
||||
)))
|
||||
})
|
||||
}
|
||||
|
||||
/// A test hook: panics inside [`no_panic`], so the tests can check that a
|
||||
/// library panic reaches Python as an ordinary exception.
|
||||
#[pyfunction]
|
||||
fn _panic_for_test() -> PyResult<()> {
|
||||
no_panic(|| panic!("deliberate panic for the test suite"))
|
||||
}
|
||||
|
||||
/// Convert a `clawhdf5_rs::Error` into a `PyErr`.
|
||||
///
|
||||
/// Maps different error variants to more specific Python exception types:
|
||||
@@ -276,6 +312,8 @@ fn clawhdf5(m: &Bound<'_, PyModule>) -> PyResult<()> {
|
||||
m.add_class::<PyGroup>()?;
|
||||
m.add_class::<PyAttrs>()?;
|
||||
m.add_class::<PyEmpty>()?;
|
||||
m.add("InternalError", m.py().get_type::<InternalError>())?;
|
||||
m.add_function(wrap_pyfunction!(_panic_for_test, m)?)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user