Skip to main content

pyo3/types/
traceback.rs

1use crate::err::{error_on_minusone, PyResult};
2use crate::platform::prelude::*;
3use crate::types::{any::PyAnyMethods, string::PyStringMethods, PyString};
4use crate::{ffi, Bound, PyAny};
5#[cfg(RustPython)]
6use crate::{
7    sync::PyOnceLock,
8    types::{PyType, PyTypeMethods},
9    Py,
10};
11#[cfg(all(not(Py_LIMITED_API), not(PyPy), not(GraalPy)))]
12use crate::{types::PyFrame, PyTypeCheck, Python};
13
14/// Represents a Python traceback.
15///
16/// Values of this type are accessed via PyO3's smart pointers, e.g. as
17/// [`Py<PyTraceback>`][crate::Py] or [`Bound<'py, PyTraceback>`][Bound].
18///
19/// For APIs available on traceback objects, see the [`PyTracebackMethods`] trait which is implemented for
20/// [`Bound<'py, PyTraceback>`][Bound].
21#[repr(transparent)]
22pub struct PyTraceback(PyAny);
23
24#[cfg(not(RustPython))]
25pyobject_native_type_core!(
26    PyTraceback,
27    pyobject_native_static_type_object!(ffi::PyTraceBack_Type),
28    "builtins",
29    "traceback",
30    #checkfunction=ffi::PyTraceBack_Check
31);
32
33#[cfg(RustPython)]
34pyobject_native_type_core!(
35    PyTraceback,
36    |py| {
37        static TYPE: PyOnceLock<Py<PyType>> = PyOnceLock::new();
38        TYPE.import(py, "types", "TracebackType").unwrap().as_type_ptr()
39    },
40    "builtins",
41    "traceback",
42    #checkfunction=ffi::PyTraceBack_Check
43);
44
45impl PyTraceback {
46    /// Creates a new traceback object from the given frame.
47    ///
48    /// The `next` is the next traceback in the direction of where the exception was raised
49    /// or `None` if this is the last frame in the traceback.
50    #[cfg(all(not(Py_LIMITED_API), not(PyPy), not(GraalPy)))]
51    pub fn new<'py>(
52        py: Python<'py>,
53        next: Option<Bound<'py, PyTraceback>>,
54        frame: Bound<'py, PyFrame>,
55        instruction_index: i32,
56        line_number: i32,
57    ) -> PyResult<Bound<'py, PyTraceback>> {
58        unsafe {
59            Ok(PyTraceback::classinfo_object(py)
60                .call1((next, frame, instruction_index, line_number))?
61                .cast_into_unchecked())
62        }
63    }
64}
65
66/// Implementation of functionality for [`PyTraceback`].
67///
68/// These methods are defined for the `Bound<'py, PyTraceback>` smart pointer, so to use method call
69/// syntax these methods are separated into a trait, because stable Rust does not yet support
70/// `arbitrary_self_types`.
71#[doc(alias = "PyTraceback")]
72pub trait PyTracebackMethods<'py>: crate::sealed::Sealed {
73    /// Formats the traceback as a string.
74    ///
75    /// This does not include the exception type and value. The exception type and value can be
76    /// formatted using the `Display` implementation for `PyErr`.
77    ///
78    /// # Example
79    ///
80    /// The following code formats a Python traceback and exception pair from Rust:
81    ///
82    /// ```rust
83    /// # use pyo3::{Python, PyResult, prelude::PyTracebackMethods, ffi::c_str};
84    /// # let result: PyResult<()> =
85    /// Python::attach(|py| {
86    ///     let err = py
87    ///         .run(c"raise Exception('banana')", None, None)
88    ///         .expect_err("raise will create a Python error");
89    ///
90    ///     let traceback = err.traceback(py).expect("raised exception will have a traceback");
91    ///     assert_eq!(
92    ///         format!("{}{}", traceback.format()?, err),
93    ///         "\
94    /// Traceback (most recent call last):
95    ///   File \"<string>\", line 1, in <module>
96    /// Exception: banana\
97    /// "
98    ///     );
99    ///     Ok(())
100    /// })
101    /// # ;
102    /// # result.expect("example failed");
103    /// ```
104    fn format(&self) -> PyResult<String>;
105}
106
107impl<'py> PyTracebackMethods<'py> for Bound<'py, PyTraceback> {
108    fn format(&self) -> PyResult<String> {
109        let py = self.py();
110        let string_io = py
111            .import(intern!(py, "io"))?
112            .getattr(intern!(py, "StringIO"))?
113            .call0()?;
114        let result = unsafe { ffi::PyTraceBack_Print(self.as_ptr(), string_io.as_ptr()) };
115        error_on_minusone(py, result)?;
116        let formatted = string_io
117            .getattr(intern!(py, "getvalue"))?
118            .call0()?
119            .cast::<PyString>()?
120            .to_cow()?
121            .into_owned();
122        Ok(formatted)
123    }
124}
125
126#[cfg(test)]
127mod tests {
128    use super::*;
129    use crate::IntoPyObject;
130    use crate::{
131        types::{dict::PyDictMethods, PyDict},
132        PyErr, Python,
133    };
134
135    #[test]
136    fn format_traceback() {
137        Python::attach(|py| {
138            let err = py
139                .run(c"raise Exception('banana')", None, None)
140                .expect_err("raising should have given us an error");
141
142            assert_eq!(
143                err.traceback(py).unwrap().format().unwrap(),
144                "Traceback (most recent call last):\n  File \"<string>\", line 1, in <module>\n"
145            );
146        })
147    }
148
149    #[test]
150    fn test_err_from_value() {
151        Python::attach(|py| {
152            let locals = PyDict::new(py);
153            // Produce an error from python so that it has a traceback
154            py.run(
155                cr"
156try:
157    raise ValueError('raised exception')
158except Exception as e:
159    err = e
160",
161                None,
162                Some(&locals),
163            )
164            .unwrap();
165            let err = PyErr::from_value(locals.get_item("err").unwrap().unwrap());
166            let traceback = err.value(py).getattr("__traceback__").unwrap();
167            assert!(err.traceback(py).unwrap().is(&traceback));
168        })
169    }
170
171    #[test]
172    fn test_err_into_py() {
173        Python::attach(|py| {
174            let locals = PyDict::new(py);
175            // Produce an error from python so that it has a traceback
176            py.run(
177                cr"
178def f():
179    raise ValueError('raised exception')
180",
181                None,
182                Some(&locals),
183            )
184            .unwrap();
185            let f = locals.get_item("f").unwrap().unwrap();
186            let err = f.call0().unwrap_err();
187            let traceback = err.traceback(py).unwrap();
188            let err_object = err.clone_ref(py).into_pyobject(py).unwrap();
189
190            assert!(err_object.getattr("__traceback__").unwrap().is(&traceback));
191        })
192    }
193
194    #[test]
195    #[cfg(all(not(Py_LIMITED_API), not(PyPy), not(GraalPy)))]
196    fn test_create_traceback() {
197        Python::attach(|py| {
198            let traceback = PyTraceback::new(
199                py,
200                None,
201                PyFrame::new(py, c"file2.py", c"func2", 20).unwrap(),
202                0,
203                20,
204            )
205            .unwrap();
206            let traceback = PyTraceback::new(
207                py,
208                Some(traceback),
209                PyFrame::new(py, c"file1.py", c"func1", 10).unwrap(),
210                0,
211                10,
212            )
213            .unwrap();
214            assert_eq!(
215                traceback.format().unwrap(), "Traceback (most recent call last):\n  File \"file1.py\", line 10, in func1\n  File \"file2.py\", line 20, in func2\n"
216            );
217        })
218    }
219}