pyo3_error/
lib.rs

1//! [![CI Status]][workflow] [![MSRV]][repo] [![Latest Version]][crates.io] [![Rust Doc Crate]][docs.rs] [![Rust Doc Main]][docs]
2//!
3//! [CI Status]: https://img.shields.io/github/actions/workflow/status/juntyr/pyo3-error/ci.yml?branch=main
4//! [workflow]: https://github.com/juntyr/pyo3-error/actions/workflows/ci.yml?query=branch%3Amain
5//!
6//! [MSRV]: https://img.shields.io/badge/MSRV-1.63.0-blue
7//! [repo]: https://github.com/juntyr/pyo3-error
8//!
9//! [Latest Version]: https://img.shields.io/crates/v/pyo3-error
10//! [crates.io]: https://crates.io/crates/pyo3-error
11//!
12//! [Rust Doc Crate]: https://img.shields.io/docsrs/pyo3-error
13//! [docs.rs]: https://docs.rs/pyo3-error/
14//!
15//! [Rust Doc Main]: https://img.shields.io/badge/docs-main-blue
16//! [docs]: https://juntyr.github.io/pyo3-error/pyo3_error
17//!
18//! Unified error causality chains across Rust and Python using [`PyErrChain`].
19
20use std::{borrow::Cow, error::Error, fmt, io};
21
22use pyo3::{exceptions::PyException, intern, prelude::*, sync::GILOnceCell, types::IntoPyDict};
23
24/// [`PyErrChain`] wraps a [`PyErr`] together with its causality chain.
25///
26/// Unlike [`PyErr`], [`PyErrChain`]'s implementation of [`std::error::Error`]
27/// provides access to the error cause through the [`std::error::Error::source`]
28/// method.
29///
30/// Note that since [`PyErr`]s can be readily cloned, the [`PyErrChain`] only
31/// captures the causality chain at the time of construction. Calling
32/// [`PyErr::set_cause`] on a clone of the wrapped error after construction will
33/// thus not update the chain as captured by this [`PyErrChain`].
34pub struct PyErrChain {
35    err: PyErr,
36    cause: Option<Box<Self>>,
37}
38
39impl PyErrChain {
40    /// Create a new [`PyErrChain`] from `err`.
41    ///
42    /// The error's causality chain, as expressed by
43    /// [`std::error::Error::source`], is translated into a [`PyErr::cause`]
44    /// chain.
45    ///
46    /// If any error in the chain is a [`PyErrChain`] or a [`PyErr`], it is
47    /// extracted directly. All other error types are translated into [`PyErr`]s
48    /// using [`PyException::new_err`] with `format!("{}", err)`.
49    ///
50    /// If you want to customize the translation from [`std::error::Error`] into
51    /// [`PyErr`], please use [`Self::new_with_translator`] instead.
52    ///
53    /// This constructor is equivalent to chaining [`Self::pyerr_from_err`] with
54    /// [`Self::from_pyerr`].
55    #[must_use]
56    #[inline]
57    pub fn new<T: Into<Box<dyn Error + 'static>>>(py: Python, err: T) -> Self {
58        Self::from_pyerr(py, Self::pyerr_from_err(py, err))
59    }
60
61    /// Create a new [`PyErrChain`] from `err` using a custom translator from
62    /// [`std::error::Error`] to [`PyErr`].
63    ///
64    /// The error's causality chain, as expressed by
65    /// [`std::error::Error::source`], is translated into a [`PyErr::cause`]
66    /// chain.
67    ///
68    /// If any error in the chain is a [`PyErrChain`] or a [`PyErr`], it is
69    /// extracted directly. All other error types first attempt to be translated
70    /// into [`PyErr`]s using the [`AnyErrorToPyErr`] and [`MapErrorToPyErr`].
71    /// As a fallback, all remaining errors are translated into [`PyErr`]s using
72    /// [`PyException::new_err`] with `format!("{}", err)`.
73    ///
74    /// This constructor is equivalent to chaining
75    /// [`Self::pyerr_from_err_with_translator`] with [`Self::from_pyerr`].
76    #[must_use]
77    #[inline]
78    pub fn new_with_translator<
79        E: Into<Box<dyn Error + 'static>>,
80        T: AnyErrorToPyErr,
81        M: MapErrorToPyErr,
82    >(
83        py: Python,
84        err: E,
85    ) -> Self {
86        Self::from_pyerr(py, Self::pyerr_from_err_with_translator::<E, T, M>(py, err))
87    }
88
89    /// Transform an `err` implementing [`std::error::Error`] into a [`PyErr`]
90    /// that preserves the error's causality chain.
91    ///
92    /// The error's causality chain, as expressed by
93    /// [`std::error::Error::source`], is translated into a [`PyErr::cause`]
94    /// chain.
95    ///
96    /// If any error in the chain is a [`PyErrChain`] or a [`PyErr`], it is
97    /// extracted directly. All other error types are translated into [`PyErr`]s
98    /// using [`PyException::new_err`] with `format!("{}", err)`.
99    ///
100    /// If you want to customize the translation from [`std::error::Error`] into
101    /// [`PyErr`], please use [`Self::pyerr_from_err_with_translator`] instead.
102    #[must_use]
103    #[inline]
104    pub fn pyerr_from_err<T: Into<Box<dyn Error + 'static>>>(py: Python, err: T) -> PyErr {
105        Self::pyerr_from_err_with_translator::<T, ErrorNoPyErr, DowncastToPyErr>(py, err)
106    }
107
108    /// Transform an `err` implementing [`std::error::Error`] into a [`PyErr`]
109    /// that preserves the error's causality chain using a custom translator.
110    ///
111    /// The error's causality chain, as expressed by
112    /// [`std::error::Error::source`], is translated into a [`PyErr::cause`]
113    /// chain.
114    ///
115    /// If any error in the chain is a [`PyErrChain`] or a [`PyErr`], it is
116    /// extracted directly. All other error types first attempt to be translated
117    /// into [`PyErr`]s using the [`AnyErrorToPyErr`] and [`MapErrorToPyErr`].
118    /// As a fallback, all remaining errors are translated into [`PyErr`]s using
119    /// [`PyException::new_err`] with `format!("{}", err)`.
120    #[must_use]
121    pub fn pyerr_from_err_with_translator<
122        E: Into<Box<dyn Error + 'static>>,
123        T: AnyErrorToPyErr,
124        M: MapErrorToPyErr,
125    >(
126        py: Python,
127        err: E,
128    ) -> PyErr {
129        let err: Box<dyn Error + 'static> = err.into();
130
131        let err = match M::try_map(py, err, |err: Box<Self>| err.into_pyerr()) {
132            Ok(err) => return err,
133            Err(err) => err,
134        };
135        let err = match M::try_map(py, err, |err: Box<PyErr>| *err) {
136            Ok(err) => return err,
137            Err(err) => err,
138        };
139
140        let mut chain = Vec::new();
141
142        let mut source = err.source();
143        let mut cause = None;
144
145        while let Some(err) = source.take() {
146            if let Some(err) = M::try_map_ref(py, err, |err: &Self| err.as_pyerr().clone_ref(py)) {
147                cause = err.cause(py);
148                chain.push(err);
149                break;
150            }
151            if let Some(err) = M::try_map_ref(py, err, |err: &PyErr| err.clone_ref(py)) {
152                cause = err.cause(py);
153                chain.push(err);
154                break;
155            }
156
157            source = err.source();
158
159            #[allow(clippy::option_if_let_else)]
160            chain.push(match T::try_from_err_ref::<M>(py, err) {
161                Some(err) => err,
162                None => PyException::new_err(format!("{err}")),
163            });
164        }
165
166        while let Some(err) = chain.pop() {
167            err.set_cause(py, cause.take());
168            cause = Some(err);
169        }
170
171        let err = match T::try_from_err::<M>(py, err) {
172            Ok(err) => err,
173            Err(err) => PyException::new_err(format!("{err}")),
174        };
175        err.set_cause(py, cause);
176
177        err
178    }
179
180    /// Wrap a [`PyErr`] and capture its causality chain, as expressed by
181    /// [`PyErr::cause`].
182    #[must_use]
183    pub fn from_pyerr(py: Python, err: PyErr) -> Self {
184        let mut chain = Vec::new();
185
186        let mut cause = err.cause(py);
187
188        while let Some(err) = cause.take() {
189            cause = err.cause(py);
190            chain.push(Self { err, cause: None });
191        }
192
193        let mut cause = None;
194
195        while let Some(mut err) = chain.pop() {
196            err.cause = cause.take();
197            cause = Some(Box::new(err));
198        }
199
200        Self { err, cause }
201    }
202
203    /// Extract the wrapped [`PyErr`].
204    #[must_use]
205    pub fn into_pyerr(self) -> PyErr {
206        self.err
207    }
208
209    /// Get a reference to the wrapped [`PyErr`].
210    ///
211    /// Note that while [`PyErr::set_cause`] can be called on the returned
212    /// [`PyErr`], the change in causality chain will not be reflected in
213    /// this [`PyErrChain`].
214    #[must_use]
215    pub const fn as_pyerr(&self) -> &PyErr {
216        &self.err
217    }
218
219    /// Get a reference to the cause of the wrapped [`PyErr`].
220    ///
221    /// Note that while [`PyErr::set_cause`] can be called on the returned
222    /// [`PyErr`], the change in causality chain will not be reflected in
223    /// this [`PyErrChain`].
224    #[must_use]
225    pub fn cause(&self) -> Option<&PyErr> {
226        self.cause.as_deref().map(Self::as_pyerr)
227    }
228
229    /// Clone the [`PyErrChain`].
230    ///
231    /// This requires the GIL, which is why [`PyErrChain`] does not implement
232    /// [`Clone`].
233    ///
234    /// Note that all elements of the cloned [`PyErrChain`] will be shared using
235    /// reference counting in Python with the existing [`PyErrChain`] `self`.
236    #[must_use]
237    pub fn clone_ref(&self, py: Python) -> Self {
238        Self {
239            err: self.err.clone_ref(py),
240            cause: self
241                .cause
242                .as_ref()
243                .map(|cause| Box::new(cause.clone_ref(py))),
244        }
245    }
246}
247
248impl fmt::Debug for PyErrChain {
249    fn fmt(&self, fmt: &mut fmt::Formatter) -> fmt::Result {
250        Python::with_gil(|py| {
251            let traceback = self.err.traceback(py).map(|tb| {
252                tb.format()
253                    .map_or(Cow::Borrowed("<traceback str() failed>"), |tb| {
254                        Cow::Owned(tb)
255                    })
256            });
257
258            fmt.debug_struct("PyErrChain")
259                .field("type", &self.err.get_type(py))
260                .field("value", self.err.value(py))
261                .field("traceback", &traceback)
262                .field("cause", &self.cause)
263                .finish()
264        })
265    }
266}
267
268impl fmt::Display for PyErrChain {
269    #[inline]
270    fn fmt(&self, fmt: &mut fmt::Formatter) -> fmt::Result {
271        fmt::Display::fmt(&self.err, fmt)
272    }
273}
274
275impl Error for PyErrChain {
276    fn source(&self) -> Option<&(dyn Error + 'static)> {
277        self.cause.as_deref().map(|cause| cause as &dyn Error)
278    }
279}
280
281impl From<PyErr> for PyErrChain {
282    fn from(err: PyErr) -> Self {
283        Python::with_gil(|py| Self::from_pyerr(py, err))
284    }
285}
286
287impl From<PyErrChain> for PyErr {
288    fn from(err: PyErrChain) -> Self {
289        err.into_pyerr()
290    }
291}
292
293/// Utility trait to try to translate from [`std::error::Error`] to [`PyErr`].
294///
295/// [`ErrorNoPyErr`] may be used to always fail at translating.
296///
297/// [`IoErrorToPyErr`] may be used to translate [`std::io::Error`] to [`PyErr`].
298pub trait AnyErrorToPyErr {
299    /// Try to translate from a boxed `err` to a [`PyErr`], or return the `err`.
300    ///
301    /// When a strongly typed translation from some specific error type `E` to a
302    /// [`PyErr`] is attempted, [`MapErrorToPyErr::try_map`] should be used to allow
303    /// the mapper to test for `E` in addition to wrapped errors such as
304    /// `MyError<E>`.
305    ///
306    /// # Errors
307    ///
308    /// Returns the original `err` if translating to a [`PyErr`] failed.
309    fn try_from_err<T: MapErrorToPyErr>(
310        py: Python,
311        err: Box<dyn Error + 'static>,
312    ) -> Result<PyErr, Box<dyn Error + 'static>>;
313
314    /// Try to translate from an `err` reference to a [`PyErr`], or return
315    /// [`None`].
316    ///
317    /// When a strongly typed translation from some specific error type `E` to a
318    /// [`PyErr`] is attempted, [`MapErrorToPyErr::try_map_ref`] should be used to
319    /// allow the mapper to test for `E` in addition to wrapped errors such as
320    /// `MyError<E>`.
321    ///
322    fn try_from_err_ref<T: MapErrorToPyErr>(
323        py: Python,
324        err: &(dyn Error + 'static),
325    ) -> Option<PyErr>;
326}
327
328/// Utility trait to try to translate via specific error types `E` implementing
329/// [`std::error::Error`] and wrapped errors such as `MyError<E>` to [`PyErr`]s.
330///
331/// [`DowncastToPyErr`] may be used to only try to translate via `E` using
332/// downcasting.
333pub trait MapErrorToPyErr {
334    /// Try to map from a boxed `err` via the specific error type `T` or wrapped
335    /// errors such as `MyError<E>` to a [`PyErr`], or return the `err`.
336    ///
337    /// The `map` function should be used to access the provided mapping from
338    /// `T` to [`PyErr`].
339    ///
340    /// # Errors
341    ///
342    /// Returns the original `err` if mapping to a [`PyErr`] failed.
343    fn try_map<T: Error + 'static>(
344        py: Python,
345        err: Box<dyn Error + 'static>,
346        map: impl FnOnce(Box<T>) -> PyErr,
347    ) -> Result<PyErr, Box<dyn Error + 'static>>;
348
349    /// Try to map from a boxed `err` via the specific error type `T` or wrapped
350    /// errors such as `MyError<E>` to a [`PyErr`], or return the `err`.
351    ///
352    /// The `map` function should be used to access the provided mapping from
353    /// `T` to [`PyErr`].
354    ///
355    /// # Errors
356    ///
357    /// Returns the original `err` if mapping to a [`PyErr`] failed.
358    fn try_map_send_sync<T: Error + 'static>(
359        py: Python,
360        err: Box<dyn Error + Send + Sync + 'static>,
361        map: impl FnOnce(Box<T>) -> PyErr,
362    ) -> Result<PyErr, Box<dyn Error + Send + Sync + 'static>>;
363
364    /// Try to map from an `err` reference via the specific error type `T` or
365    /// wrapped errors such as `MyError<E>` to a [`PyErr`], or return [`None`].
366    ///
367    /// The `map` function should be used to access the provided mapping from
368    /// `&T` to [`PyErr`].
369    fn try_map_ref<T: Error + 'static>(
370        py: Python,
371        err: &(dyn Error + 'static),
372        map: impl FnOnce(&T) -> PyErr,
373    ) -> Option<PyErr>;
374}
375
376/// Never attempt to translate any [`std::error::Error`] to [`PyErr`] when used
377/// as [`AnyErrorToPyErr`].
378pub struct ErrorNoPyErr;
379
380impl AnyErrorToPyErr for ErrorNoPyErr {
381    #[inline]
382    fn try_from_err<T: MapErrorToPyErr>(
383        _py: Python,
384        err: Box<dyn Error + 'static>,
385    ) -> Result<PyErr, Box<dyn Error + 'static>> {
386        Err(err)
387    }
388
389    #[inline]
390    fn try_from_err_ref<T: MapErrorToPyErr>(
391        _py: Python,
392        _err: &(dyn Error + 'static),
393    ) -> Option<PyErr> {
394        None
395    }
396}
397
398/// Translate [`std::io::Error`] to [`PyErr`] when used as [`AnyErrorToPyErr`].
399pub struct IoErrorToPyErr;
400
401impl AnyErrorToPyErr for IoErrorToPyErr {
402    fn try_from_err<T: MapErrorToPyErr>(
403        py: Python,
404        err: Box<dyn Error + 'static>,
405    ) -> Result<PyErr, Box<dyn Error + 'static>> {
406        T::try_map(py, err, |err: Box<io::Error>| {
407            let kind = err.kind();
408
409            if err.get_ref().is_some() {
410                #[allow(clippy::unwrap_used)] // we just checked that it will be `Some(_)`
411                let err = err.into_inner().unwrap();
412
413                let err = match T::try_map_send_sync(py, err, |err: Box<PyErr>| *err) {
414                    Ok(err) => return err,
415                    Err(err) => err,
416                };
417
418                let err =
419                    match T::try_map_send_sync(py, err, |err: Box<PyErrChain>| err.into_pyerr()) {
420                        Ok(err) => return err,
421                        Err(err) => err,
422                    };
423
424                return PyErr::from(io::Error::new(kind, err));
425            }
426
427            PyErr::from(*err)
428        })
429    }
430
431    fn try_from_err_ref<T: MapErrorToPyErr>(
432        py: Python,
433        err: &(dyn Error + 'static),
434    ) -> Option<PyErr> {
435        T::try_map_ref(py, err, |err: &io::Error| {
436            if let Some(err) = err.get_ref() {
437                if let Some(err) = T::try_map_ref(py, err, |err: &PyErr| err.clone_ref(py)) {
438                    return err;
439                }
440
441                if let Some(err) =
442                    T::try_map_ref(py, err, |err: &PyErrChain| err.as_pyerr().clone_ref(py))
443                {
444                    return err;
445                }
446            }
447
448            PyErr::from(io::Error::new(err.kind(), format!("{err}")))
449        })
450    }
451}
452
453/// Try to map a [`std::error::Error`] via a specific error type `T` to a
454/// [`PyErr`] by downcasting when used as [`MapErrorToPyErr`];
455pub struct DowncastToPyErr;
456
457impl MapErrorToPyErr for DowncastToPyErr {
458    fn try_map<T: Error + 'static>(
459        _py: Python,
460        err: Box<dyn Error + 'static>,
461        map: impl FnOnce(Box<T>) -> PyErr,
462    ) -> Result<PyErr, Box<dyn Error + 'static>> {
463        err.downcast().map(map)
464    }
465
466    fn try_map_send_sync<T: Error + 'static>(
467        _py: Python,
468        err: Box<dyn Error + Send + Sync + 'static>,
469        map: impl FnOnce(Box<T>) -> PyErr,
470    ) -> Result<PyErr, Box<dyn Error + Send + Sync + 'static>> {
471        err.downcast().map(map)
472    }
473
474    fn try_map_ref<T: Error + 'static>(
475        _py: Python,
476        err: &(dyn Error + 'static),
477        map: impl FnOnce(&T) -> PyErr,
478    ) -> Option<PyErr> {
479        err.downcast_ref().map(map)
480    }
481}
482
483#[allow(clippy::missing_panics_doc)]
484/// Utility function to add a traceback with the error's `file`, `line`, and
485/// `column` location information to the `err`.
486///
487/// This function may be used when implementing [`AnyErrorToPyErr`] or
488/// [`MapErrorToPyErr`] to pythonize any available error location information.
489#[must_use]
490pub fn err_with_location(py: Python, err: PyErr, file: &str, line: u32, column: u32) -> PyErr {
491    const RAISE: &str = "raise err";
492
493    static COMPILE: GILOnceCell<Py<PyAny>> = GILOnceCell::new();
494    static EXEC: GILOnceCell<Py<PyAny>> = GILOnceCell::new();
495
496    let _ = column;
497
498    #[allow(clippy::expect_used)] // failure is a Python bug
499    let compile = COMPILE
500        .import(py, "builtins", "compile")
501        .expect("Python does not provide a compile() function");
502    #[allow(clippy::expect_used)] // failure is a Python bug
503    let exec = EXEC
504        .import(py, "builtins", "exec")
505        .expect("Python does not provide an exec() function");
506
507    let mut code = String::with_capacity((line as usize) + RAISE.len());
508    for _ in 1..line {
509        code.push('\n');
510    }
511    code.push_str(RAISE);
512
513    #[allow(clippy::expect_used)] // failure is a Python bug
514    let code = compile
515        .call1((code, file, intern!(py, "exec")))
516        .expect("failed to compile PyErr location helper");
517    #[allow(clippy::expect_used)] // failure is a Python bug
518    let globals = [(intern!(py, "err"), err)]
519        .into_py_dict(py)
520        .expect("failed to create a dict(err=...)");
521
522    #[allow(clippy::expect_used)] // failure is a Python bug
523    let err = exec.call1((code, globals)).expect_err("raise must raise");
524    err
525}
526
527#[cfg(test)]
528mod tests {
529    use super::*;
530
531    #[test]
532    fn python_cause() {
533        Python::with_gil(|py| {
534            let err = py
535                .run(
536                    &std::ffi::CString::new(
537                        r#"
538try:
539    try:
540        raise Exception("source")
541    except Exception as err:
542        raise IndexError("middle") from err
543except Exception as err:
544    raise LookupError("top") from err
545"#,
546                    )
547                    .unwrap(),
548                    None,
549                    None,
550                )
551                .expect_err("raise must raise");
552
553            let err = PyErrChain::new(py, err);
554            assert_eq!(format!("{err}"), "LookupError: top");
555
556            let err = err.source().expect("must have source");
557            assert_eq!(format!("{err}"), "IndexError: middle");
558
559            let err = err.source().expect("must have source");
560            assert_eq!(format!("{err}"), "Exception: source");
561
562            assert!(err.source().is_none());
563        })
564    }
565
566    #[test]
567    fn rust_source() {
568        #[derive(Debug)]
569        struct MyErr {
570            msg: &'static str,
571            source: Option<Box<Self>>,
572        }
573
574        impl fmt::Display for MyErr {
575            fn fmt(&self, fmt: &mut fmt::Formatter) -> fmt::Result {
576                fmt.write_str(self.msg)
577            }
578        }
579
580        impl Error for MyErr {
581            fn source(&self) -> Option<&(dyn Error + 'static)> {
582                match &self.source {
583                    None => None,
584                    Some(source) => Some(&**source as &dyn Error),
585                }
586            }
587        }
588
589        Python::with_gil(|py| {
590            let err = PyErrChain::new(
591                py,
592                MyErr {
593                    msg: "top",
594                    source: Some(Box::new(MyErr {
595                        msg: "middle",
596                        source: Some(Box::new(MyErr {
597                            msg: "source",
598                            source: None,
599                        })),
600                    })),
601                },
602            );
603
604            let source = err.source().expect("must have source");
605            let source = source.source().expect("must have source");
606            assert!(source.source().is_none());
607
608            let err = PyErr::from(err);
609            assert_eq!(format!("{err}"), "Exception: top");
610
611            let err = err.cause(py).expect("must have cause");
612            assert_eq!(format!("{err}"), "Exception: middle");
613
614            let err = err.cause(py).expect("must have cause");
615            assert_eq!(format!("{err}"), "Exception: source");
616
617            assert!(err.cause(py).is_none());
618        })
619    }
620
621    #[test]
622    fn err_location() {
623        Python::with_gil(|py| {
624            let err = err_with_location(py, PyException::new_err("oh no"), "foo.rs", 27, 15);
625
626            // check the message, location traceback, and cause for the root error
627            assert_eq!(format!("{err}"), "Exception: oh no");
628            assert_eq!(
629                err.traceback(py)
630                    .expect("must have traceback")
631                    .format()
632                    .expect("traceback must be formattable"),
633                r#"Traceback (most recent call last):
634  File "foo.rs", line 27, in <module>
635"#,
636            );
637            assert!(err.cause(py).is_none());
638
639            // add an extra level of location traceback to the root error
640            let err = err_with_location(py, err, "bar.rs", 24, 18);
641
642            // create a new top-level error, caused by the root error
643            let top = PyException::new_err("oh yes");
644            top.set_cause(py, Some(err));
645            let err = err_with_location(py, top, "baz.rs", 41, 1);
646
647            // check the message and location traceback for the top-level error
648            assert_eq!(format!("{err}"), "Exception: oh yes");
649            assert_eq!(
650                err.traceback(py)
651                    .expect("must have traceback")
652                    .format()
653                    .expect("traceback must be formattable"),
654                r#"Traceback (most recent call last):
655  File "baz.rs", line 41, in <module>
656"#,
657            );
658
659            // ensure that the top-level error has a cause
660            let cause = err.cause(py).expect("must have a cause");
661
662            // check the message, extended location traceback, and cause for the root error
663            assert_eq!(format!("{cause}"), "Exception: oh no");
664            assert_eq!(
665                cause
666                    .traceback(py)
667                    .expect("must have traceback")
668                    .format()
669                    .expect("traceback must be formattable"),
670                r#"Traceback (most recent call last):
671  File "bar.rs", line 24, in <module>
672  File "foo.rs", line 27, in <module>
673"#,
674            );
675            assert!(cause.cause(py).is_none());
676        })
677    }
678
679    #[test]
680    fn anyhow() {
681        Python::with_gil(|py| {
682            let err = anyhow::anyhow!("source").context("middle").context("top");
683
684            let err = PyErrChain::new(py, err);
685            assert_eq!(format!("{err}"), "Exception: top");
686
687            let err = err.source().expect("must have source");
688            assert_eq!(format!("{err}"), "Exception: middle");
689
690            let err = err.source().expect("must have source");
691            assert_eq!(format!("{err}"), "Exception: source");
692
693            assert!(err.source().is_none());
694        })
695    }
696}