Skip to content

Commit ecec832

Browse files
Allow constructing PyTraceback
1 parent 42a35d9 commit ecec832

5 files changed

Lines changed: 118 additions & 3 deletions

File tree

newsfragments/5857.added.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Allow constructing `PyTraceback`

src/sealed.rs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,6 @@
11
use crate::impl_::pyfunction::PyFunctionDef;
2+
#[cfg(all(not(Py_LIMITED_API), not(PyPy), not(GraalPy)))]
3+
use crate::types::PyFrame;
24
use crate::types::{
35
PyBool, PyByteArray, PyBytes, PyCapsule, PyComplex, PyDict, PyFloat, PyFrozenSet, PyList,
46
PyMapping, PyMappingProxy, PyModule, PyRange, PySequence, PySet, PySlice, PyString,
@@ -42,6 +44,8 @@ impl Sealed for Bound<'_, PySet> {}
4244
impl Sealed for Bound<'_, PySlice> {}
4345
impl Sealed for Bound<'_, PyString> {}
4446
impl Sealed for Bound<'_, PyTraceback> {}
47+
#[cfg(all(not(Py_LIMITED_API), not(PyPy), not(GraalPy)))]
48+
impl Sealed for Bound<'_, PyFrame> {}
4549
impl Sealed for Bound<'_, PyTuple> {}
4650
impl Sealed for Bound<'_, PyType> {}
4751
impl Sealed for Bound<'_, PyWeakref> {}

src/types/frame.rs

Lines changed: 61 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,10 @@
1-
use crate::ffi;
1+
use crate::ffi_ptr_ext::FfiPtrExt;
2+
use crate::sealed::Sealed;
3+
use crate::types::PyDict;
24
use crate::PyAny;
5+
use crate::{ffi, Bound, PyResult, Python};
6+
use pyo3_ffi::PyObject;
7+
use std::ffi::CStr;
38

49
/// Represents a Python frame.
510
///
@@ -15,3 +20,58 @@ pyobject_native_type_core!(
1520
"FrameType",
1621
#checkfunction=ffi::PyFrame_Check
1722
);
23+
24+
impl PyFrame {
25+
/// Creates a new frame object.
26+
pub fn new<'py>(
27+
py: Python<'py>,
28+
file_name: &CStr,
29+
func_name: &CStr,
30+
line_number: i32,
31+
) -> PyResult<Bound<'py, PyFrame>> {
32+
// Safety: Thread is attached because we have a python token
33+
let state = unsafe { ffi::compat::PyThreadState_GetUnchecked() };
34+
let globals = PyDict::new(py);
35+
let locals = PyDict::new(py);
36+
37+
unsafe {
38+
let code = ffi::PyCode_NewEmpty(file_name.as_ptr(), func_name.as_ptr(), line_number);
39+
Ok(
40+
ffi::PyFrame_New(state, code, globals.as_ptr(), locals.as_ptr())
41+
.cast::<PyObject>()
42+
.assume_owned_or_err(py)?
43+
.cast_into_unchecked::<PyFrame>(),
44+
)
45+
}
46+
}
47+
}
48+
49+
/// Implementation of functionality for [`PyFrame`].
50+
///
51+
/// These methods are defined for the `Bound<'py, PyFrame>` smart pointer, so to use method call
52+
/// syntax these methods are separated into a trait, because stable Rust does not yet support
53+
/// `arbitrary_self_types`.
54+
#[doc(alias = "PyFrame")]
55+
pub trait PyFrameMethods<'py>: Sealed {
56+
/// Returns the line number of the current instruction in the frame.
57+
fn line_number(&self) -> i32;
58+
}
59+
60+
impl<'py> PyFrameMethods<'py> for Bound<'py, PyFrame> {
61+
fn line_number(&self) -> i32 {
62+
unsafe { ffi::PyFrame_GetLineNumber(self.as_ptr().cast()) }
63+
}
64+
}
65+
66+
#[cfg(test)]
67+
mod tests {
68+
use super::*;
69+
70+
#[test]
71+
fn test_frame_creation() {
72+
Python::attach(|py| {
73+
let frame = PyFrame::new(py, c"file.py", c"func", 42).unwrap();
74+
assert_eq!(frame.line_number(), 42);
75+
});
76+
}
77+
}

src/types/mod.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ pub use self::dict::{PyDictItems, PyDictKeys, PyDictValues};
1616
pub use self::ellipsis::PyEllipsis;
1717
pub use self::float::{PyFloat, PyFloatMethods};
1818
#[cfg(all(not(Py_LIMITED_API), not(PyPy), not(GraalPy)))]
19-
pub use self::frame::PyFrame;
19+
pub use self::frame::{PyFrame, PyFrameMethods};
2020
pub use self::frozenset::{PyFrozenSet, PyFrozenSetBuilder, PyFrozenSetMethods};
2121
pub use self::function::PyCFunction;
2222
#[cfg(not(Py_LIMITED_API))]

src/types/traceback.rs

Lines changed: 51 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
use crate::err::{error_on_minusone, PyResult};
22
use crate::types::{any::PyAnyMethods, string::PyStringMethods, PyString};
33
use crate::{ffi, Bound, PyAny};
4+
#[cfg(all(not(Py_LIMITED_API), not(PyPy), not(GraalPy)))]
5+
use crate::{types::PyFrame, PyTypeCheck, Python};
46

57
/// Represents a Python traceback.
68
///
@@ -20,6 +22,27 @@ pyobject_native_type_core!(
2022
#checkfunction=ffi::PyTraceBack_Check
2123
);
2224

25+
impl PyTraceback {
26+
/// Creates a new traceback object from the given frame.
27+
///
28+
/// The `next` is the next traceback in the direction of where the exception was raised
29+
/// or `None` if this is the last frame in the traceback.
30+
#[cfg(all(not(Py_LIMITED_API), not(PyPy), not(GraalPy)))]
31+
pub fn new<'py>(
32+
py: Python<'py>,
33+
next: Option<Bound<'py, PyTraceback>>,
34+
frame: Bound<'py, PyFrame>,
35+
instruction_index: i32,
36+
line_number: i32,
37+
) -> PyResult<Bound<'py, PyTraceback>> {
38+
unsafe {
39+
Ok(PyTraceback::classinfo_object(py)
40+
.call1((next, frame, instruction_index, line_number))?
41+
.cast_into_unchecked())
42+
}
43+
}
44+
}
45+
2346
/// Implementation of functionality for [`PyTraceback`].
2447
///
2548
/// These methods are defined for the `Bound<'py, PyTraceback>` smart pointer, so to use method call
@@ -82,9 +105,10 @@ impl<'py> PyTracebackMethods<'py> for Bound<'py, PyTraceback> {
82105

83106
#[cfg(test)]
84107
mod tests {
108+
use super::*;
85109
use crate::IntoPyObject;
86110
use crate::{
87-
types::{any::PyAnyMethods, dict::PyDictMethods, traceback::PyTracebackMethods, PyDict},
111+
types::{dict::PyDictMethods, PyDict},
88112
PyErr, Python,
89113
};
90114

@@ -146,4 +170,30 @@ def f():
146170
assert!(err_object.getattr("__traceback__").unwrap().is(&traceback));
147171
})
148172
}
173+
174+
#[test]
175+
#[cfg(all(not(Py_LIMITED_API), not(PyPy), not(GraalPy)))]
176+
fn test_create_traceback() {
177+
Python::attach(|py| {
178+
let traceback = PyTraceback::new(
179+
py,
180+
None,
181+
PyFrame::new(py, c"file2.py", c"func2", 20).unwrap(),
182+
0,
183+
20,
184+
)
185+
.unwrap();
186+
let traceback = PyTraceback::new(
187+
py,
188+
Some(traceback),
189+
PyFrame::new(py, c"file1.py", c"func1", 10).unwrap(),
190+
0,
191+
10,
192+
)
193+
.unwrap();
194+
assert_eq!(
195+
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"
196+
);
197+
})
198+
}
149199
}

0 commit comments

Comments
 (0)