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}