Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -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 = []

Expand Down
5 changes: 5 additions & 0 deletions guide/src/features.md
Original file line number Diff line number Diff line change
Expand Up @@ -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`

Expand All @@ -165,6 +166,10 @@ Like [`Arc<parking_lot::Mutex>`](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.
Expand Down
1 change: 1 addition & 0 deletions newsfragments/6342.added.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Add support for converting `btparse` and therefore `std` backtraces into Python tracebacks.
30 changes: 28 additions & 2 deletions src/conversions/anyhow.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<anyhow::Error> for PyErr {
fn from(mut error: anyhow::Error) -> Self {
Expand All @@ -113,12 +113,22 @@ impl From<anyhow::Error> 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) {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I wonder, is it possible to somehow end up with nested Rust -> Python tracebacks in here? I suspect it'd be quite hard to extract that structure...

@flying-sheep flying-sheep Aug 22, 2026

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think so too, as std::backtrace objects can’t be manipulated (thus my addition of btparse).

I think we can at do a better job at “anyhow error that can be downcast to a PyErr” by stitching the tracebacks together.

I still need guidance for stable-eyre and color-eyre – they both use the crates.io backtrace library, which can be modified, so that would be possible.

@bschoenmaeckers bschoenmaeckers Aug 22, 2026

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I wonder, is it possible to somehow end up with nested Rust -> Python tracebacks in here? I suspect it'd be quite hard to extract that structure...

I have a working POC at #5872.
It is blocked by #5876 to get more control on exception creation.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks great, and seems quite orthogonal to this PR. Of course whichever is merged first would mean the other could benefit from it.

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::*;
Expand Down Expand Up @@ -178,6 +188,7 @@ mod test_anyhow {
|py| converted.is_instance_of::<PyValueError>(py)
))
}

#[test]
fn test_pyo3_unwrap_complex_err() {
let origin_exc = PyValueError::new_err("Value Error");
Expand All @@ -188,4 +199,19 @@ mod test_anyhow {
|py| converted.is_instance_of::<PyRuntimeError>(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));

@davidhewitt davidhewitt Aug 22, 2026

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would love to see a more complete test of the full formatted traceback here!

})
}
}
70 changes: 70 additions & 0 deletions src/conversions/backtrace.rs
Original file line number Diff line number Diff line change
@@ -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<Self::Output, Self::Error> {
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<Self::Output, Self::Error> {
let mut tb: PyResult<Bound<'_, PyTraceback>> =
Err(PyErr::new::<PyRuntimeError, _>("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<Self::Output, Self::Error> {
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"<unknown>"),
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")
})
}
1 change: 1 addition & 0 deletions src/conversions/mod.rs
Original file line number Diff line number Diff line change
@@ -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;
Expand Down
Loading