use fidius_core::PluginError;
use pyo3::prelude::*;
use pyo3::types::PyTraceback;
use serde_json::json;
pub fn pyerr_to_plugin_error(err: PyErr) -> PluginError {
Python::with_gil(|py| {
let value = err.value(py);
if is_fidius_plugin_error(py, value) {
let code = value
.getattr("code")
.and_then(|c| c.extract::<String>())
.unwrap_or_else(|_| "PluginError".to_string());
let message = value
.getattr("message")
.and_then(|m| m.extract::<String>())
.or_else(|_| value.str().and_then(|s| s.extract::<String>()))
.unwrap_or_default();
let details = value
.getattr("details")
.ok()
.filter(|d| !d.is_none())
.and_then(|d| json_dumps(py, &d));
return PluginError {
code,
message,
details,
};
}
let code = value
.getattr("__class__")
.and_then(|cls| cls.getattr("__name__"))
.and_then(|name| name.extract::<String>())
.unwrap_or_else(|_| "UNKNOWN_PYTHON_ERROR".to_string());
let message = value
.str()
.and_then(|s| s.extract::<String>())
.unwrap_or_else(|_| "<unprintable Python exception>".to_string());
let traceback = err
.traceback(py)
.and_then(|tb| format_traceback(py, tb).ok())
.unwrap_or_default();
let details = json!({ "traceback": traceback }).to_string();
PluginError {
code,
message,
details: Some(details),
}
})
}
fn format_traceback(py: Python<'_>, tb: Bound<'_, PyTraceback>) -> PyResult<String> {
let traceback_mod = py.import("traceback")?;
let frames = traceback_mod.call_method1("format_tb", (tb,))?;
let parts: Vec<String> = frames.extract()?;
Ok(parts.join(""))
}
fn is_fidius_plugin_error(py: Python<'_>, value: &Bound<'_, PyAny>) -> bool {
py.import("fidius")
.and_then(|m| m.getattr("PluginError"))
.and_then(|cls| value.is_instance(&cls))
.unwrap_or(false)
}
fn json_dumps(py: Python<'_>, obj: &Bound<'_, PyAny>) -> Option<String> {
py.import("json")
.ok()
.and_then(|j| j.call_method1("dumps", (obj,)).ok())
.and_then(|s| s.extract::<String>().ok())
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn maps_value_error_to_plugin_error() {
crate::ensure_initialized();
let err = Python::with_gil(|py| -> PyErr {
py.eval(
std::ffi::CString::new("(_ for _ in ()).throw(ValueError('boom'))")
.unwrap()
.as_c_str(),
None,
None,
)
.unwrap_err()
});
let pe = pyerr_to_plugin_error(err);
assert_eq!(pe.code, "ValueError");
assert!(pe.message.contains("boom"));
let details = pe.details.expect("details should be set");
assert!(details.contains("traceback"));
}
}