inline_python/context.rs
1use crate::PythonBlock;
2use crate::run::run_python_code;
3use pyo3::{
4 FromPyObject, IntoPyObject, Py, PyResult, Python,
5 prelude::*,
6 types::{PyCFunction, PyDict},
7};
8
9/// An execution context for Python code.
10///
11/// This can be used to keep all global variables and imports intact between macro invocations:
12///
13/// ```
14/// # use inline_python::{Context, python};
15/// let c = Context::new();
16///
17/// c.run(python! {
18/// foo = 5
19/// });
20///
21/// c.run(python! {
22/// assert foo == 5
23/// });
24/// ```
25///
26/// You may also use it to inspect global variables after the execution of the Python code,
27/// or set global variables before running:
28///
29/// ```
30/// # use inline_python::{Context, python};
31/// let c = Context::new();
32///
33/// c.set("x", 13);
34///
35/// c.run(python! {
36/// foo = x + 2
37/// });
38///
39/// assert_eq!(c.get::<i32>("foo"), 15);
40/// ```
41pub struct Context {
42 pub(crate) globals: Py<PyDict>,
43}
44
45impl Context {
46 /// Create a new context for running Python code.
47 ///
48 /// This function panics if it fails to create the context.
49 #[allow(clippy::new_without_default)]
50 #[track_caller]
51 pub fn new() -> Self {
52 Python::with_gil(Self::new_with_gil)
53 }
54
55 #[track_caller]
56 pub(crate) fn new_with_gil(py: Python) -> Self {
57 match Self::try_new(py) {
58 Ok(x) => x,
59 Err(err) => panic!("{}", panic_string(py, &err)),
60 }
61 }
62
63 fn try_new(py: Python) -> PyResult<Self> {
64 Ok(Self {
65 globals: py.import("__main__")?.dict().copy()?.into(),
66 })
67 }
68
69 /// Get the globals as dictionary.
70 pub fn globals(&self) -> &Py<PyDict> {
71 &self.globals
72 }
73
74 /// Retrieve a global variable from the context.
75 ///
76 /// This function panics if the variable doesn't exist, or the conversion fails.
77 pub fn get<T: for<'p> FromPyObject<'p>>(&self, name: &str) -> T {
78 Python::with_gil(|py| match self.globals.bind(py).get_item(name) {
79 Err(_) | Ok(None) => {
80 panic!("Python context does not contain a variable named `{name}`",)
81 }
82 Ok(Some(value)) => match FromPyObject::extract_bound(&value) {
83 Ok(value) => value,
84 Err(e) => panic!(
85 "Unable to convert `{name}` to `{ty}`: {e}",
86 ty = std::any::type_name::<T>(),
87 ),
88 },
89 })
90 }
91
92 /// Set a global variable in the context.
93 ///
94 /// This function panics if the conversion fails.
95 pub fn set<T: for<'p> IntoPyObject<'p>>(&self, name: &str, value: T) {
96 Python::with_gil(|py| {
97 if let Err(e) = self.globals().bind(py).set_item(name, value) {
98 panic!(
99 "Unable to set `{name}` from a `{ty}`: {e}",
100 ty = std::any::type_name::<T>(),
101 );
102 }
103 })
104 }
105
106 /// Add a wrapped `#[pyfunction]` or `#[pymodule]` using its own `__name__`.
107 ///
108 /// Use this with `pyo3::wrap_pyfunction` or `pyo3::wrap_pymodule`.
109 ///
110 /// ```ignore
111 /// # use inline_python::{Context, python};
112 /// use pyo3::{prelude::*, wrap_pyfunction};
113 ///
114 /// #[pyfunction]
115 /// fn get_five() -> i32 {
116 /// 5
117 /// }
118 ///
119 /// fn main() {
120 /// let c = Context::new();
121 ///
122 /// c.add_wrapped(wrap_pyfunction!(get_five));
123 ///
124 /// c.run(python! {
125 /// assert get_five() == 5
126 /// });
127 /// }
128 /// ```
129 pub fn add_wrapped(&self, wrapper: &impl Fn(Python) -> PyResult<Bound<'_, PyCFunction>>) {
130 Python::with_gil(|py| {
131 let obj = wrapper(py).unwrap();
132 let name = obj
133 .getattr("__name__")
134 .expect("wrapped item should have a __name__");
135 if let Err(err) = self.globals().bind(py).set_item(name, obj) {
136 panic!("{}", panic_string(py, &err));
137 }
138 })
139 }
140
141 /// Run Python code using this context.
142 ///
143 /// This function should be called using the `python!{}` macro:
144 ///
145 /// ```
146 /// # use inline_python::{Context, python};
147 /// let c = Context::new();
148 ///
149 /// c.run(python!{
150 /// print("Hello World")
151 /// });
152 /// ```
153 ///
154 /// This function panics if the Python code fails.
155 pub fn run(
156 &self,
157 #[cfg(not(doc))] code: PythonBlock<impl FnOnce(&Bound<PyDict>)>,
158 #[cfg(doc)] code: PythonBlock, // Just show 'PythonBlock' in the docs.
159 ) {
160 Python::with_gil(|py| self.run_with_gil(py, code));
161 }
162
163 #[cfg(not(doc))]
164 pub(crate) fn run_with_gil<F: FnOnce(&Bound<PyDict>)>(
165 &self,
166 py: Python<'_>,
167 block: PythonBlock<F>,
168 ) {
169 (block.set_vars)(self.globals().bind(py));
170 if let Err(err) = run_python_code(py, self, block.bytecode) {
171 (block.panic)(panic_string(py, &err));
172 }
173 }
174}
175
176fn panic_string(py: Python, err: &PyErr) -> String {
177 match py_err_to_string(py, &err) {
178 Ok(msg) => msg,
179 Err(_) => err.to_string(),
180 }
181}
182
183/// Print the error while capturing stderr into a String.
184fn py_err_to_string(py: Python, err: &PyErr) -> Result<String, PyErr> {
185 let sys = py.import("sys")?;
186 let stderr = py.import("io")?.getattr("StringIO")?.call0()?;
187 let original_stderr = sys.dict().get_item("stderr")?;
188 sys.dict().set_item("stderr", &stderr)?;
189 err.print(py);
190 sys.dict().set_item("stderr", original_stderr)?;
191 stderr.call_method0("getvalue")?.extract()
192}