diff --git a/help.html b/help.html index 4e65179..8905567 100644 --- a/help.html +++ b/help.html @@ -1 +1 @@ -
pub fn err_with_location(
+ py: Python<'_>,
+ err: PyErr,
+ file: &str,
+ line: u32,
+ column: u32,
+) -> PyErr
Utility function to add a traceback with the error’s file
, line
, and
+column
location information to the err
.
This function may be used when implementing AnyErrorToPyErr
or
+MapErrorToPyErr
to pythonize any available error location information.
Unified error causality chains across Rust and Python using PyErrChain
.
std::error::Error
via a specific error type T
to a
+PyErr
by downcasting when used as MapErrorToPyErr
;PyErrChain
wraps a PyErr
together with its causality chain.std::error::Error
to PyErr
.E
implementing
+std::error::Error
and wrapped errors such as MyError<E>
to PyErr
s.file
, line
, and
+column
location information to the err
.pub struct DowncastToPyErr;
Try to map a std::error::Error
via a specific error type T
to a
+PyErr
by downcasting when used as MapErrorToPyErr
;
pub struct ErrorNoPyErr;
Never attempt to translate any std::error::Error
to PyErr
when used
+as AnyErrorToPyErr
.
pub struct IoErrorToPyErr;
Translate std::io::Error
to PyErr
when used as AnyErrorToPyErr
.
pub struct PyErrChain { /* private fields */ }
PyErrChain
wraps a PyErr
together with its causality chain.
Unlike PyErr
, PyErrChain
’s implementation of std::error::Error
+provides access to the error cause through the std::error::Error::source
+method.
Note that since PyErr
s can be readily cloned, the PyErrChain
only
+captures the causality chain at the time of construction. Calling
+PyErr::set_cause
on a clone of the wrapped error after construction will
+thus not update the chain as captured by this PyErrChain
.
Create a new PyErrChain
from err
.
The error’s causality chain, as expressed by
+std::error::Error::source
, is translated into a PyErr::cause
+chain.
If any error in the chain is a PyErrChain
or a PyErr
, it is
+extracted directly. All other error types are translated into PyErr
s
+using PyException::new_err
with format!("{}", err)
.
If you want to customize the translation from std::error::Error
into
+PyErr
, please use Self::new_with_downcaster
instead.
Create a new PyErrChain
from err
using a custom translator from
+std::error::Error
to PyErr
.
The error’s causality chain, as expressed by
+std::error::Error::source
, is translated into a PyErr::cause
+chain.
If any error in the chain is a PyErrChain
or a PyErr
, it is
+extracted directly. All other error types first attempt to be translated
+into PyErr
s using the [PyErrDowncaster
] and [PyErrMapper
]. As a
+fallback, all remaining errors are translated into PyErr
s using
+PyException::new_err
with format!("{}", err)
.
Clone the PyErrChain
.
This requires the GIL, which is why PyErrChain
does not implement
+Clone
.
Note that all elements of the cloned PyErrChain
will be shared using
+reference counting in Python with the existing PyErrChain
self
.
Get a reference to the wrapped PyErr
.
Note that while PyErr::set_cause
can be called on the returned
+PyErr
, the change in causality chain will not be reflected in
+this PyErrChain
.
Get a reference to the cause of the wrapped PyErr
.
Note that while PyErr::set_cause
can be called on the returned
+PyErr
, the change in causality chain will not be reflected in
+this PyErrChain
.
pub trait AnyErrorToPyErr {
+ // Required methods
+ fn try_from_err<T: MapErrorToPyErr>(
+ py: Python<'_>,
+ err: Box<dyn Error + 'static>,
+ ) -> Result<PyErr, Box<dyn Error + 'static>>;
+ fn try_from_err_ref<T: MapErrorToPyErr>(
+ py: Python<'_>,
+ err: &(dyn Error + 'static),
+ ) -> Option<PyErr>;
+}
Utility trait to try to translate from std::error::Error
to PyErr
.
ErrorNoPyErr
may be used to always fail at translating.
IoErrorToPyErr
may be used to translate std::io::Error
to PyErr
.
Try to translate from a boxed err
to a PyErr
, or return the err
.
When a strongly typed translation from some specific error type E
to a
+PyErr
is attempted, MapErrorToPyErr::try_map
should be used to allow
+the mapper to test for E
in addition to wrapped errors such as
+MyError<E>
.
Returns the original err
if translating to a PyErr
failed.
Try to translate from an err
reference to a PyErr
, or return
+None
.
When a strongly typed translation from some specific error type E
to a
+PyErr
is attempted, MapErrorToPyErr::try_map_ref
should be used to
+allow the mapper to test for E
in addition to wrapped errors such as
+MyError<E>
.
pub trait MapErrorToPyErr {
+ // Required methods
+ fn try_map<T: Error + 'static>(
+ py: Python<'_>,
+ err: Box<dyn Error + 'static>,
+ map: impl FnOnce(Box<T>) -> PyErr,
+ ) -> Result<PyErr, Box<dyn Error + 'static>>;
+ fn try_map_ref<T: Error + 'static>(
+ py: Python<'_>,
+ err: &(dyn Error + 'static),
+ map: impl FnOnce(&T) -> PyErr,
+ ) -> Option<PyErr>;
+}
Utility trait to try to translate via specific error types E
implementing
+std::error::Error
and wrapped errors such as MyError<E>
to PyErr
s.
DowncastToPyErr
may be used to only try to translate via E
using
+downcasting.
std::error::Error
…\nTry to map a std::error::Error
via a specific error type T
…\nNever attempt to translate any std::error::Error
to PyErr
…\nTranslate std::io::Error
to PyErr
when used as …\nUtility trait to try to translate via specific error types …\nPyErrChain
wraps a PyErr
together with its causality chain.\nGet a reference to the wrapped PyErr
.\nGet a reference to the cause of the wrapped PyErr
.\nClone the PyErrChain
.\nUtility function to add a traceback with the error’s file
…\nReturns the argument unchanged.\nReturns the argument unchanged.\nReturns the argument unchanged.\nReturns the argument unchanged.\nCalls U::from(self)
.\nCalls U::from(self)
.\nCalls U::from(self)
.\nCalls U::from(self)
.\nCreate a new PyErrChain
from err
.\nCreate a new PyErrChain
from err
using a custom translator …\nTry to translate from a boxed err
to a PyErr
, or return …\nTry to translate from an err
reference to a PyErr
, or …\nTry to map from a boxed err
via the specific error type T
…\nTry to map from an err
reference via the specific error …")
\ No newline at end of file
diff --git a/settings.html b/settings.html
index 370f3ce..b31b116 100644
--- a/settings.html
+++ b/settings.html
@@ -1 +1 @@
-1 +lib.rs - source \ No newline at end of file diff --git a/trait.impl/core/convert/trait.From.js b/trait.impl/core/convert/trait.From.js new file mode 100644 index 0000000..a98680a --- /dev/null +++ b/trait.impl/core/convert/trait.From.js @@ -0,0 +1,9 @@ +(function() { + var implementors = Object.fromEntries([["pyo3_error",[["impl From<PyErr> for PyErrChain"],["impl From<PyErrChain> for PyErr"]]]]); + if (window.register_implementors) { + window.register_implementors(implementors); + } else { + window.pending_implementors = implementors; + } +})() +//{"start":57,"fragment_lengths":[816]} \ No newline at end of file diff --git a/trait.impl/core/error/trait.Error.js b/trait.impl/core/error/trait.Error.js new file mode 100644 index 0000000..b35d6e5 --- /dev/null +++ b/trait.impl/core/error/trait.Error.js @@ -0,0 +1,9 @@ +(function() { + var implementors = Object.fromEntries([["pyo3_error",[["impl Error for PyErrChain"]]]]); + if (window.register_implementors) { + window.register_implementors(implementors); + } else { + window.pending_implementors = implementors; + } +})() +//{"start":57,"fragment_lengths":[282]} \ No newline at end of file diff --git a/trait.impl/core/fmt/trait.Debug.js b/trait.impl/core/fmt/trait.Debug.js new file mode 100644 index 0000000..298b291 --- /dev/null +++ b/trait.impl/core/fmt/trait.Debug.js @@ -0,0 +1,9 @@ +(function() { + var implementors = Object.fromEntries([["pyo3_error",[["impl Debug for PyErrChain"]]]]); + if (window.register_implementors) { + window.register_implementors(implementors); + } else { + window.pending_implementors = implementors; + } +})() +//{"start":57,"fragment_lengths":[278]} \ No newline at end of file diff --git a/trait.impl/core/fmt/trait.Display.js b/trait.impl/core/fmt/trait.Display.js new file mode 100644 index 0000000..f89e921 --- /dev/null +++ b/trait.impl/core/fmt/trait.Display.js @@ -0,0 +1,9 @@ +(function() { + var implementors = Object.fromEntries([["pyo3_error",[["impl Display for PyErrChain"]]]]); + if (window.register_implementors) { + window.register_implementors(implementors); + } else { + window.pending_implementors = implementors; + } +})() +//{"start":57,"fragment_lengths":[284]} \ No newline at end of file diff --git a/trait.impl/core/marker/trait.Freeze.js b/trait.impl/core/marker/trait.Freeze.js new file mode 100644 index 0000000..baf550b --- /dev/null +++ b/trait.impl/core/marker/trait.Freeze.js @@ -0,0 +1,9 @@ +(function() { + var implementors = Object.fromEntries([["pyo3_error",[["impl !Freeze for PyErrChain",1,["pyo3_error::PyErrChain"]],["impl Freeze for DowncastToPyErr",1,["pyo3_error::DowncastToPyErr"]],["impl Freeze for ErrorNoPyErr",1,["pyo3_error::ErrorNoPyErr"]],["impl Freeze for IoErrorToPyErr",1,["pyo3_error::IoErrorToPyErr"]]]]]); + if (window.register_implementors) { + window.register_implementors(implementors); + } else { + window.pending_implementors = implementors; + } +})() +//{"start":57,"fragment_lengths":[1261]} \ No newline at end of file diff --git a/trait.impl/core/marker/trait.Send.js b/trait.impl/core/marker/trait.Send.js new file mode 100644 index 0000000..30da0c7 --- /dev/null +++ b/trait.impl/core/marker/trait.Send.js @@ -0,0 +1,9 @@ +(function() { + var implementors = Object.fromEntries([["pyo3_error",[["impl Send for DowncastToPyErr",1,["pyo3_error::DowncastToPyErr"]],["impl Send for ErrorNoPyErr",1,["pyo3_error::ErrorNoPyErr"]],["impl Send for IoErrorToPyErr",1,["pyo3_error::IoErrorToPyErr"]],["impl Send for PyErrChain",1,["pyo3_error::PyErrChain"]]]]]); + if (window.register_implementors) { + window.register_implementors(implementors); + } else { + window.pending_implementors = implementors; + } +})() +//{"start":57,"fragment_lengths":[1236]} \ No newline at end of file diff --git a/trait.impl/core/marker/trait.Sync.js b/trait.impl/core/marker/trait.Sync.js new file mode 100644 index 0000000..45abcba --- /dev/null +++ b/trait.impl/core/marker/trait.Sync.js @@ -0,0 +1,9 @@ +(function() { + var implementors = Object.fromEntries([["pyo3_error",[["impl Sync for DowncastToPyErr",1,["pyo3_error::DowncastToPyErr"]],["impl Sync for ErrorNoPyErr",1,["pyo3_error::ErrorNoPyErr"]],["impl Sync for IoErrorToPyErr",1,["pyo3_error::IoErrorToPyErr"]],["impl Sync for PyErrChain",1,["pyo3_error::PyErrChain"]]]]]); + if (window.register_implementors) { + window.register_implementors(implementors); + } else { + window.pending_implementors = implementors; + } +})() +//{"start":57,"fragment_lengths":[1236]} \ No newline at end of file diff --git a/trait.impl/core/marker/trait.Unpin.js b/trait.impl/core/marker/trait.Unpin.js new file mode 100644 index 0000000..1562b95 --- /dev/null +++ b/trait.impl/core/marker/trait.Unpin.js @@ -0,0 +1,9 @@ +(function() { + var implementors = Object.fromEntries([["pyo3_error",[["impl Unpin for DowncastToPyErr",1,["pyo3_error::DowncastToPyErr"]],["impl Unpin for ErrorNoPyErr",1,["pyo3_error::ErrorNoPyErr"]],["impl Unpin for IoErrorToPyErr",1,["pyo3_error::IoErrorToPyErr"]],["impl Unpin for PyErrChain",1,["pyo3_error::PyErrChain"]]]]]); + if (window.register_implementors) { + window.register_implementors(implementors); + } else { + window.pending_implementors = implementors; + } +})() +//{"start":57,"fragment_lengths":[1248]} \ No newline at end of file diff --git a/trait.impl/core/panic/unwind_safe/trait.RefUnwindSafe.js b/trait.impl/core/panic/unwind_safe/trait.RefUnwindSafe.js new file mode 100644 index 0000000..a1a929d --- /dev/null +++ b/trait.impl/core/panic/unwind_safe/trait.RefUnwindSafe.js @@ -0,0 +1,9 @@ +(function() { + var implementors = Object.fromEntries([["pyo3_error",[["impl !RefUnwindSafe for PyErrChain",1,["pyo3_error::PyErrChain"]],["impl RefUnwindSafe for DowncastToPyErr",1,["pyo3_error::DowncastToPyErr"]],["impl RefUnwindSafe for ErrorNoPyErr",1,["pyo3_error::ErrorNoPyErr"]],["impl RefUnwindSafe for IoErrorToPyErr",1,["pyo3_error::IoErrorToPyErr"]]]]]); + if (window.register_implementors) { + window.register_implementors(implementors); + } else { + window.pending_implementors = implementors; + } +})() +//{"start":57,"fragment_lengths":[1437]} \ No newline at end of file diff --git a/trait.impl/core/panic/unwind_safe/trait.UnwindSafe.js b/trait.impl/core/panic/unwind_safe/trait.UnwindSafe.js new file mode 100644 index 0000000..b51ef77 --- /dev/null +++ b/trait.impl/core/panic/unwind_safe/trait.UnwindSafe.js @@ -0,0 +1,9 @@ +(function() { + var implementors = Object.fromEntries([["pyo3_error",[["impl !UnwindSafe for PyErrChain",1,["pyo3_error::PyErrChain"]],["impl UnwindSafe for DowncastToPyErr",1,["pyo3_error::DowncastToPyErr"]],["impl UnwindSafe for ErrorNoPyErr",1,["pyo3_error::ErrorNoPyErr"]],["impl UnwindSafe for IoErrorToPyErr",1,["pyo3_error::IoErrorToPyErr"]]]]]); + if (window.register_implementors) { + window.register_implementors(implementors); + } else { + window.pending_implementors = implementors; + } +})() +//{"start":57,"fragment_lengths":[1401]} \ No newline at end of file diff --git a/trait.impl/pyo3_error/trait.AnyErrorToPyErr.js b/trait.impl/pyo3_error/trait.AnyErrorToPyErr.js new file mode 100644 index 0000000..aa064c2 --- /dev/null +++ b/trait.impl/pyo3_error/trait.AnyErrorToPyErr.js @@ -0,0 +1,9 @@ +(function() { + var implementors = Object.fromEntries([["pyo3_error",[]]]); + if (window.register_implementors) { + window.register_implementors(implementors); + } else { + window.pending_implementors = implementors; + } +})() +//{"start":57,"fragment_lengths":[17]} \ No newline at end of file diff --git a/trait.impl/pyo3_error/trait.MapErrorToPyErr.js b/trait.impl/pyo3_error/trait.MapErrorToPyErr.js new file mode 100644 index 0000000..aa064c2 --- /dev/null +++ b/trait.impl/pyo3_error/trait.MapErrorToPyErr.js @@ -0,0 +1,9 @@ +(function() { + var implementors = Object.fromEntries([["pyo3_error",[]]]); + if (window.register_implementors) { + window.register_implementors(implementors); + } else { + window.pending_implementors = implementors; + } +})() +//{"start":57,"fragment_lengths":[17]} \ No newline at end of file 1 2 3 4 @@ -29,6 +29,525 @@ 29 30 31 +32 +33 +34 +35 +36 +37 +38 +39 +40 +41 +42 +43 +44 +45 +46 +47 +48 +49 +50 +51 +52 +53 +54 +55 +56 +57 +58 +59 +60 +61 +62 +63 +64 +65 +66 +67 +68 +69 +70 +71 +72 +73 +74 +75 +76 +77 +78 +79 +80 +81 +82 +83 +84 +85 +86 +87 +88 +89 +90 +91 +92 +93 +94 +95 +96 +97 +98 +99 +100 +101 +102 +103 +104 +105 +106 +107 +108 +109 +110 +111 +112 +113 +114 +115 +116 +117 +118 +119 +120 +121 +122 +123 +124 +125 +126 +127 +128 +129 +130 +131 +132 +133 +134 +135 +136 +137 +138 +139 +140 +141 +142 +143 +144 +145 +146 +147 +148 +149 +150 +151 +152 +153 +154 +155 +156 +157 +158 +159 +160 +161 +162 +163 +164 +165 +166 +167 +168 +169 +170 +171 +172 +173 +174 +175 +176 +177 +178 +179 +180 +181 +182 +183 +184 +185 +186 +187 +188 +189 +190 +191 +192 +193 +194 +195 +196 +197 +198 +199 +200 +201 +202 +203 +204 +205 +206 +207 +208 +209 +210 +211 +212 +213 +214 +215 +216 +217 +218 +219 +220 +221 +222 +223 +224 +225 +226 +227 +228 +229 +230 +231 +232 +233 +234 +235 +236 +237 +238 +239 +240 +241 +242 +243 +244 +245 +246 +247 +248 +249 +250 +251 +252 +253 +254 +255 +256 +257 +258 +259 +260 +261 +262 +263 +264 +265 +266 +267 +268 +269 +270 +271 +272 +273 +274 +275 +276 +277 +278 +279 +280 +281 +282 +283 +284 +285 +286 +287 +288 +289 +290 +291 +292 +293 +294 +295 +296 +297 +298 +299 +300 +301 +302 +303 +304 +305 +306 +307 +308 +309 +310 +311 +312 +313 +314 +315 +316 +317 +318 +319 +320 +321 +322 +323 +324 +325 +326 +327 +328 +329 +330 +331 +332 +333 +334 +335 +336 +337 +338 +339 +340 +341 +342 +343 +344 +345 +346 +347 +348 +349 +350 +351 +352 +353 +354 +355 +356 +357 +358 +359 +360 +361 +362 +363 +364 +365 +366 +367 +368 +369 +370 +371 +372 +373 +374 +375 +376 +377 +378 +379 +380 +381 +382 +383 +384 +385 +386 +387 +388 +389 +390 +391 +392 +393 +394 +395 +396 +397 +398 +399 +400 +401 +402 +403 +404 +405 +406 +407 +408 +409 +410 +411 +412 +413 +414 +415 +416 +417 +418 +419 +420 +421 +422 +423 +424 +425 +426 +427 +428 +429 +430 +431 +432 +433 +434 +435 +436 +437 +438 +439 +440 +441 +442 +443 +444 +445 +446 +447 +448 +449 +450 +451 +452 +453 +454 +455 +456 +457 +458 +459 +460 +461 +462 +463 +464 +465 +466 +467 +468 +469 +470 +471 +472 +473 +474 +475 +476 +477 +478 +479 +480 +481 +482 +483 +484 +485 +486 +487 +488 +489 +490 +491 +492 +493 +494 +495 +496 +497 +498 +499 +500 +501 +502 +503 +504 +505 +506 +507 +508 +509 +510 +511 +512 +513 +514 +515 +516 +517 +518 +519 +520 +521 +522 +523 +524 +525 +526 +527 +528 +529 +530 +531 +532 +533 +534 +535 +536 +537 +538 +539 +540 +541 +542 +543 +544 +545 +546 +547 +548 +549 +550//! [![CI Status]][workflow] [![MSRV]][repo] [![Latest Version]][crates.io] [![Rust Doc Crate]][docs.rs] [![Rust Doc Main]][docs] //! //! [CI Status]: https://img.shields.io/github/actions/workflow/status/juntyr/pyo3-error/ci.yml?branch=main @@ -45,9 +564,430 @@ //! //! [Rust Doc Main]: https://img.shields.io/badge/docs-main-blue //! [docs]: https://juntyr.github.io/pyo3-error/pyo3_error +//! +//! Unified error causality chains across Rust and Python using [`PyErrChain`]. + +use std::{borrow::Cow, error::Error, fmt, io}; + +use pyo3::{exceptions::PyException, intern, prelude::*, sync::GILOnceCell, types::IntoPyDict}; + +/// [`PyErrChain`] wraps a [`PyErr`] together with its causality chain. +/// +/// Unlike [`PyErr`], [`PyErrChain`]'s implementation of [`std::error::Error`] +/// provides access to the error cause through the [`std::error::Error::source`] +/// method. +/// +/// Note that since [`PyErr`]s can be readily cloned, the [`PyErrChain`] only +/// captures the causality chain at the time of construction. Calling +/// [`PyErr::set_cause`] on a clone of the wrapped error after construction will +/// thus not update the chain as captured by this [`PyErrChain`]. +pub struct PyErrChain { + err: PyErr, + cause: Option<Box<Self>>, +} + +impl PyErrChain { + /// Create a new [`PyErrChain`] from `err`. + /// + /// The error's causality chain, as expressed by + /// [`std::error::Error::source`], is translated into a [`PyErr::cause`] + /// chain. + /// + /// If any error in the chain is a [`PyErrChain`] or a [`PyErr`], it is + /// extracted directly. All other error types are translated into [`PyErr`]s + /// using [`PyException::new_err`] with `format!("{}", err)`. + /// + /// If you want to customize the translation from [`std::error::Error`] into + /// [`PyErr`], please use [`Self::new_with_downcaster`] instead. + #[must_use] + pub fn new<T: Error + 'static>(py: Python, err: T) -> Self { + Self::new_with_downcaster::<T, ErrorNoPyErr, DowncastToPyErr>(py, err) + } + + /// Create a new [`PyErrChain`] from `err` using a custom translator from + /// [`std::error::Error`] to [`PyErr`]. + /// + /// The error's causality chain, as expressed by + /// [`std::error::Error::source`], is translated into a [`PyErr::cause`] + /// chain. + /// + /// If any error in the chain is a [`PyErrChain`] or a [`PyErr`], it is + /// extracted directly. All other error types first attempt to be translated + /// into [`PyErr`]s using the [`PyErrDowncaster`] and [`PyErrMapper`]. As a + /// fallback, all remaining errors are translated into [`PyErr`]s using + /// [`PyException::new_err`] with `format!("{}", err)`. + #[must_use] + pub fn new_with_downcaster<E: Error + 'static, T: AnyErrorToPyErr, M: MapErrorToPyErr>( + py: Python, + err: E, + ) -> Self { + let err: Box<dyn Error + 'static> = Box::new(err); + + let err = match err.downcast::<Self>() { + Ok(err) => return *err, + Err(err) => err, + }; + + let err = match err.downcast::<PyErr>() { + Ok(err) => return Self::from(*err), + Err(err) => err, + }; + + let mut stack = Vec::new(); + + let mut source = err.source(); + let mut cause = None; + + while let Some(err) = source.take() { + if let Some(err) = err.downcast_ref::<Self>() { + let mut err = err.clone_ref(py); + cause = err.cause.take(); + stack.push(err); + break; + } + + if let Some(err) = err.downcast_ref::<PyErr>() { + let mut err = Self::from(err.clone_ref(py)); + cause = err.cause.take(); + stack.push(err); + break; + } + + source = err.source(); + stack.push(Self { + #[allow(clippy::option_if_let_else)] + err: match T::try_from_err_ref::<M>(py, err) { + Some(err) => err, + None => PyException::new_err(format!("{err}")), + }, + cause: None, + }); + } + + while let Some(mut err) = stack.pop() { + err.cause = cause.take(); + err.err.set_cause( + py, + err.cause + .as_deref() + .map(|cause| cause.as_err().clone_ref(py)), + ); + cause = Some(Box::new(err)); + } + + let err = match T::try_from_err::<M>(py, err) { + Ok(err) => err, + Err(err) => PyException::new_err(format!("{err}")), + }; + err.set_cause( + py, + cause.as_deref().map(|cause| cause.as_err().clone_ref(py)), + ); + + Self { err, cause } + } + + /// Clone the [`PyErrChain`]. + /// + /// This requires the GIL, which is why [`PyErrChain`] does not implement + /// [`Clone`]. + /// + /// Note that all elements of the cloned [`PyErrChain`] will be shared using + /// reference counting in Python with the existing [`PyErrChain`] `self`. + #[must_use] + pub fn clone_ref(&self, py: Python) -> Self { + Self { + err: self.err.clone_ref(py), + cause: self + .cause + .as_ref() + .map(|cause| Box::new(cause.clone_ref(py))), + } + } + + /// Get a reference to the wrapped [`PyErr`]. + /// + /// Note that while [`PyErr::set_cause`] can be called on the returned + /// [`PyErr`], the change in causality chain will not be reflected in + /// this [`PyErrChain`]. + #[must_use] + pub const fn as_err(&self) -> &PyErr { + &self.err + } + + /// Get a reference to the cause of the wrapped [`PyErr`]. + /// + /// Note that while [`PyErr::set_cause`] can be called on the returned + /// [`PyErr`], the change in causality chain will not be reflected in + /// this [`PyErrChain`]. + #[must_use] + pub fn cause(&self) -> Option<&PyErr> { + self.cause.as_deref().map(Self::as_err) + } +} + +impl fmt::Debug for PyErrChain { + fn fmt(&self, fmt: &mut fmt::Formatter) -> fmt::Result { + Python::with_gil(|py| { + let traceback = self.err.traceback_bound(py).map(|tb| { + tb.format() + .map_or(Cow::Borrowed("<traceback str() failed>"), |tb| { + Cow::Owned(tb) + }) + }); + + fmt.debug_struct("PyErrChain") + .field("type", &self.err.get_type_bound(py)) + .field("value", self.err.value_bound(py)) + .field("traceback", &traceback) + .field("cause", &self.cause) + .finish() + }) + } +} + +impl fmt::Display for PyErrChain { + #[inline] + fn fmt(&self, fmt: &mut fmt::Formatter) -> fmt::Result { + fmt::Display::fmt(&self.err, fmt) + } +} + +impl Error for PyErrChain { + fn source(&self) -> Option<&(dyn Error + 'static)> { + self.cause.as_deref().map(|cause| cause as &dyn Error) + } +} + +impl From<PyErr> for PyErrChain { + fn from(err: PyErr) -> Self { + Python::with_gil(|py| { + let mut stack = Vec::new(); + + let mut cause = err.cause(py); + + while let Some(err) = cause.take() { + cause = err.cause(py); + stack.push(Self { err, cause: None }); + } + + let mut cause = None; + + while let Some(mut err) = stack.pop() { + err.cause = cause.take(); + cause = Some(Box::new(err)); + } + + Self { err, cause } + }) + } +} + +impl From<PyErrChain> for PyErr { + fn from(err: PyErrChain) -> Self { + err.err + } +} + +/// Utility trait to try to translate from [`std::error::Error`] to [`PyErr`]. +/// +/// [`ErrorNoPyErr`] may be used to always fail at translating. +/// +/// [`IoErrorToPyErr`] may be used to translate [`std::io::Error`] to [`PyErr`]. +pub trait AnyErrorToPyErr { + /// Try to translate from a boxed `err` to a [`PyErr`], or return the `err`. + /// + /// When a strongly typed translation from some specific error type `E` to a + /// [`PyErr`] is attempted, [`MapErrorToPyErr::try_map`] should be used to allow + /// the mapper to test for `E` in addition to wrapped errors such as + /// `MyError<E>`. + /// + /// # Errors + /// + /// Returns the original `err` if translating to a [`PyErr`] failed. + fn try_from_err<T: MapErrorToPyErr>( + py: Python, + err: Box<dyn Error + 'static>, + ) -> Result<PyErr, Box<dyn Error + 'static>>; + + /// Try to translate from an `err` reference to a [`PyErr`], or return + /// [`None`]. + /// + /// When a strongly typed translation from some specific error type `E` to a + /// [`PyErr`] is attempted, [`MapErrorToPyErr::try_map_ref`] should be used to + /// allow the mapper to test for `E` in addition to wrapped errors such as + /// `MyError<E>`. + /// + fn try_from_err_ref<T: MapErrorToPyErr>( + py: Python, + err: &(dyn Error + 'static), + ) -> Option<PyErr>; +} -pub fn add(left: u64, right: u64) -> u64 { - left + right +/// Utility trait to try to translate via specific error types `E` implementing +/// [`std::error::Error`] and wrapped errors such as `MyError<E>` to [`PyErr`]s. +/// +/// [`DowncastToPyErr`] may be used to only try to translate via `E` using +/// downcasting. +pub trait MapErrorToPyErr { + /// Try to map from a boxed `err` via the specific error type `T` or wrapped + /// errors such as `MyError<E>` to a [`PyErr`], or return the `err`. + /// + /// The `map` function should be used to access the provided mapping from + /// `T` to [`PyErr`]. + /// + /// # Errors + /// + /// Returns the original `err` if mapping to a [`PyErr`] failed. + fn try_map<T: Error + 'static>( + py: Python, + err: Box<dyn Error + 'static>, + map: impl FnOnce(Box<T>) -> PyErr, + ) -> Result<PyErr, Box<dyn Error + 'static>>; + + /// Try to map from an `err` reference via the specific error type `T` or + /// wrapped errors such as `MyError<E>` to a [`PyErr`], or return [`None`]. + /// + /// The `map` function should be used to access the provided mapping from + /// `&T` to [`PyErr`]. + fn try_map_ref<T: Error + 'static>( + py: Python, + err: &(dyn Error + 'static), + map: impl FnOnce(&T) -> PyErr, + ) -> Option<PyErr>; +} + +/// Never attempt to translate any [`std::error::Error`] to [`PyErr`] when used +/// as [`AnyErrorToPyErr`]. +pub struct ErrorNoPyErr; + +impl AnyErrorToPyErr for ErrorNoPyErr { + #[inline] + fn try_from_err<T: MapErrorToPyErr>( + _py: Python, + err: Box<dyn Error + 'static>, + ) -> Result<PyErr, Box<dyn Error + 'static>> { + Err(err) + } + + #[inline] + fn try_from_err_ref<T: MapErrorToPyErr>( + _py: Python, + _err: &(dyn Error + 'static), + ) -> Option<PyErr> { + None + } +} + +/// Translate [`std::io::Error`] to [`PyErr`] when used as [`AnyErrorToPyErr`]. +pub struct IoErrorToPyErr; + +impl AnyErrorToPyErr for IoErrorToPyErr { + fn try_from_err<T: MapErrorToPyErr>( + py: Python, + err: Box<dyn Error + 'static>, + ) -> Result<PyErr, Box<dyn Error + 'static>> { + T::try_map(py, err, |err: Box<io::Error>| { + // TODO: replace with io::Error::downcast once MSRV >= 1.79 + #[allow(clippy::redundant_closure_for_method_calls)] + if err.get_ref().map_or(false, |err| err.is::<PyErrChain>()) { + #[allow(clippy::unwrap_used)] // we have just checked that all unwraps succeed + let err: Box<PyErrChain> = err.into_inner().unwrap().downcast().unwrap(); + return PyErr::from(*err); + } + + PyErr::from(*err) + }) + } + + fn try_from_err_ref<T: MapErrorToPyErr>( + py: Python, + err: &(dyn Error + 'static), + ) -> Option<PyErr> { + T::try_map_ref(py, err, |err: &io::Error| { + if let Some(err) = err.get_ref() { + if let Some(err) = err.downcast_ref::<PyErr>() { + return err.clone_ref(py); + } + + if let Some(err) = err.downcast_ref::<PyErrChain>() { + return err.as_err().clone_ref(py); + } + } + + PyErr::from(io::Error::new(err.kind(), format!("{err}"))) + }) + } +} + +/// Try to map a [`std::error::Error`] via a specific error type `T` to a +/// [`PyErr`] by downcasting when used as [`MapErrorToPyErr`]; +pub struct DowncastToPyErr; + +impl MapErrorToPyErr for DowncastToPyErr { + fn try_map<T: Error + 'static>( + _py: Python, + err: Box<dyn Error + 'static>, + map: impl FnOnce(Box<T>) -> PyErr, + ) -> Result<PyErr, Box<dyn Error + 'static>> { + err.downcast().map(map) + } + + fn try_map_ref<T: Error + 'static>( + _py: Python, + err: &(dyn Error + 'static), + map: impl FnOnce(&T) -> PyErr, + ) -> Option<PyErr> { + err.downcast_ref().map(map) + } +} + +#[allow(clippy::missing_panics_doc)] +/// Utility function to add a traceback with the error's `file`, `line`, and +/// `column` location information to the `err`. +/// +/// This function may be used when implementing [`AnyErrorToPyErr`] or +/// [`MapErrorToPyErr`] to pythonize any available error location information. +#[must_use] +pub fn err_with_location(py: Python, err: PyErr, file: &str, line: u32, column: u32) -> PyErr { + const RAISE: &str = "raise err"; + + static COMPILE: GILOnceCell<Py<PyAny>> = GILOnceCell::new(); + static EXEC: GILOnceCell<Py<PyAny>> = GILOnceCell::new(); + + let _ = column; + + #[allow(clippy::expect_used)] // failure is a Python bug + let compile = COMPILE + .get_or_try_init(py, || -> Result<Py<PyAny>, PyErr> { + Ok(py.import_bound("builtins")?.getattr("compile")?.unbind()) + }) + .expect("Python does not provide a compile() function") + .bind(py); + + #[allow(clippy::expect_used)] // failure is a Python bug + let exec = EXEC + .get_or_try_init(py, || -> Result<Py<PyAny>, PyErr> { + Ok(py.import_bound("builtins")?.getattr("exec")?.unbind()) + }) + .expect("Python does not provide an exec() function") + .bind(py); + + let mut code = String::with_capacity((line as usize) + RAISE.len()); + for _ in 1..line { + code.push('\n'); + } + code.push_str(RAISE); + + #[allow(clippy::expect_used)] // failure is a Python bug + let code = compile + .call1((code, file, intern!(py, "exec"))) + .expect("failed to compile PyErr location helper"); + let globals = [(intern!(py, "err"), err)].into_py_dict_bound(py); + + #[allow(clippy::expect_used)] // failure is a Python bug + let err = exec.call1((code, globals)).expect_err("raise must raise"); + err } #[cfg(test)] @@ -55,9 +995,107 @@ use super::*; #[test] - fn it_works() { - let result = add(2, 2); - assert_eq!(result, 4); + fn python_cause() { + Python::with_gil(|py| { + let err = py + .run_bound( + r#" +try: + try: + raise Exception("source") + except Exception as err: + raise IndexError("middle") from err +except Exception as err: + raise LookupError("top") from err +"#, + None, + None, + ) + .expect_err("raise must raise"); + + let err = PyErrChain::new(py, err); + assert_eq!(format!("{err}"), "LookupError: top"); + + let err = err.source().expect("must have source"); + assert_eq!(format!("{err}"), "IndexError: middle"); + + let err = err.source().expect("must have source"); + assert_eq!(format!("{err}"), "Exception: source"); + + assert!(err.source().is_none()); + }) + } + + #[test] + fn rust_source() { + #[derive(Debug)] + struct MyErr { + msg: &'static str, + source: Option<Box<Self>>, + } + + impl fmt::Display for MyErr { + fn fmt(&self, fmt: &mut fmt::Formatter) -> fmt::Result { + fmt.write_str(self.msg) + } + } + + impl Error for MyErr { + fn source(&self) -> Option<&(dyn Error + 'static)> { + match &self.source { + None => None, + Some(source) => Some(&**source as &dyn Error), + } + } + } + + Python::with_gil(|py| { + let err = PyErrChain::new( + py, + MyErr { + msg: "top", + source: Some(Box::new(MyErr { + msg: "middle", + source: Some(Box::new(MyErr { + msg: "source", + source: None, + })), + })), + }, + ); + + let source = err.source().expect("must have source"); + let source = source.source().expect("must have source"); + assert!(source.source().is_none()); + + let err = PyErr::from(err); + assert_eq!(format!("{err}"), "Exception: top"); + + let err = err.cause(py).expect("must have cause"); + assert_eq!(format!("{err}"), "Exception: middle"); + + let err = err.cause(py).expect("must have cause"); + assert_eq!(format!("{err}"), "Exception: source"); + + assert!(err.cause(py).is_none()); + }) + } + + #[test] + fn err_location() { + Python::with_gil(|py| { + let err = err_with_location(py, PyException::new_err("oh no"), "foo.rs", 27, 15); + + assert_eq!(format!("{err}"), "Exception: oh no"); + assert_eq!( + err.traceback_bound(py) + .expect("must have traceback") + .format() + .expect("traceback must be formattable"), + "Traceback (most recent call last):\n File \"foo.rs\", line 27, in <module>\n", + ); + assert!(err.cause(py).is_none()); + }) } }