diff --git a/Cargo.toml b/Cargo.toml index 7af1eed7d14..fcd38e9f28b 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -70,6 +70,7 @@ uuid = { version = "1.12.0", optional = true } lock_api = { version = "0.4", optional = true } parking_lot = { version = "0.12", optional = true } iana-time-zone = { version = "0.1", optional = true, features = ["fallback"]} +btparse = { version = "0.2.0", optional = true } [target.'cfg(not(target_has_atomic = "64"))'.dependencies] portable-atomic = "1.0" @@ -137,6 +138,9 @@ abi3t-py315 = ["abi3t", "pyo3-ffi/abi3t-py315"] # deprecated: no longer needed, raw-dylib is used instead generate-import-lib = ["pyo3-ffi/generate-import-lib"] +anyhow = ["dep:anyhow", "btparse"] +btparse = ["dep:btparse"] + # Changes `Python::attach` to automatically initialize the Python interpreter if needed. auto-initialize = [] diff --git a/guide/src/features.md b/guide/src/features.md index c3a3c107094..a5bd3ce1094 100644 --- a/guide/src/features.md +++ b/guide/src/features.md @@ -155,6 +155,7 @@ These features enable conversions between Python types and types from other Rust Adds a dependency on [anyhow](https://docs.rs/anyhow). Enables a conversion from [anyhow](https://docs.rs/anyhow)’s [`Error`](https://docs.rs/anyhow/latest/anyhow/struct.Error.html) type to [`PyErr`]({{#PYO3_DOCS_URL}}/pyo3/struct.PyErr.html), for easy error handling. +Includes the [`btparse`](#btparse) feature. ### `arc_lock` @@ -165,6 +166,10 @@ Like [`Arc`](https://docs.rs/parking_lot/latest/parking_lot/ Adds a dependency on [bigdecimal](https://docs.rs/bigdecimal) and enables conversions into its [`BigDecimal`](https://docs.rs/bigdecimal/latest/bigdecimal/struct.BigDecimal.html) type. +### `btparse` + +Adds a dependency on [btparse](https://docs.rs/btparse) and enables conversions into its [`Backtrace`](https://docs.rs/btparse/latest/btparse/struct.Backtrace.html) and [`Frame`](https://docs.rs/btparse/latest/btparse/struct.Frame.html) types. + ### `bytes` Adds a dependency on [bytes](https://docs.rs/bytes/latest/bytes) and enables conversions into its [`Bytes`](https://docs.rs/bytes/latest/bytes/struct.Bytes.html) type. diff --git a/newsfragments/6342.added.md b/newsfragments/6342.added.md new file mode 100644 index 00000000000..97cdebcca6f --- /dev/null +++ b/newsfragments/6342.added.md @@ -0,0 +1 @@ +Add support for converting `btparse` and therefore `std` backtraces into Python tracebacks. diff --git a/src/conversions/anyhow.rs b/src/conversions/anyhow.rs index 8d2a8279e33..33ba729aa37 100644 --- a/src/conversions/anyhow.rs +++ b/src/conversions/anyhow.rs @@ -102,7 +102,7 @@ //! [Error handling]: https://doc.rust-lang.org/book/ch09-02-recoverable-errors-with-result.html "Recoverable Errors with Result - The Rust Programming Language" use crate::exceptions::PyRuntimeError; -use crate::PyErr; +use crate::{IntoPyObject, PyErr, Python}; impl From for PyErr { fn from(mut error: anyhow::Error) -> Self { @@ -113,12 +113,22 @@ impl From for PyErr { Err(error) => error, }; } - PyRuntimeError::new_err(format!("{error:?}")) + + let err = PyRuntimeError::new_err(format!("{error:?}")); + #[cfg(all(not(Py_LIMITED_API), not(PyPy), not(GraalPy)))] + Python::try_attach(|py| { + if let Ok(tb) = error.backtrace().into_pyobject(py) { + err.set_traceback(py, Some(tb)); + } + }); + err } } #[cfg(test)] mod test_anyhow { + use std::path::PathBuf; + use crate::exceptions::{PyRuntimeError, PyValueError}; use crate::platform::prelude::*; use crate::prelude::*; @@ -178,6 +188,7 @@ mod test_anyhow { |py| converted.is_instance_of::(py) )) } + #[test] fn test_pyo3_unwrap_complex_err() { let origin_exc = PyValueError::new_err("Value Error"); @@ -188,4 +199,19 @@ mod test_anyhow { |py| converted.is_instance_of::(py) )) } + + #[test] + fn test_traceback() { + let origin_exc = PyValueError::new_err("Value Error"); + let mut err: anyhow::Error = origin_exc.into(); + err = err.context("Context"); + let converted: PyErr = err.into(); + Python::attach(|py| { + let traceback = converted.traceback(py).expect("expected traceback"); + let format = traceback.format().expect("expected formatting to work"); + let file_path = PathBuf::from(file!()); + let file_name = file_path.file_name().and_then(|s| s.to_str()).unwrap(); + assert!(format.contains(file_name)); + }) + } } diff --git a/src/conversions/backtrace.rs b/src/conversions/backtrace.rs new file mode 100644 index 00000000000..e13b19eb72d --- /dev/null +++ b/src/conversions/backtrace.rs @@ -0,0 +1,70 @@ +#![cfg(all(feature = "btparse", not(Py_LIMITED_API), not(PyPy), not(GraalPy)))] +//! Conversion from standard backtrace + +use alloc::ffi::CString; + +use crate::{ + exceptions::PyRuntimeError, + types::{PyFrame, PyTraceback}, + Bound, IntoPyObject, PyErr, PyResult, Python, +}; + +impl<'py> IntoPyObject<'py> for &std::backtrace::Backtrace { + type Target = PyTraceback; + type Output = Bound<'py, Self::Target>; + type Error = PyErr; + + fn into_pyobject(self, py: Python<'py>) -> Result { + btparse::deserialize(self) + .map_err(|e| PyRuntimeError::new_err(format!("{e}")))? + .into_pyobject(py) + } +} + +impl<'py> IntoPyObject<'py> for btparse::Backtrace { + type Target = PyTraceback; + type Output = Bound<'py, Self::Target>; + type Error = PyErr; + + fn into_pyobject(self, py: Python<'py>) -> Result { + let mut tb: PyResult> = + Err(PyErr::new::("no frames")); + for frame in self.frames { + let line_number = frame.line.unwrap_or(0).try_into().unwrap_or(0); + tb = Ok(PyTraceback::new( + py, + tb.ok(), + frame.into_pyobject(py)?, + 0, + line_number, + )?); + } + tb + } +} + +impl<'py> IntoPyObject<'py> for btparse::Frame { + type Target = PyFrame; + type Output = Bound<'py, Self::Target>; + type Error = PyErr; + + fn into_pyobject(self, py: Python<'py>) -> Result { + let file_name = self.file.and_then(|s| CString::new(s).ok()); + let function = cstring_maybe_trunc(self.function.as_str()); + PyFrame::new( + py, + file_name.as_deref().unwrap_or(c""), + function.as_c_str(), + self.line.unwrap_or(0).try_into().unwrap_or(0), + ) + } +} + +fn cstring_maybe_trunc(s: &str) -> CString { + CString::new(s).unwrap_or_else(|e| { + let end = e.nul_position(); + let mut v = e.into_vec(); + v.truncate(end + 1); + CString::from_vec_with_nul(v).expect("NulError lied") + }) +} diff --git a/src/conversions/mod.rs b/src/conversions/mod.rs index 921e4ea40c6..638c9e5ef94 100644 --- a/src/conversions/mod.rs +++ b/src/conversions/mod.rs @@ -1,6 +1,7 @@ //! This module contains conversions between various Rust object and their representation in Python. pub mod anyhow; +pub mod backtrace; pub mod bigdecimal; pub mod bytes; pub mod chrono;