diff --git a/Cargo.lock b/Cargo.lock index 7e5aed5..573134a 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -62,6 +62,7 @@ name = "async_py" version = "0.2.1" dependencies = [ "dunce", + "once_cell", "pyo3", "rustpython-stdlib", "rustpython-vm", diff --git a/Cargo.toml b/Cargo.toml index 0a4a628..5157f33 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -14,7 +14,8 @@ categories = ["api-bindings", "asynchronous", "external-ffi-bindings"] dunce = "1.0.4" serde_json = "1.0.114" thiserror = "2.0" -tokio = { version = "1.36.0", features = ["sync", "macros"] } +once_cell = "1.19" +tokio = { version = "1.47.0", features = ["sync", "macros", "rt", "rt-multi-thread"] } [features] default = ["pyo3"] @@ -27,18 +28,16 @@ version = "0.26.0" features = ["auto-initialize"] optional = true -[dev-dependencies] -tempfile = "3" [dependencies.rustpython-vm] version = "0.4.0" optional = true features = ["threading", "serde", "importlib"] + [dependencies.rustpython-stdlib] version = "0.4.0" optional = true features = ["threading"] -# The `full` feature for tokio is needed for the tests -[dev-dependencies.tokio] -version = "1" -features = ["full"] +[dev-dependencies] +tempfile = "3" +tokio = { version = "1.47.0", features = ["full"] } diff --git a/README.md b/README.md index e95c53d..983a317 100644 --- a/README.md +++ b/README.md @@ -50,6 +50,44 @@ def greet(name): println!("{}", result2.as_str().unwrap()); // Prints: Hello World! Called 2 times from Python. } ``` + +### Async Python Example + +```rust +use async_py::PyRunner; + +#[tokio::main] +async fn main() { + let runner = PyRunner::new(); + let code = r#" +import asyncio +counter = 0 + +async def add_and_sleep(a, b, sleep_time): + global counter + await asyncio.sleep(sleep_time) + counter += 1 + return a + b + counter +"#; + + runner.run(code).await.unwrap(); + let result1 = runner.call_async_function("add_and_sleep", vec![5.into(), 10.into(), 1.into()]); + let result2 = runner.call_async_function("add_and_sleep", vec![5.into(), 10.into(), 0.1.into()]); + let (result1, result2) = tokio::join!(result1, result2); + assert_eq!(result1.unwrap(), Value::Number(17.into())); + assert_eq!(result2.unwrap(), Value::Number(16.into())); +} +``` +Both function calls are triggered to run async code at the same time. While the first call waits for the sleep, +the second can already start and also increment the counter first. Therefore, +result1 will wait longer and compute 5 + 10 + 2, while the result2 can compute 5 + 10 + 1. + +Each call will use its own event loop. This may not be very efficient and changed later. + +Make sure to use `call_async_function` for async python functions. Using `call_function` will +probably raise an error. +`call_async_function` is not available for RustPython. + ### Using a venv It is generally recommended to use a venv to install pip packages. While you cannot switch the interpreter version with this crate, you can use an diff --git a/src/lib.rs b/src/lib.rs index 28e5f84..3e9126e 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -10,10 +10,13 @@ mod pyo3_runner; #[cfg(feature = "rustpython")] mod rustpython_runner; +use once_cell::sync::Lazy; use serde_json::Value; use std::path::{Path, PathBuf}; +use std::sync::mpsc as std_mpsc; use std::thread; use thiserror::Error; +use tokio::runtime::Runtime; use tokio::sync::{mpsc, oneshot}; #[derive(Debug)] @@ -23,6 +26,7 @@ pub(crate) enum CmdType { EvalCode(String), ReadVariable(String), CallFunction { name: String, args: Vec }, + CallAsyncFunction { name: String, args: Vec }, Stop, } /// Represents a command to be sent to the Python execution thread. It includes the @@ -33,8 +37,26 @@ pub(crate) struct PyCommand { responder: oneshot::Sender>, } +/// A boxed, send-able future that resolves to a PyRunnerResult. +type Task = Box Result + Send>; + +/// A lazily-initialized worker thread for handling synchronous function calls. +/// This thread has its own private Tokio runtime to safely block on async operations +/// without interfering with any existing runtime the user might be in. +static SYNC_WORKER: Lazy> = Lazy::new(|| { + let (tx, rx) = std_mpsc::channel::(); + + thread::spawn(move || { + let rt = Runtime::new().expect("Failed to create Tokio runtime for sync worker"); + // When the sender (tx) is dropped, rx.recv() will return an Err, ending the loop. + while let Ok(task) = rx.recv() { + let _ = task(&rt); // The result is sent back via a channel inside the task. + } + }); + tx +}); /// Custom error types for the `PyRunner`. -#[derive(Error, Debug)] +#[derive(Error, Debug, Clone)] pub enum PyRunnerError { #[error("Failed to send command to Python thread. The thread may have panicked.")] SendCommandFailed, @@ -70,6 +92,13 @@ pub struct PyRunner { sender: mpsc::Sender, } + +impl Default for PyRunner { + fn default() -> Self { + PyRunner::new() + } +} + impl PyRunner { /// Creates a new `PyRunner` and spawns a dedicated thread for Python execution. /// @@ -87,10 +116,16 @@ impl PyRunner { // This is crucial to avoid blocking the async runtime and to manage the GIL correctly. thread::spawn(move || { #[cfg(all(feature = "pyo3", not(feature = "rustpython")))] - pyo3_runner::python_thread_main(receiver); + { + use tokio::runtime::Builder; + let rt = Builder::new_multi_thread().enable_all().build().unwrap(); + rt.block_on(pyo3_runner::python_thread_main(receiver)); + } #[cfg(feature = "rustpython")] - rustpython_runner::python_thread_main(receiver); + { + rustpython_runner::python_thread_main(receiver); + } }); Self { sender } @@ -119,17 +154,68 @@ impl PyRunner { .map_err(PyRunnerError::PyError) } + /// A private helper function to encapsulate the logic of sending a command + /// and receiving a response synchronously. + fn send_command_sync(&self, cmd_type: CmdType) -> Result { + let (tx, rx) = std_mpsc::channel(); + let sender = self.sender.clone(); + + let task = Box::new(move |rt: &Runtime| { + let result = rt.block_on(async { + // This is the async `send_command` logic, but we can't call it + // directly because of `&self` lifetime issues inside the closure. + let (responder, receiver) = oneshot::channel(); + let cmd = PyCommand { + cmd_type, + responder, + }; + sender + .send(cmd) + .await + .map_err(|_| PyRunnerError::SendCommandFailed)?; + receiver + .await + .map_err(|_| PyRunnerError::ReceiveResultFailed.clone())? + .map_err(PyRunnerError::PyError) + }); + if tx.send(result.clone()).is_err() { + return Err(PyRunnerError::SendCommandFailed); + } + result + }); + + SYNC_WORKER + .send(task) + .map_err(|_| PyRunnerError::SendCommandFailed)?; + rx.recv().map_err(|_| PyRunnerError::ReceiveResultFailed)? + } /// Asynchronously executes a block of Python code. /// /// * `code`: A string slice containing the Python code to execute. + /// /// This is equivalent to Python's `exec()` function. pub async fn run(&self, code: &str) -> Result<(), PyRunnerError> { self.send_command(CmdType::RunCode(code.into())) .await .map(|_| ()) } + + /// Synchronously executes a block of Python code. + /// + /// This is a blocking wrapper around `run`. It is intended for use in + /// synchronous applications. + /// + /// * `code`: A string slice containing the Python code to execute. + /// + /// **Note:** This function is safe to call from any context (sync or async). + pub fn run_sync(&self, code: &str) -> Result<(), PyRunnerError> { + self.send_command_sync(CmdType::RunCode(code.into())) + .map(|_| ()) + } + /// Asynchronously runs a python file. /// * `file`: Absolute path to a python file to execute. + /// /// Also loads the path of the file to sys.path for imports. pub async fn run_file(&self, file: &Path) -> Result<(), PyRunnerError> { self.send_command(CmdType::RunFile(file.to_path_buf())) @@ -137,32 +223,72 @@ impl PyRunner { .map(|_| ()) } + /// Synchronously runs a python file. + /// + /// This is a blocking wrapper around `run_file`. It is intended for use in + /// synchronous applications. + /// + /// * `file`: Absolute path to a python file to execute. + /// + /// **Note:** This function is safe to call from any context (sync or async). + pub fn run_file_sync(&self, file: &Path) -> Result<(), PyRunnerError> { + self.send_command_sync(CmdType::RunFile(file.to_path_buf())) + .map(|_| ()) + } + /// Asynchronously evaluates a single Python expression. /// - /// * `code`: A string slice containing the Python expression to evaluate. - /// Must not contain definitions or multiple lines. + /// * `code`: A string slice containing the Python expression to evaluate. Must not contain definitions or multiple lines. + /// /// Returns a `Result` containing the expression's result as a `serde_json::Value` on success, /// or a `PyRunnerError` on failure. This is equivalent to Python's `eval()` function. pub async fn eval(&self, code: &str) -> Result { self.send_command(CmdType::EvalCode(code.into())).await } + /// Synchronously evaluates a single Python expression. + /// + /// This is a blocking wrapper around `eval`. It is intended for use in + /// synchronous applications. + /// + /// * `code`: A string slice containing the Python expression to evaluate. + /// + /// **Note:** This function is safe to call from any context (sync or async). + pub fn eval_sync(&self, code: &str) -> Result { + self.send_command_sync(CmdType::EvalCode(code.into())) + } + /// Asynchronously reads a variable from the Python interpreter's global scope. /// /// * `var_name`: The name of the variable to read. It can be a dot-separated path /// to access attributes of objects (e.g., "my_module.my_variable"). + /// /// Returns the variable's value as a `serde_json::Value` on success. pub async fn read_variable(&self, var_name: &str) -> Result { self.send_command(CmdType::ReadVariable(var_name.into())) .await } + /// Synchronously reads a variable from the Python interpreter's global scope. + /// + /// This is a blocking wrapper around `read_variable`. It is intended for use in + /// synchronous applications. + /// + /// * `var_name`: The name of the variable to read. + /// + /// **Note:** This function is safe to call from any context (sync or async). + pub fn read_variable_sync(&self, var_name: &str) -> Result { + self.send_command_sync(CmdType::ReadVariable(var_name.into())) + } + /// Asynchronously calls a Python function in the interpreter's global scope. /// /// * `name`: The name of the function to call. It can be a dot-separated path /// to access functions within modules (e.g., "my_module.my_function"). /// * `args`: A vector of `serde_json::Value` to pass as arguments to the function. + /// /// Returns the function's return value as a `serde_json::Value` on success. + /// Does not release GIL during await. pub async fn call_function( &self, name: &str, @@ -175,12 +301,81 @@ impl PyRunner { .await } + /// Synchronously calls a Python function in the interpreter's global scope. + /// + /// This is a blocking wrapper around `call_function`. It will create a new + /// Tokio runtime to execute the async function. It is intended for use in + /// synchronous applications. + /// + /// * `name`: The name of the function to call. + /// * `args`: A vector of `serde_json::Value` to pass as arguments to the function. + /// + /// **Note:** This function is safe to call from any context (sync or async). + pub fn call_function_sync(&self, name: &str, args: Vec) -> Result { + self.send_command_sync(CmdType::CallFunction { + name: name.into(), + args, + }) + } + + /// Asynchronously calls an async Python function in the interpreter's global scope. + /// + /// * `name`: The name of the function to call. It can be a dot-separated path + /// to access functions within modules (e.g., "my_module.my_function"). + /// * `args`: A vector of `serde_json::Value` to pass as arguments to the function. + /// + /// Returns the function's return value as a `serde_json::Value` on success. + /// Will release GIL during await. + pub async fn call_async_function( + &self, + name: &str, + args: Vec, + ) -> Result { + self.send_command(CmdType::CallAsyncFunction { + name: name.into(), + args, + }) + .await + } + + /// Synchronously calls an async Python function in the interpreter's global scope. + /// + /// This is a blocking wrapper around `call_async_function`. It is intended for use in + /// synchronous applications. + /// + /// * `name`: The name of the function to call. + /// * `args`: A vector of `serde_json::Value` to pass as arguments to the function. + /// + /// **Note:** This function is safe to call from any context (sync or async). + #[cfg(feature = "pyo3")] + pub fn call_async_function_sync( + &self, + name: &str, + args: Vec, + ) -> Result { + self.send_command_sync(CmdType::CallAsyncFunction { + name: name.into(), + args, + }) + } + /// Stops the Python execution thread gracefully. pub async fn stop(&self) -> Result<(), PyRunnerError> { // We can ignore the `Ok(Value::Null)` result. self.send_command(CmdType::Stop).await?; Ok(()) } + + /// Synchronously stops the Python execution thread gracefully. + /// + /// This is a blocking wrapper around `stop`. It is intended for use in + /// synchronous applications. + /// + /// **Note:** This function is safe to call from any context (sync or async). + pub fn stop_sync(&self) -> Result<(), PyRunnerError> { + self.send_command_sync(CmdType::Stop).map(|_| ()) + } + /// Set python venv environment folder (does not change interpreter) pub async fn set_venv(&self, venv_path: &Path) -> Result<(), PyRunnerError> { if !venv_path.is_dir() { @@ -190,7 +385,7 @@ impl PyRunner { ))); } let set_venv_code = include_str!("set_venv.py"); - self.run(&set_venv_code).await?; + self.run(set_venv_code).await?; let site_packages = if cfg!(target_os = "windows") { venv_path.join("Lib").join("site-packages") @@ -214,6 +409,46 @@ impl PyRunner { )) .await } + + /// Synchronously sets the python venv environment folder. + /// + /// This is a blocking wrapper around `set_venv`. It is intended for use in + /// synchronous applications. + /// + /// * `venv_path`: Path to the venv directory. + /// + /// **Note:** This function is safe to call from any context (sync or async). + pub fn set_venv_sync(&self, venv_path: &Path) -> Result<(), PyRunnerError> { + if !venv_path.is_dir() { + return Err(PyRunnerError::PyError(format!( + "Could not find venv directory {}", + venv_path.display() + ))); + } + let set_venv_code = include_str!("set_venv.py"); + self.run_sync(set_venv_code)?; + + let site_packages = if cfg!(target_os = "windows") { + venv_path.join("Lib").join("site-packages") + } else { + let version_code = "f\"python{sys.version_info.major}.{sys.version_info.minor}\""; + let py_version = self.eval_sync(version_code)?; + venv_path + .join("lib") + .join(py_version.as_str().unwrap()) + .join("site-packages") + }; + #[cfg(all(feature = "pyo3", not(feature = "rustpython")))] + let with_pth = "True"; + #[cfg(feature = "rustpython")] + let with_pth = "False"; + + self.run_sync(&format!( + "add_venv_libs_to_syspath({}, {})", + print_path_for_python(&site_packages), + with_pth + )) + } } #[cfg(test)] @@ -251,8 +486,26 @@ z = x + y"#; assert_eq!(z_val, Value::Number(30.into())); } + #[tokio::test] + async fn test_run_sync_from_async() { + let executor = PyRunner::new(); + let code = r#" +x = 10 +y = 20 +z = x + y"#; + + let result_module = executor.run(code).await; + + assert!(result_module.is_ok()); + + let z_val = executor.read_variable_sync("z").unwrap(); + + assert_eq!(z_val, Value::Number(30.into())); + } + #[tokio::test] async fn test_run_with_function() { + // cargo test tests::test_run_with_function --release -- --nocapture let executor = PyRunner::new(); let code = r#" def add(a, b): @@ -260,11 +513,83 @@ def add(a, b): "#; executor.run(code).await.unwrap(); + let start_time = std::time::Instant::now(); let result = executor .call_function("add", vec![5.into(), 9.into()]) .await .unwrap(); assert_eq!(result, Value::Number(14.into())); + let duration = start_time.elapsed(); + println!( + "test_run_with_function took: {} microseconds", + duration.as_micros() + ); + } + + #[test] + fn test_sync_run_with_function() { + // cargo test tests::test_run_with_function --release -- --nocapture + let executor = PyRunner::new(); + let code = r#" +def add(a, b): + return a + b +"#; + + executor.run_sync(code).unwrap(); + let start_time = std::time::Instant::now(); + let result = executor + .call_function_sync("add", vec![5.into(), 9.into()]) + .unwrap(); + assert_eq!(result, Value::Number(14.into())); + let duration = start_time.elapsed(); + println!( + "test_run_with_function_sync took: {} microseconds", + duration.as_micros() + ); + } + + #[cfg(feature = "pyo3")] + #[tokio::test] + async fn test_run_with_async_function() { + let executor = PyRunner::new(); + let code = r#" +import asyncio +counter = 0 + +async def add_and_sleep(a, b, sleep_time): + global counter + await asyncio.sleep(sleep_time) + counter += 1 + return a + b + counter +"#; + + executor.run(code).await.unwrap(); + let result1 = + executor.call_async_function("add_and_sleep", vec![5.into(), 10.into(), 1.into()]); + let result2 = + executor.call_async_function("add_and_sleep", vec![5.into(), 10.into(), 0.1.into()]); + let (result1, result2) = tokio::join!(result1, result2); + assert_eq!(result1.unwrap(), Value::Number(17.into())); + assert_eq!(result2.unwrap(), Value::Number(16.into())); + } + + #[cfg(feature = "pyo3")] + #[test] + fn test_run_with_async_function_sync() { + let executor = PyRunner::new(); + let code = r#" +import asyncio + +async def add(a, b): + await asyncio.sleep(0.1) + return a + b +"#; + + executor.run_sync(code).unwrap(); + let result = executor + .call_async_function_sync("add", vec![5.into(), 9.into()]) + .unwrap(); + assert_eq!(result, Value::Number(14.into())); } #[tokio::test] diff --git a/src/pyo3_runner.rs b/src/pyo3_runner.rs index 5c867e0..ed9fea1 100644 --- a/src/pyo3_runner.rs +++ b/src/pyo3_runner.rs @@ -12,33 +12,49 @@ use pyo3::{ }; use serde_json::Value; use std::ffi::CString; -use tokio::sync::mpsc; +use tokio::sync::{mpsc, oneshot}; /// The main loop for the Python thread. This function is spawned in a new /// thread and is responsible for all Python interaction. -pub(crate) fn python_thread_main(mut receiver: mpsc::Receiver) { +pub(crate) async fn python_thread_main(mut receiver: mpsc::Receiver) { Python::initialize(); - Python::attach(|py| { - let globals = PyDict::new(py); - while let Some(mut cmd) = py.detach(|| receiver.blocking_recv()) { + let globals = Python::attach(|py| PyDict::new(py).unbind()); + while let Some(mut cmd) = receiver.recv().await { + Python::attach(|py| { + let globals = globals.bind(py); let result = match std::mem::replace(&mut cmd.cmd_type, CmdType::Stop) { CmdType::RunCode(code) => { let c_code = CString::new(code).expect("CString::new failed"); - py.run(&c_code, Some(&globals), None).map(|_| Value::Null) + py.run(&c_code, Some(globals), None).map(|_| Value::Null) } CmdType::EvalCode(code) => { let c_code = CString::new(code).expect("CString::new failed"); - py.eval(&c_code, Some(&globals), None) - .and_then(|obj| py_any_to_json(py, &obj)) + py.eval(&c_code, Some(globals), None) + .and_then(|obj| py_any_to_json(&obj)) } - CmdType::RunFile(file) => handle_run_file(py, &globals, file), + CmdType::RunFile(file) => handle_run_file(py, globals, file), CmdType::ReadVariable(var_name) => { - get_py_object(&globals, &var_name).and_then(|obj| py_any_to_json(py, &obj)) + get_py_object(globals, &var_name).and_then(|obj| py_any_to_json(&obj)) } CmdType::CallFunction { name, args } => { - handle_call_function(py, &globals, name, args) + handle_call_function(py, globals, name, args) } - CmdType::Stop => break, + CmdType::CallAsyncFunction { name, args } => { + let result: PyResult<_> = (|| { + let func = get_py_object(globals, &name)?; + check_func_callable(&func, &name)?; + Ok(func.unbind()) + })(); + + match result { + Ok(func) => { + py.detach(|| tokio::spawn(handle_call_async_function(func, args, cmd.responder))); + return; // The response is sent async, so we can return early. + } + Err(e) => Err(e), + } + } + CmdType::Stop => return receiver.close(), }; // Convert PyErr to a string representation to avoid exposing it outside this module. @@ -47,17 +63,17 @@ pub(crate) fn python_thread_main(mut receiver: mpsc::Receiver) { Err(e) => Err(e.to_string()), }; let _ = cmd.responder.send(response); - } + }); // After the loop, we can send a final confirmation for the Stop command if needed, // but the current implementation in lib.rs handles the channel closing. - }); + } } /// Resolves a potentially dot-separated Python object name from the globals dictionary. fn get_py_object<'py>( globals: &pyo3::Bound<'py, PyDict>, name: &str, -) -> PyResult> { +) -> PyResult> { let mut parts = name.split('.'); let first_part = parts.next().unwrap(); // split always yields at least one item @@ -72,6 +88,17 @@ fn get_py_object<'py>( Ok(obj) } +fn check_func_callable(func: &Bound, name: &str) -> PyResult<()> { + if !func.is_callable() { + Err(PyErr::new::(format!( + "'{}' is not a callable function", + name + ))) + } else { + Ok(()) + } +} + fn handle_run_file( py: Python, globals: &pyo3::Bound<'_, PyDict>, @@ -88,7 +115,7 @@ with open({}, 'r') as f: print_path_for_python(&file.to_path_buf()) ); let c_code = CString::new(code).expect("CString::new failed"); - py.run(&c_code, Some(&globals), None).map(|_| Value::Null) + py.run(&c_code, Some(globals), None).map(|_| Value::Null) } /// Handles the `CallFunction` command. @@ -99,25 +126,47 @@ fn handle_call_function( args: Vec, ) -> PyResult { let func = get_py_object(globals, &name)?; + check_func_callable(&func, &name)?; + let t_args = vec_to_py_tuple(&py, args)?; + let result = func.call1(t_args)?; + py_any_to_json(&result) +} - if !func.is_callable() { - return Err(PyErr::new::(format!( - "'{}' is not a callable function", - name - ))); - } - +fn vec_to_py_tuple<'py>( + py: &Python<'py>, + args: Vec, +) -> PyResult> { let py_args = args .into_iter() - .map(|v| json_value_to_pyobject(py, v)) + .map(|v| json_value_to_pyobject(*py, v)) .collect::>>()?; - let t_args = pyo3::types::PyTuple::new(py, py_args)?; - let result = func.call1(t_args)?; - py_any_to_json(py, &result) + pyo3::types::PyTuple::new(*py, py_args) +} + +/// Handles the `CallAsyncFunction` command. +async fn handle_call_async_function( + func: Py, + args: Vec, + responder: oneshot::Sender>, +) { + let result = Python::attach(|py| { + let func = func.bind(py); + let t_args = vec_to_py_tuple(&py, args)?; + let coroutine = func.call1(t_args)?; + + let asyncio = py.import("asyncio")?; + let loop_obj = asyncio.call_method0("new_event_loop")?; + asyncio.call_method1("set_event_loop", (loop_obj.clone(),))?; + let result = loop_obj.call_method1("run_until_complete", (coroutine,))?; + loop_obj.call_method0("close")?; + + py_any_to_json(&result) + }); + let _ = responder.send(result.map_err(|e| e.to_string())); } /// Recursively converts a Python object to a `serde_json::Value`. -fn py_any_to_json(py: Python, obj: &pyo3::Bound<'_, pyo3::PyAny>) -> PyResult { +fn py_any_to_json(obj: &pyo3::Bound<'_, PyAny>) -> PyResult { if obj.is_none() { return Ok(Value::Null); } @@ -142,13 +191,13 @@ fn py_any_to_json(py: Python, obj: &pyo3::Bound<'_, pyo3::PyAny>) -> PyResult() { let items: PyResult> = - list.iter().map(|item| py_any_to_json(py, &item)).collect(); + list.iter().map(|item| py_any_to_json(&item)).collect(); return Ok(Value::Array(items?)); } if let Ok(dict) = obj.cast::() { let mut map = serde_json::Map::new(); for (key, value) in dict.iter() { - map.insert(key.to_string(), py_any_to_json(py, &value)?); + map.insert(key.to_string(), py_any_to_json(&value)?); } return Ok(Value::Object(map)); } @@ -158,7 +207,7 @@ fn py_any_to_json(py: Python, obj: &pyo3::Bound<'_, pyo3::PyAny>) -> PyResult PyResult> { +fn json_value_to_pyobject(py: Python, value: Value) -> PyResult> { match value { Value::Null => Ok(py.None()), Value::Bool(b) => b.into_py_any(py), diff --git a/src/rustpython_runner.rs b/src/rustpython_runner.rs index 21d9e0b..15be641 100644 --- a/src/rustpython_runner.rs +++ b/src/rustpython_runner.rs @@ -47,6 +47,10 @@ pub(crate) fn python_thread_main(mut receiver: mpsc::Receiver) { call_function(vm, scope.clone(), name, args.clone()) .and_then(|obj| py_to_json(vm, &obj)) } + CmdType::CallAsyncFunction { name, args } => { + dbg!(name, args); + unimplemented!("Async functions are not supported yet in RustPython") + } CmdType::Stop => break, }; let response = result.map_err(|err| { diff --git a/src/set_venv.py b/src/set_venv.py index d121522..9682654 100644 --- a/src/set_venv.py +++ b/src/set_venv.py @@ -35,4 +35,4 @@ def add_venv_libs_to_syspath(site_packages, with_pth=False): except Exception as e: print(f"Warning: Could not process {pth_file}: {e}") - return site_packages \ No newline at end of file + return site_packages