Skip to content

Commit 2dc118e

Browse files
authored
add PyCode::compile to create executable code objects (#5217)
1 parent cc1b9c5 commit 2dc118e

File tree

5 files changed

+159
-89
lines changed

5 files changed

+159
-89
lines changed

newsfragments/5217.added.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
add `PyCode::compile` and `PyCodeMethods::run` to create and execute code objects

pyo3-ffi/src/cpython/code.rs

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ use crate::pyport::Py_ssize_t;
44
#[cfg(not(GraalPy))]
55
use std::os::raw::c_char;
66
use std::os::raw::{c_int, c_void};
7-
#[cfg(not(any(PyPy, GraalPy)))]
7+
#[cfg(not(PyPy))]
88
use std::ptr::addr_of_mut;
99

1010
// skipped private _PY_MONITORING_LOCAL_EVENTS
@@ -71,14 +71,14 @@ pub const CO_FUTURE_GENERATOR_STOP: c_int = 0x8_0000;
7171

7272
pub const CO_MAXBLOCKS: usize = 20;
7373

74-
#[cfg(not(any(PyPy, GraalPy)))]
74+
#[cfg(not(PyPy))]
7575
#[cfg_attr(windows, link(name = "pythonXY"))]
7676
extern "C" {
7777
pub static mut PyCode_Type: PyTypeObject;
7878
}
7979

8080
#[inline]
81-
#[cfg(not(any(PyPy, GraalPy)))]
81+
#[cfg(not(PyPy))]
8282
pub unsafe fn PyCode_Check(op: *mut PyObject) -> c_int {
8383
(Py_TYPE(op) == addr_of_mut!(PyCode_Type)) as c_int
8484
}

src/marker.rs

Lines changed: 19 additions & 76 deletions
Original file line numberDiff line numberDiff line change
@@ -117,21 +117,18 @@
117117
//! [`Rc`]: std::rc::Rc
118118
//! [`Py`]: crate::Py
119119
use crate::conversion::IntoPyObject;
120-
use crate::err::PyErr;
121120
use crate::err::{self, PyResult};
122-
use crate::ffi_ptr_ext::FfiPtrExt;
123121
use crate::gil::{GILGuard, SuspendGIL};
124122
use crate::impl_::not_send::NotSend;
125-
use crate::py_result_ext::PyResultExt;
126123
use crate::types::any::PyAnyMethods;
127124
use crate::types::{
128-
PyAny, PyDict, PyEllipsis, PyModule, PyNone, PyNotImplemented, PyString, PyType,
125+
PyAny, PyCode, PyCodeMethods, PyDict, PyEllipsis, PyModule, PyNone, PyNotImplemented, PyString,
126+
PyType,
129127
};
130128
use crate::version::PythonVersionInfo;
131129
use crate::{ffi, Bound, PyObject, PyTypeInfo};
132130
use std::ffi::CStr;
133131
use std::marker::PhantomData;
134-
use std::os::raw::c_int;
135132

136133
/// Types that are safe to access while the GIL is not held.
137134
///
@@ -567,7 +564,13 @@ impl<'py> Python<'py> {
567564
globals: Option<&Bound<'py, PyDict>>,
568565
locals: Option<&Bound<'py, PyDict>>,
569566
) -> PyResult<Bound<'py, PyAny>> {
570-
self.run_code(code, ffi::Py_eval_input, globals, locals)
567+
let code = PyCode::compile(
568+
self,
569+
code,
570+
ffi::c_str!("<string>"),
571+
crate::types::PyCodeInput::Eval,
572+
)?;
573+
code.run(globals, locals)
571574
}
572575

573576
/// Executes one or more Python statements in the given context.
@@ -611,79 +614,17 @@ impl<'py> Python<'py> {
611614
globals: Option<&Bound<'py, PyDict>>,
612615
locals: Option<&Bound<'py, PyDict>>,
613616
) -> PyResult<()> {
614-
let res = self.run_code(code, ffi::Py_file_input, globals, locals);
615-
res.map(|obj| {
617+
let code = PyCode::compile(
618+
self,
619+
code,
620+
ffi::c_str!("<string>"),
621+
crate::types::PyCodeInput::File,
622+
)?;
623+
code.run(globals, locals).map(|obj| {
616624
debug_assert!(obj.is_none());
617625
})
618626
}
619627

620-
/// Runs code in the given context.
621-
///
622-
/// `start` indicates the type of input expected: one of `Py_single_input`,
623-
/// `Py_file_input`, or `Py_eval_input`.
624-
///
625-
/// If `globals` is `None`, it defaults to Python module `__main__`.
626-
/// If `locals` is `None`, it defaults to the value of `globals`.
627-
fn run_code(
628-
self,
629-
code: &CStr,
630-
start: c_int,
631-
globals: Option<&Bound<'py, PyDict>>,
632-
locals: Option<&Bound<'py, PyDict>>,
633-
) -> PyResult<Bound<'py, PyAny>> {
634-
let mptr = unsafe {
635-
ffi::compat::PyImport_AddModuleRef(ffi::c_str!("__main__").as_ptr())
636-
.assume_owned_or_err(self)?
637-
};
638-
let attr = mptr.getattr(crate::intern!(self, "__dict__"))?;
639-
let globals = match globals {
640-
Some(globals) => globals,
641-
None => attr.downcast::<PyDict>()?,
642-
};
643-
let locals = locals.unwrap_or(globals);
644-
645-
// If `globals` don't provide `__builtins__`, most of the code will fail if Python
646-
// version is <3.10. That's probably not what user intended, so insert `__builtins__`
647-
// for them.
648-
//
649-
// See also:
650-
// - https://github.com/python/cpython/pull/24564 (the same fix in CPython 3.10)
651-
// - https://github.com/PyO3/pyo3/issues/3370
652-
let builtins_s = crate::intern!(self, "__builtins__");
653-
let has_builtins = globals.contains(builtins_s)?;
654-
if !has_builtins {
655-
crate::sync::with_critical_section(globals, || {
656-
// check if another thread set __builtins__ while this thread was blocked on the critical section
657-
let has_builtins = globals.contains(builtins_s)?;
658-
if !has_builtins {
659-
// Inherit current builtins.
660-
let builtins = unsafe { ffi::PyEval_GetBuiltins() };
661-
662-
// `PyDict_SetItem` doesn't take ownership of `builtins`, but `PyEval_GetBuiltins`
663-
// seems to return a borrowed reference, so no leak here.
664-
if unsafe {
665-
ffi::PyDict_SetItem(globals.as_ptr(), builtins_s.as_ptr(), builtins)
666-
} == -1
667-
{
668-
return Err(PyErr::fetch(self));
669-
}
670-
}
671-
Ok(())
672-
})?;
673-
}
674-
675-
let code_obj = unsafe {
676-
ffi::Py_CompileString(code.as_ptr(), ffi::c_str!("<string>").as_ptr(), start)
677-
.assume_owned_or_err(self)?
678-
};
679-
680-
unsafe {
681-
ffi::PyEval_EvalCode(code_obj.as_ptr(), globals.as_ptr(), locals.as_ptr())
682-
.assume_owned_or_err(self)
683-
.downcast_into_unchecked()
684-
}
685-
}
686-
687628
/// Gets the Python type object for type `T`.
688629
#[inline]
689630
pub fn get_type<T>(self) -> Bound<'py, PyType>
@@ -766,7 +707,7 @@ impl<'py> Python<'py> {
766707
/// Lets the Python interpreter check and handle any pending signals. This will invoke the
767708
/// corresponding signal handlers registered in Python (if any).
768709
///
769-
/// Returns `Err(`[`PyErr`]`)` if any signal handler raises an exception.
710+
/// Returns `Err(`[`PyErr`](crate::PyErr)`)` if any signal handler raises an exception.
770711
///
771712
/// These signals include `SIGINT` (normally raised by CTRL + C), which by default raises
772713
/// `KeyboardInterrupt`. For this reason it is good practice to call this function regularly
@@ -939,6 +880,8 @@ mod tests {
939880
#[test]
940881
#[cfg(not(Py_LIMITED_API))]
941882
fn test_acquire_gil() {
883+
use std::ffi::c_int;
884+
942885
const GIL_NOT_HELD: c_int = 0;
943886
const GIL_HELD: c_int = 1;
944887

src/types/code.rs

Lines changed: 135 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,9 @@
1-
use crate::ffi;
2-
use crate::PyAny;
1+
use super::PyAnyMethods as _;
2+
use super::PyDict;
3+
use crate::ffi_ptr_ext::FfiPtrExt;
4+
use crate::py_result_ext::PyResultExt;
5+
use crate::{ffi, Bound, PyAny, PyErr, PyResult, Python};
6+
use std::ffi::CStr;
37

48
/// Represents a Python code object.
59
///
@@ -8,22 +12,146 @@ use crate::PyAny;
812
#[repr(transparent)]
913
pub struct PyCode(PyAny);
1014

15+
#[cfg(not(any(Py_LIMITED_API, PyPy)))]
1116
pyobject_native_type_core!(
1217
PyCode,
1318
pyobject_native_static_type_object!(ffi::PyCode_Type),
1419
#checkfunction=ffi::PyCode_Check
1520
);
1621

22+
#[cfg(any(Py_LIMITED_API, PyPy))]
23+
pyobject_native_type_named!(PyCode);
24+
25+
#[cfg(any(Py_LIMITED_API, PyPy))]
26+
impl crate::PyTypeCheck for PyCode {
27+
const NAME: &'static str = "PyCode";
28+
#[cfg(feature = "experimental-inspect")]
29+
const PYTHON_TYPE: &'static str = "types.CodeType";
30+
31+
fn type_check(object: &Bound<'_, PyAny>) -> bool {
32+
let py = object.py();
33+
static TYPE: crate::sync::GILOnceCell<crate::Py<super::PyType>> =
34+
crate::sync::GILOnceCell::new();
35+
36+
TYPE.import(py, "types", "CodeType")
37+
.and_then(|ty| object.is_instance(ty))
38+
.unwrap_or_default()
39+
}
40+
}
41+
42+
/// Compilation mode of [`PyCode::compile`]
43+
pub enum PyCodeInput {
44+
/// Python grammar for isolated expressions
45+
Eval,
46+
/// Python grammar for sequences of statements as read from a file
47+
File,
48+
}
49+
50+
impl PyCode {
51+
/// Compiles code in the given context.
52+
///
53+
/// `input` decides whether `code` is treated as
54+
/// - [`PyCodeInput::Eval`]: an isolated expression
55+
/// - [`PyCodeInput::File`]: a sequence of statements
56+
pub fn compile<'py>(
57+
py: Python<'py>,
58+
code: &CStr,
59+
filename: &CStr,
60+
input: PyCodeInput,
61+
) -> PyResult<Bound<'py, PyCode>> {
62+
let start = match input {
63+
PyCodeInput::Eval => ffi::Py_eval_input,
64+
PyCodeInput::File => ffi::Py_file_input,
65+
};
66+
unsafe {
67+
ffi::Py_CompileString(code.as_ptr(), filename.as_ptr(), start)
68+
.assume_owned_or_err(py)
69+
.downcast_into_unchecked()
70+
}
71+
}
72+
}
73+
74+
/// Implementation of functionality for [`PyCode`].
75+
///
76+
/// These methods are defined for the `Bound<'py, PyCode>` smart pointer, so to use method call
77+
/// syntax these methods are separated into a trait, because stable Rust does not yet support
78+
/// `arbitrary_self_types`.
79+
pub trait PyCodeMethods<'py> {
80+
/// Runs code object.
81+
///
82+
/// If `globals` is `None`, it defaults to Python module `__main__`.
83+
/// If `locals` is `None`, it defaults to the value of `globals`.
84+
fn run(
85+
&self,
86+
globals: Option<&Bound<'py, PyDict>>,
87+
locals: Option<&Bound<'py, PyDict>>,
88+
) -> PyResult<Bound<'py, PyAny>>;
89+
}
90+
91+
impl<'py> PyCodeMethods<'py> for Bound<'py, PyCode> {
92+
fn run(
93+
&self,
94+
globals: Option<&Bound<'py, PyDict>>,
95+
locals: Option<&Bound<'py, PyDict>>,
96+
) -> PyResult<Bound<'py, PyAny>> {
97+
let mptr = unsafe {
98+
ffi::compat::PyImport_AddModuleRef(ffi::c_str!("__main__").as_ptr())
99+
.assume_owned_or_err(self.py())?
100+
};
101+
let attr = mptr.getattr(crate::intern!(self.py(), "__dict__"))?;
102+
let globals = match globals {
103+
Some(globals) => globals,
104+
None => attr.downcast::<PyDict>()?,
105+
};
106+
let locals = locals.unwrap_or(globals);
107+
108+
// If `globals` don't provide `__builtins__`, most of the code will fail if Python
109+
// version is <3.10. That's probably not what user intended, so insert `__builtins__`
110+
// for them.
111+
//
112+
// See also:
113+
// - https://github.com/python/cpython/pull/24564 (the same fix in CPython 3.10)
114+
// - https://github.com/PyO3/pyo3/issues/3370
115+
let builtins_s = crate::intern!(self.py(), "__builtins__");
116+
let has_builtins = globals.contains(builtins_s)?;
117+
if !has_builtins {
118+
crate::sync::with_critical_section(globals, || {
119+
// check if another thread set __builtins__ while this thread was blocked on the critical section
120+
let has_builtins = globals.contains(builtins_s)?;
121+
if !has_builtins {
122+
// Inherit current builtins.
123+
let builtins = unsafe { ffi::PyEval_GetBuiltins() };
124+
125+
// `PyDict_SetItem` doesn't take ownership of `builtins`, but `PyEval_GetBuiltins`
126+
// seems to return a borrowed reference, so no leak here.
127+
if unsafe {
128+
ffi::PyDict_SetItem(globals.as_ptr(), builtins_s.as_ptr(), builtins)
129+
} == -1
130+
{
131+
return Err(PyErr::fetch(self.py()));
132+
}
133+
}
134+
Ok(())
135+
})?;
136+
}
137+
138+
unsafe {
139+
ffi::PyEval_EvalCode(self.as_ptr(), globals.as_ptr(), locals.as_ptr())
140+
.assume_owned_or_err(self.py())
141+
}
142+
}
143+
}
144+
17145
#[cfg(test)]
18146
mod tests {
19-
use super::*;
20-
use crate::types::PyTypeMethods;
21-
use crate::{PyTypeInfo, Python};
22-
23147
#[test]
148+
#[cfg(not(any(Py_LIMITED_API, PyPy)))]
24149
fn test_type_object() {
150+
use crate::types::PyTypeMethods;
151+
use crate::{PyTypeInfo, Python};
152+
25153
Python::attach(|py| {
26-
assert_eq!(PyCode::type_object(py).name().unwrap(), "code");
154+
assert_eq!(super::PyCode::type_object(py).name().unwrap(), "code");
27155
})
28156
}
29157
}

src/types/mod.rs

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,7 @@ pub use self::boolobject::{PyBool, PyBoolMethods};
55
pub use self::bytearray::{PyByteArray, PyByteArrayMethods};
66
pub use self::bytes::{PyBytes, PyBytesMethods};
77
pub use self::capsule::{PyCapsule, PyCapsuleMethods};
8-
#[cfg(all(not(Py_LIMITED_API), not(PyPy), not(GraalPy)))]
9-
pub use self::code::PyCode;
8+
pub use self::code::{PyCode, PyCodeInput, PyCodeMethods};
109
pub use self::complex::{PyComplex, PyComplexMethods};
1110
#[allow(deprecated)]
1211
pub use self::datetime::{
@@ -223,7 +222,6 @@ pub(crate) mod boolobject;
223222
pub(crate) mod bytearray;
224223
pub(crate) mod bytes;
225224
pub(crate) mod capsule;
226-
#[cfg(all(not(Py_LIMITED_API), not(PyPy), not(GraalPy)))]
227225
mod code;
228226
pub(crate) mod complex;
229227
pub(crate) mod datetime;

0 commit comments

Comments
 (0)