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(all(Py_3_9, not(Py_LIMITED_API)))]
69    fn outer(&self) -> Option<Bound<'py, PyFrame>>;
70
71    /// Gets the frame code
72    #[cfg(any(all(Py_3_9, 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(all(Py_3_9, 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(all(Py_3_9, 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    #[cfg(Py_3_9)]
197    fn get_frame(py: Python<'_>) -> Bound<'_, PyFrame> {
198        use crate::types::PyAnyMethods as _;
199
200        let m = crate::types::PyModule::from_code(
201            py,
202            cr#"
203import sys
204CONST = "global"
205def get_frame():
206    var = 42
207    return sys._getframe()
208"#,
209            c"frame.py",
210            c"frame",
211        )
212        .unwrap();
213
214        m.getattr("get_frame")
215            .unwrap()
216            .call0()
217            .unwrap()
218            .cast_into()
219            .unwrap()
220    }
221
222    #[test]
223    fn test_frame_creation() {
224        Python::attach(|py| {
225            let frame = PyFrame::new(py, c"file.py", c"func", 42).unwrap();
226            assert_eq!(frame.line_number(), 42);
227        });
228    }
229
230    #[test]
231    #[cfg(all(Py_3_9, not(Py_LIMITED_API)))]
232    fn test_frame_outer() {
233        Python::attach(|py| {
234            use crate::types::PyAnyMethods as _;
235
236            let m = crate::types::PyModule::from_code(
237                py,
238                cr#"
239import sys
240def inner():
241    return sys._getframe()
242def outer():
243    return inner()
244"#,
245                c"outer.py",
246                c"outer",
247            )
248            .unwrap();
249
250            let frame = m
251                .getattr("outer")
252                .unwrap()
253                .call0()
254                .unwrap()
255                .cast_into()
256                .unwrap();
257
258            let back = frame.outer().unwrap();
259            let f_back = frame.getattr("f_back").unwrap();
260
261            assert_eq!(back.as_ptr(), f_back.as_ptr());
262            assert_eq!(back.line_number(), 6)
263        })
264    }
265
266    #[test]
267    #[cfg(any(all(Py_3_9, not(Py_LIMITED_API)), Py_3_10))]
268    fn test_frame_get_code() {
269        Python::attach(|py| {
270            use crate::types::PyAnyMethods as _;
271
272            let frame = get_frame(py);
273            let code = frame.code();
274            let f_code = frame.getattr("f_code").unwrap();
275
276            assert_eq!(code.as_ptr(), f_code.as_ptr());
277        })
278    }
279
280    #[test]
281    #[cfg(all(Py_3_12, not(Py_LIMITED_API)))]
282    fn test_frame_get_var() {
283        Python::attach(|py| {
284            use crate::types::PyAnyMethods as _;
285
286            let frame = get_frame(py);
287            assert_eq!(frame.var(c"var").unwrap().extract::<u32>().unwrap(), 42);
288            assert!(frame.var(c"var2").is_err());
289        })
290    }
291
292    #[test]
293    #[cfg(all(Py_3_11, not(Py_LIMITED_API)))]
294    fn test_frame_get_builtins() {
295        Python::attach(|py| {
296            use crate::types::PyAnyMethods as _;
297
298            let frame = get_frame(py);
299            let builtins = frame.builtins();
300
301            assert_eq!(
302                builtins
303                    .get_item("__name__")
304                    .unwrap()
305                    .extract::<&str>()
306                    .unwrap(),
307                "builtins"
308            );
309            assert!(builtins.contains("len").unwrap());
310        })
311    }
312
313    #[test]
314    #[cfg(all(Py_3_11, not(Py_LIMITED_API)))]
315    #[should_panic(expected = "`PyFrame_GetBuiltins` returns a `dict`")]
316    fn test_frame_builtins_panics_on_type_error() {
317        use crate::types::PyAnyMethods as _;
318        Python::attach(|py| -> PyResult<()> {
319            let sys = py.import("sys")?;
320            let globals = PyDict::new(py);
321            globals.set_item("getframe", sys.getattr("_getframe")?)?;
322            globals.set_item("__builtins__", py.eval(c"object()", None, None)?)?;
323            py.run(c"frame = getframe()", Some(&globals), None)?;
324            let frame: Bound<'_, PyFrame> = globals.get_item("frame")?.cast_into()?;
325
326            // This should panic, as `__builtins__` is not a dict
327            let _builtins = frame.builtins();
328
329            Ok(())
330        })
331        .unwrap();
332    }
333
334    #[test]
335    #[cfg(all(Py_3_11, not(Py_LIMITED_API)))]
336    fn test_frame_get_globals() {
337        Python::attach(|py| {
338            use crate::types::PyAnyMethods as _;
339
340            let frame = get_frame(py);
341            let globals = frame.globals();
342
343            assert_eq!(
344                globals
345                    .get_item("CONST")
346                    .unwrap()
347                    .extract::<&str>()
348                    .unwrap(),
349                "global"
350            );
351        })
352    }
353
354    #[test]
355    #[cfg(all(Py_3_11, not(Py_LIMITED_API)))]
356    fn test_frame_get_locals() {
357        Python::attach(|py| {
358            use crate::types::PyAnyMethods as _;
359
360            let frame = get_frame(py);
361            let locals = frame.locals();
362
363            assert_eq!(
364                locals.get_item("var").unwrap().extract::<u32>().unwrap(),
365                42
366            );
367        })
368    }
369}