Skip to main content

pyo3/types/
frame.rs

1#![deny(clippy::undocumented_unsafe_blocks)]
2use crate::ffi_ptr_ext::FfiPtrExt;
3use crate::sealed::Sealed;
4use crate::types::{PyCode, PyDict};
5use crate::PyAny;
6use crate::{ffi, Bound, PyResult, Python};
7use core::ffi::CStr;
8use pyo3_ffi::PyObject;
9
10/// Represents a Python frame.
11///
12/// Values of this type are accessed via PyO3's smart pointers, e.g. as
13/// [`Py<PyFrame>`][crate::Py] or [`Bound<'py, PyFrame>`][crate::Bound].
14#[repr(transparent)]
15pub struct PyFrame(PyAny);
16
17pyobject_native_type_core!(
18    PyFrame,
19    pyobject_native_static_type_object!(ffi::PyFrame_Type),
20    "types",
21    "FrameType",
22    #checkfunction=ffi::PyFrame_Check
23);
24
25impl PyFrame {
26    /// Creates a new frame object.
27    pub fn new<'py>(
28        py: Python<'py>,
29        file_name: &CStr,
30        func_name: &CStr,
31        line_number: i32,
32    ) -> PyResult<Bound<'py, PyFrame>> {
33        // Safety: Thread is attached because we have a python token
34        let state = unsafe { ffi::compat::PyThreadState_GetUnchecked() };
35        let code = PyCode::empty(py, file_name, func_name, line_number);
36        let globals = PyDict::new(py);
37        let locals = PyDict::new(py);
38
39        // SAFETY:
40        // - we're attached to the interpreter
41        // - `PyFrame_New` returns an owned reference or raises an exception
42        // - the result is a frame object
43        unsafe {
44            Ok(ffi::PyFrame_New(
45                state,
46                code.as_ptr().cast(),
47                globals.as_ptr(),
48                locals.as_ptr(),
49            )
50            .cast::<PyObject>()
51            .assume_owned_or_err(py)?
52            .cast_into_unchecked::<PyFrame>())
53        }
54    }
55}
56
57/// Implementation of functionality for [`PyFrame`].
58///
59/// These methods are defined for the `Bound<'py, PyFrame>` smart pointer, so to use method call
60/// syntax these methods are separated into a trait, because stable Rust does not yet support
61/// `arbitrary_self_types`.
62#[doc(alias = "PyFrame")]
63pub trait PyFrameMethods<'py>: Sealed {
64    /// Returns the line number of the current instruction in the frame.
65    fn line_number(&self) -> i32;
66
67    /// Gets this frame's next outer frame if there is one
68    #[cfg(not(Py_LIMITED_API))]
69    fn outer(&self) -> Option<Bound<'py, PyFrame>>;
70
71    /// Gets the frame code
72    #[cfg(any(not(Py_LIMITED_API), Py_3_10))]
73    fn code(&self) -> Bound<'py, PyCode>;
74
75    /// Gets the variable `name` of this frame.
76    #[cfg(all(Py_3_12, not(Py_LIMITED_API)))]
77    fn var(&self, name: &CStr) -> PyResult<Bound<'py, PyAny>>;
78
79    /// Gets this frame's `f_builtins` attribute
80    #[cfg(all(Py_3_11, not(Py_LIMITED_API)))]
81    fn builtins(&self) -> Bound<'py, PyDict>;
82
83    /// Gets this frame's `f_globals` attribute
84    #[cfg(all(Py_3_11, not(Py_LIMITED_API)))]
85    fn globals(&self) -> Bound<'py, PyDict>;
86
87    /// Gets this frame's `f_locals` attribute
88    #[cfg(all(Py_3_11, not(Py_LIMITED_API)))]
89    fn locals(&self) -> Bound<'py, PyAny>;
90}
91
92impl<'py> PyFrameMethods<'py> for Bound<'py, PyFrame> {
93    fn line_number(&self) -> i32 {
94        // SAFETY:
95        // - we're attached to the interpreter
96        // - `self` is a `PyFrameObject`
97        unsafe { ffi::PyFrame_GetLineNumber(self.as_ptr().cast()) }
98    }
99
100    #[cfg(not(Py_LIMITED_API))]
101    fn outer(&self) -> Option<Bound<'py, PyFrame>> {
102        // SAFETY:
103        // - we're attached to the interpreter
104        // - `self` is a `PyFrameObject`
105        // - `PyFrame_GetBack` returns an owned reference
106        // - the result may be null if there is no outer frame, but no exception is raised
107        // - the result is a frame object
108        unsafe {
109            ffi::PyFrame_GetBack(self.as_ptr().cast())
110                .cast::<ffi::PyObject>()
111                .assume_owned_or_opt(self.py())
112                .map(|obj| obj.cast_into_unchecked())
113        }
114    }
115
116    #[cfg(any(not(Py_LIMITED_API), Py_3_10))]
117    fn code(&self) -> Bound<'py, PyCode> {
118        // SAFETY:
119        // - we're attached to the interpreter
120        // - `self` is a `PyFrameObject`
121        // - `PyFrame_GetCode` returns an owned reference
122        // - the result can not be null
123        // - the result is a code object
124        unsafe {
125            ffi::PyFrame_GetCode(self.as_ptr().cast())
126                .cast::<ffi::PyObject>()
127                .assume_owned_unchecked(self.py())
128                .cast_into_unchecked()
129        }
130    }
131
132    #[cfg(all(Py_3_12, not(Py_LIMITED_API)))]
133    fn var(&self, name: &CStr) -> PyResult<Bound<'py, PyAny>> {
134        // SAFETY:
135        // - we're attached to the interpreter
136        // - `self` is a `PyFrameObject`
137        // - `PyFrame_GetVarString` returns an owned reference or raises an exception
138        // - `name` is a valid null terminated C string
139        unsafe {
140            ffi::PyFrame_GetVarString(self.as_ptr().cast(), name.as_ptr().cast_mut())
141                .assume_owned_or_err(self.py())
142        }
143    }
144
145    #[cfg(all(Py_3_11, not(Py_LIMITED_API)))]
146    fn builtins(&self) -> Bound<'py, PyDict> {
147        // SAFETY:
148        // - we're attached to the interpreter
149        // - `self` is a `PyFrameObject`
150        // - `PyFrame_GetBuiltins` returns an owned reference
151        // - the result can not be null
152        unsafe {
153            ffi::PyFrame_GetBuiltins(self.as_ptr().cast())
154                .assume_owned_unchecked(self.py())
155                .cast_into()
156                // The result is expected (and documented) to be a dict object, however it is
157                // possible for Python code to overwrite `__builtins__` with any arbitrary object.
158                // As reasonable code should never do this, we panic here for correctness in case
159                // the type does not match.
160                //
161                // See https://github.com/PyO3/pyo3/issues/6048
162                .expect("`PyFrame_GetBuiltins` returns a `dict`")
163        }
164    }
165
166    #[cfg(all(Py_3_11, not(Py_LIMITED_API)))]
167    fn globals(&self) -> Bound<'py, PyDict> {
168        // SAFETY:
169        // - we're attached to the interpreter
170        // - `self` is a `PyFrameObject`
171        // - `PyFrame_GetGlobals` returns an owned reference
172        // - the result can not be null
173        // - the result is a dict object
174        unsafe {
175            ffi::PyFrame_GetGlobals(self.as_ptr().cast())
176                .assume_owned_unchecked(self.py())
177                .cast_into_unchecked()
178        }
179    }
180
181    #[cfg(all(Py_3_11, not(Py_LIMITED_API)))]
182    fn locals(&self) -> Bound<'py, PyAny> {
183        // SAFETY:
184        // - we're attached to the interpreter
185        // - `self` is a `PyFrameObject`
186        // - `PyFrame_GetLocals` returns an owned reference
187        // - the result can not be null
188        unsafe { ffi::PyFrame_GetLocals(self.as_ptr().cast()).assume_owned_unchecked(self.py()) }
189    }
190}
191
192#[cfg(test)]
193mod tests {
194    use super::*;
195
196    fn get_frame(py: Python<'_>) -> Bound<'_, PyFrame> {
197        use crate::types::PyAnyMethods as _;
198
199        let m = crate::types::PyModule::from_code(
200            py,
201            cr#"
202import sys
203CONST = "global"
204def get_frame():
205    var = 42
206    return sys._getframe()
207"#,
208            c"frame.py",
209            c"frame",
210        )
211        .unwrap();
212
213        m.getattr("get_frame")
214            .unwrap()
215            .call0()
216            .unwrap()
217            .cast_into()
218            .unwrap()
219    }
220
221    #[test]
222    fn test_frame_creation() {
223        Python::attach(|py| {
224            let frame = PyFrame::new(py, c"file.py", c"func", 42).unwrap();
225            assert_eq!(frame.line_number(), 42);
226        });
227    }
228
229    #[test]
230    #[cfg(not(Py_LIMITED_API))]
231    fn test_frame_outer() {
232        Python::attach(|py| {
233            use crate::types::PyAnyMethods as _;
234
235            let m = crate::types::PyModule::from_code(
236                py,
237                cr#"
238import sys
239def inner():
240    return sys._getframe()
241def outer():
242    return inner()
243"#,
244                c"outer.py",
245                c"outer",
246            )
247            .unwrap();
248
249            let frame = m
250                .getattr("outer")
251                .unwrap()
252                .call0()
253                .unwrap()
254                .cast_into()
255                .unwrap();
256
257            let back = frame.outer().unwrap();
258            let f_back = frame.getattr("f_back").unwrap();
259
260            assert_eq!(back.as_ptr(), f_back.as_ptr());
261            assert_eq!(back.line_number(), 6)
262        })
263    }
264
265    #[test]
266    #[cfg(any(not(Py_LIMITED_API), Py_3_10))]
267    fn test_frame_get_code() {
268        Python::attach(|py| {
269            use crate::types::PyAnyMethods as _;
270
271            let frame = get_frame(py);
272            let code = frame.code();
273            let f_code = frame.getattr("f_code").unwrap();
274
275            assert_eq!(code.as_ptr(), f_code.as_ptr());
276        })
277    }
278
279    #[test]
280    #[cfg(all(Py_3_12, not(Py_LIMITED_API)))]
281    fn test_frame_get_var() {
282        Python::attach(|py| {
283            use crate::types::PyAnyMethods as _;
284
285            let frame = get_frame(py);
286            assert_eq!(frame.var(c"var").unwrap().extract::<u32>().unwrap(), 42);
287            assert!(frame.var(c"var2").is_err());
288        })
289    }
290
291    #[test]
292    #[cfg(all(Py_3_11, not(Py_LIMITED_API)))]
293    fn test_frame_get_builtins() {
294        Python::attach(|py| {
295            use crate::types::PyAnyMethods as _;
296
297            let frame = get_frame(py);
298            let builtins = frame.builtins();
299
300            assert_eq!(
301                builtins
302                    .get_item("__name__")
303                    .unwrap()
304                    .extract::<&str>()
305                    .unwrap(),
306                "builtins"
307            );
308            assert!(builtins.contains("len").unwrap());
309        })
310    }
311
312    #[test]
313    #[cfg(all(Py_3_11, not(Py_LIMITED_API)))]
314    #[should_panic(expected = "`PyFrame_GetBuiltins` returns a `dict`")]
315    fn test_frame_builtins_panics_on_type_error() {
316        use crate::types::PyAnyMethods as _;
317        Python::attach(|py| -> PyResult<()> {
318            let sys = py.import("sys")?;
319            let globals = PyDict::new(py);
320            globals.set_item("getframe", sys.getattr("_getframe")?)?;
321            globals.set_item("__builtins__", py.eval(c"object()", None, None)?)?;
322            py.run(c"frame = getframe()", Some(&globals), None)?;
323            let frame: Bound<'_, PyFrame> = globals.get_item("frame")?.cast_into()?;
324
325            // This should panic, as `__builtins__` is not a dict
326            let _builtins = frame.builtins();
327
328            Ok(())
329        })
330        .unwrap();
331    }
332
333    #[test]
334    #[cfg(all(Py_3_11, not(Py_LIMITED_API)))]
335    fn test_frame_get_globals() {
336        Python::attach(|py| {
337            use crate::types::PyAnyMethods as _;
338
339            let frame = get_frame(py);
340            let globals = frame.globals();
341
342            assert_eq!(
343                globals
344                    .get_item("CONST")
345                    .unwrap()
346                    .extract::<&str>()
347                    .unwrap(),
348                "global"
349            );
350        })
351    }
352
353    #[test]
354    #[cfg(all(Py_3_11, not(Py_LIMITED_API)))]
355    fn test_frame_get_locals() {
356        Python::attach(|py| {
357            use crate::types::PyAnyMethods as _;
358
359            let frame = get_frame(py);
360            let locals = frame.locals();
361
362            assert_eq!(
363                locals.get_item("var").unwrap().extract::<u32>().unwrap(),
364                42
365            );
366        })
367    }
368}