@@ -133,6 +133,21 @@ impl PyRunner {
133133 . await
134134 . map ( |_| ( ) )
135135 }
136+
137+ /// Synchronously executes a block of Python code.
138+ ///
139+ /// This is a blocking wrapper around `run`. It is intended for use in
140+ /// synchronous applications.
141+ ///
142+ /// * `code`: A string slice containing the Python code to execute.
143+ ///
144+ /// **Note:** Calling this from an existing async runtime can lead to panics.
145+ pub fn run_sync ( & self , code : & str ) -> Result < ( ) , PyRunnerError > {
146+ let rt =
147+ tokio:: runtime:: Runtime :: new ( ) . map_err ( |e| PyRunnerError :: PyError ( e. to_string ( ) ) ) ?;
148+ rt. block_on ( self . run ( code) )
149+ }
150+
136151 /// Asynchronously runs a python file.
137152 /// * `file`: Absolute path to a python file to execute.
138153 /// Also loads the path of the file to sys.path for imports.
@@ -142,6 +157,20 @@ impl PyRunner {
142157 . map ( |_| ( ) )
143158 }
144159
160+ /// Synchronously runs a python file.
161+ ///
162+ /// This is a blocking wrapper around `run_file`. It is intended for use in
163+ /// synchronous applications.
164+ ///
165+ /// * `file`: Absolute path to a python file to execute.
166+ ///
167+ /// **Note:** Calling this from an existing async runtime can lead to panics.
168+ pub fn run_file_sync ( & self , file : & Path ) -> Result < ( ) , PyRunnerError > {
169+ let rt =
170+ tokio:: runtime:: Runtime :: new ( ) . map_err ( |e| PyRunnerError :: PyError ( e. to_string ( ) ) ) ?;
171+ rt. block_on ( self . run_file ( file) )
172+ }
173+
145174 /// Asynchronously evaluates a single Python expression.
146175 ///
147176 /// * `code`: A string slice containing the Python expression to evaluate.
@@ -152,6 +181,20 @@ impl PyRunner {
152181 self . send_command ( CmdType :: EvalCode ( code. into ( ) ) ) . await
153182 }
154183
184+ /// Synchronously evaluates a single Python expression.
185+ ///
186+ /// This is a blocking wrapper around `eval`. It is intended for use in
187+ /// synchronous applications.
188+ ///
189+ /// * `code`: A string slice containing the Python expression to evaluate.
190+ ///
191+ /// **Note:** Calling this from an existing async runtime can lead to panics.
192+ pub fn eval_sync ( & self , code : & str ) -> Result < Value , PyRunnerError > {
193+ let rt =
194+ tokio:: runtime:: Runtime :: new ( ) . map_err ( |e| PyRunnerError :: PyError ( e. to_string ( ) ) ) ?;
195+ rt. block_on ( self . eval ( code) )
196+ }
197+
155198 /// Asynchronously reads a variable from the Python interpreter's global scope.
156199 ///
157200 /// * `var_name`: The name of the variable to read. It can be a dot-separated path
@@ -162,6 +205,20 @@ impl PyRunner {
162205 . await
163206 }
164207
208+ /// Synchronously reads a variable from the Python interpreter's global scope.
209+ ///
210+ /// This is a blocking wrapper around `read_variable`. It is intended for use in
211+ /// synchronous applications.
212+ ///
213+ /// * `var_name`: The name of the variable to read.
214+ ///
215+ /// **Note:** Calling this from an existing async runtime can lead to panics.
216+ pub fn read_variable_sync ( & self , var_name : & str ) -> Result < Value , PyRunnerError > {
217+ let rt =
218+ tokio:: runtime:: Runtime :: new ( ) . map_err ( |e| PyRunnerError :: PyError ( e. to_string ( ) ) ) ?;
219+ rt. block_on ( self . read_variable ( var_name) )
220+ }
221+
165222 /// Asynchronously calls a Python function in the interpreter's global scope.
166223 ///
167224 /// * `name`: The name of the function to call. It can be a dot-separated path
@@ -181,6 +238,27 @@ impl PyRunner {
181238 . await
182239 }
183240
241+ /// Synchronously calls a Python function in the interpreter's global scope.
242+ ///
243+ /// This is a blocking wrapper around `call_function`. It will create a new
244+ /// Tokio runtime to execute the async function. It is intended for use in
245+ /// synchronous applications.
246+ ///
247+ /// * `name`: The name of the function to call.
248+ /// * `args`: A vector of `serde_json::Value` to pass as arguments to the function.
249+ ///
250+ /// **Note:** Calling this from an existing async runtime can lead to panics.
251+ /// This is for calling from a non-async context.
252+ #[ cfg( feature = "pyo3" ) ]
253+ pub fn call_function_sync (
254+ & self ,
255+ name : & str ,
256+ args : Vec < Value > ,
257+ ) -> Result < Value , PyRunnerError > {
258+ let rt = tokio:: runtime:: Runtime :: new ( ) . map_err ( |e| PyRunnerError :: PyError ( e. to_string ( ) ) ) ?;
259+ rt. block_on ( self . call_function ( name, args) )
260+ }
261+
184262 /// Asynchronously calls an async Python function in the interpreter's global scope.
185263 ///
186264 /// * `name`: The name of the function to call. It can be a dot-separated path
@@ -200,12 +278,45 @@ impl PyRunner {
200278 . await
201279 }
202280
281+ /// Synchronously calls an async Python function in the interpreter's global scope.
282+ ///
283+ /// This is a blocking wrapper around `call_async_function`. It is intended for use in
284+ /// synchronous applications.
285+ ///
286+ /// * `name`: The name of the function to call.
287+ /// * `args`: A vector of `serde_json::Value` to pass as arguments to the function.
288+ ///
289+ /// **Note:** Calling this from an existing async runtime can lead to panics.
290+ #[ cfg( feature = "pyo3" ) ]
291+ pub fn call_async_function_sync (
292+ & self ,
293+ name : & str ,
294+ args : Vec < Value > ,
295+ ) -> Result < Value , PyRunnerError > {
296+ let rt =
297+ tokio:: runtime:: Runtime :: new ( ) . map_err ( |e| PyRunnerError :: PyError ( e. to_string ( ) ) ) ?;
298+ rt. block_on ( self . call_async_function ( name, args) )
299+ }
300+
203301 /// Stops the Python execution thread gracefully.
204302 pub async fn stop ( & self ) -> Result < ( ) , PyRunnerError > {
205303 // We can ignore the `Ok(Value::Null)` result.
206304 self . send_command ( CmdType :: Stop ) . await ?;
207305 Ok ( ( ) )
208306 }
307+
308+ /// Synchronously stops the Python execution thread gracefully.
309+ ///
310+ /// This is a blocking wrapper around `stop`. It is intended for use in
311+ /// synchronous applications.
312+ ///
313+ /// **Note:** Calling this from an existing async runtime can lead to panics.
314+ pub fn stop_sync ( & self ) -> Result < ( ) , PyRunnerError > {
315+ let rt =
316+ tokio:: runtime:: Runtime :: new ( ) . map_err ( |e| PyRunnerError :: PyError ( e. to_string ( ) ) ) ?;
317+ rt. block_on ( self . stop ( ) )
318+ }
319+
209320 /// Set python venv environment folder (does not change interpreter)
210321 pub async fn set_venv ( & self , venv_path : & Path ) -> Result < ( ) , PyRunnerError > {
211322 if !venv_path. is_dir ( ) {
@@ -239,6 +350,20 @@ impl PyRunner {
239350 ) )
240351 . await
241352 }
353+
354+ /// Synchronously sets the python venv environment folder.
355+ ///
356+ /// This is a blocking wrapper around `set_venv`. It is intended for use in
357+ /// synchronous applications.
358+ ///
359+ /// * `venv_path`: Path to the venv directory.
360+ ///
361+ /// **Note:** Calling this from an existing async runtime can lead to panics.
362+ pub fn set_venv_sync ( & self , venv_path : & Path ) -> Result < ( ) , PyRunnerError > {
363+ let rt =
364+ tokio:: runtime:: Runtime :: new ( ) . map_err ( |e| PyRunnerError :: PyError ( e. to_string ( ) ) ) ?;
365+ rt. block_on ( self . set_venv ( venv_path) )
366+ }
242367}
243368
244369#[ cfg( test) ]
@@ -276,17 +401,18 @@ z = x + y"#;
276401 assert_eq ! ( z_val, Value :: Number ( 30 . into( ) ) ) ;
277402 }
278403
404+
279405 #[ tokio:: test]
280406 async fn test_run_with_function ( ) {
281407 // cargo test tests::test_run_with_function --release -- --nocapture
282- let start_time = std:: time:: Instant :: now ( ) ;
283408 let executor = PyRunner :: new ( ) ;
284409 let code = r#"
285410def add(a, b):
286411 return a + b
287412"# ;
288413
289414 executor. run ( code) . await . unwrap ( ) ;
415+ let start_time = std:: time:: Instant :: now ( ) ;
290416 let result = executor
291417 . call_function ( "add" , vec ! [ 5 . into( ) , 9 . into( ) ] )
292418 . await
@@ -296,6 +422,25 @@ def add(a, b):
296422 println ! ( "test_run_with_function took: {} microseconds" , duration. as_micros( ) ) ;
297423 }
298424
425+ #[ test]
426+ fn test_sync_run_with_function ( ) {
427+ // cargo test tests::test_run_with_function --release -- --nocapture
428+ let executor = PyRunner :: new ( ) ;
429+ let code = r#"
430+ def add(a, b):
431+ return a + b
432+ "# ;
433+
434+ executor. run_sync ( code) . unwrap ( ) ;
435+ let start_time = std:: time:: Instant :: now ( ) ;
436+ let result = executor
437+ . call_function_sync ( "add" , vec ! [ 5 . into( ) , 9 . into( ) ] )
438+ . unwrap ( ) ;
439+ assert_eq ! ( result, Value :: Number ( 14 . into( ) ) ) ;
440+ let duration = start_time. elapsed ( ) ;
441+ println ! ( "test_run_with_function_sync took: {} microseconds" , duration. as_micros( ) ) ;
442+ }
443+
299444 #[ cfg( feature = "pyo3" ) ]
300445 #[ tokio:: test]
301446 async fn test_run_with_async_function ( ) {
@@ -319,6 +464,25 @@ async def add_and_sleep(a, b, sleep_time):
319464 assert_eq ! ( result2. unwrap( ) , Value :: Number ( 16 . into( ) ) ) ;
320465 }
321466
467+ #[ cfg( feature = "pyo3" ) ]
468+ #[ test]
469+ fn test_run_with_async_function_sync ( ) {
470+ let executor = PyRunner :: new ( ) ;
471+ let code = r#"
472+ import asyncio
473+
474+ async def add(a, b):
475+ await asyncio.sleep(0.1)
476+ return a + b
477+ "# ;
478+
479+ executor. run_sync ( code) . unwrap ( ) ;
480+ let result = executor
481+ . call_async_function_sync ( "add" , vec ! [ 5 . into( ) , 9 . into( ) ] )
482+ . unwrap ( ) ;
483+ assert_eq ! ( result, Value :: Number ( 14 . into( ) ) ) ;
484+ }
485+
322486 #[ tokio:: test]
323487 async fn test_concurrent_calls ( ) {
324488 let executor = PyRunner :: new ( ) ;
0 commit comments