@@ -20,7 +20,9 @@ use monty::{
2020 MontyObject , MontyRun , OsFunction , PrintWriter , ResourceLimits , RunProgress ,
2121} ;
2222use std:: collections:: HashMap ;
23+ use std:: future:: Future ;
2324use std:: path:: { Path , PathBuf } ;
25+ use std:: pin:: Pin ;
2426use std:: sync:: Arc ;
2527use std:: time:: Duration ;
2628
@@ -107,6 +109,41 @@ impl PythonLimits {
107109 }
108110}
109111
112+ /// Async handler for external Python function calls.
113+ ///
114+ /// Receives `(function_name, positional_args, keyword_args)` directly from monty.
115+ /// Return `ExternalResult::Return(value)` for success or `ExternalResult::Error(exc)` for failure.
116+ pub type PythonExternalFnHandler = Arc <
117+ dyn Fn (
118+ String ,
119+ Vec < MontyObject > ,
120+ Vec < ( MontyObject , MontyObject ) > ,
121+ ) -> Pin < Box < dyn Future < Output = ExternalResult > + Send > >
122+ + Send
123+ + Sync ,
124+ > ;
125+
126+ /// External function configuration for the Python builtin.
127+ ///
128+ /// Groups function names and their async handler together.
129+ /// Configure via [`BashBuilder::python_with_external_handler`](crate::BashBuilder::python_with_external_handler).
130+ #[ derive( Clone ) ]
131+ pub struct PythonExternalFns {
132+ /// Function names callable from Python (e.g., `"call_tool"`).
133+ names : Vec < String > ,
134+ /// Async handler invoked when Python calls one of these functions.
135+ handler : PythonExternalFnHandler ,
136+ }
137+
138+ impl std:: fmt:: Debug for PythonExternalFns {
139+ fn fmt ( & self , f : & mut std:: fmt:: Formatter < ' _ > ) -> std:: fmt:: Result {
140+ f. debug_struct ( "PythonExternalFns" )
141+ . field ( "names" , & self . names )
142+ . field ( "handler" , & "<fn>" )
143+ . finish ( )
144+ }
145+ }
146+
110147/// The python/python3 builtin command.
111148///
112149/// Executes Python code using the embedded Monty interpreter (pydantic/monty).
@@ -126,19 +163,38 @@ impl PythonLimits {
126163pub struct Python {
127164 /// Resource limits for the Monty interpreter.
128165 pub limits : PythonLimits ,
166+ /// Optional external function configuration.
167+ external_fns : Option < PythonExternalFns > ,
129168}
130169
131170impl Python {
132171 /// Create with default limits.
133172 pub fn new ( ) -> Self {
134173 Self {
135174 limits : PythonLimits :: default ( ) ,
175+ external_fns : None ,
136176 }
137177 }
138178
139179 /// Create with custom limits.
140180 pub fn with_limits ( limits : PythonLimits ) -> Self {
141- Self { limits }
181+ Self {
182+ limits,
183+ external_fns : None ,
184+ }
185+ }
186+
187+ /// Set external function names and handler.
188+ ///
189+ /// External functions are callable from Python by name.
190+ /// When called, execution pauses and the handler is invoked with the raw monty arguments.
191+ pub fn with_external_handler (
192+ mut self ,
193+ names : Vec < String > ,
194+ handler : PythonExternalFnHandler ,
195+ ) -> Self {
196+ self . external_fns = Some ( PythonExternalFns { names, handler } ) ;
197+ self
142198 }
143199}
144200
@@ -268,6 +324,7 @@ impl Builtin for Python {
268324 ctx. cwd ,
269325 & merged_env,
270326 & self . limits ,
327+ self . external_fns . as_ref ( ) ,
271328 )
272329 . await
273330 }
@@ -284,6 +341,7 @@ async fn run_python(
284341 cwd : & Path ,
285342 env : & HashMap < String , String > ,
286343 py_limits : & PythonLimits ,
344+ external_fns : Option < & PythonExternalFns > ,
287345) -> Result < ExecResult > {
288346 // Strip shebang if present
289347 let code = if code. starts_with ( "#!" ) {
@@ -295,7 +353,8 @@ async fn run_python(
295353 code
296354 } ;
297355
298- let runner = match MontyRun :: new ( code. to_owned ( ) , filename, vec ! [ ] , vec ! [ ] ) {
356+ let ext_fn_names = external_fns. map ( |ef| ef. names . clone ( ) ) . unwrap_or_default ( ) ;
357+ let runner = match MontyRun :: new ( code. to_owned ( ) , filename, vec ! [ ] , ext_fn_names) {
299358 Ok ( r) => r,
300359 Err ( e) => return Ok ( format_exception ( e) ) ,
301360 } ;
@@ -343,14 +402,27 @@ async fn run_python(
343402 }
344403 }
345404 }
346- RunProgress :: FunctionCall { state, .. } => {
347- // No external functions registered; return error
348- let err = MontyException :: new (
349- ExcType :: RuntimeError ,
350- Some ( "external function not available in virtual mode" . into ( ) ) ,
351- ) ;
405+ RunProgress :: FunctionCall {
406+ function_name,
407+ args,
408+ kwargs,
409+ state,
410+ ..
411+ } => {
412+ let result = if let Some ( ef) = external_fns {
413+ ( ef. handler ) ( function_name, args, kwargs) . await
414+ } else {
415+ // No external functions registered; return error
416+ ExternalResult :: Error ( MontyException :: new (
417+ ExcType :: RuntimeError ,
418+ Some (
419+ "no external function handler configured (external functions not enabled)" . into ( ) ,
420+ ) ,
421+ ) )
422+ } ;
423+
352424 let mut printer = PrintWriter :: Collect ( buf) ;
353- match state. run ( ExternalResult :: Error ( err ) , & mut printer) {
425+ match state. run ( result , & mut printer) {
354426 Ok ( next) => {
355427 buf = take_collected ( & mut printer) ;
356428 progress = next;
@@ -1111,4 +1183,165 @@ mod tests {
11111183 assert_eq ! ( limits. max_memory, 64 * 1024 * 1024 ) ;
11121184 assert_eq ! ( limits. max_recursion, 200 ) ;
11131185 }
1186+
1187+ // --- External function tests ---
1188+
1189+ /// Helper: run Python with an external function handler.
1190+ async fn run_with_external (
1191+ code : & str ,
1192+ fn_names : & [ & str ] ,
1193+ handler : PythonExternalFnHandler ,
1194+ ) -> ExecResult {
1195+ let args = vec ! [ "-c" . to_string( ) , code. to_string( ) ] ;
1196+ let env = HashMap :: new ( ) ;
1197+ let mut variables = HashMap :: new ( ) ;
1198+ let mut cwd = PathBuf :: from ( "/home/user" ) ;
1199+ let fs = Arc :: new ( InMemoryFs :: new ( ) ) ;
1200+ let py = Python :: with_limits ( PythonLimits :: default ( ) )
1201+ . with_external_handler ( fn_names. iter ( ) . map ( |s| s. to_string ( ) ) . collect ( ) , handler) ;
1202+ let ctx = Context :: new_for_test ( & args, & env, & mut variables, & mut cwd, fs, None ) ;
1203+ py. execute ( ctx) . await . unwrap ( )
1204+ }
1205+
1206+ #[ tokio:: test]
1207+ async fn test_external_fn_return_value ( ) {
1208+ let handler: PythonExternalFnHandler = Arc :: new ( |_name, _args, _kwargs| {
1209+ Box :: pin ( async { ExternalResult :: Return ( MontyObject :: Int ( 42 ) ) } )
1210+ } ) ;
1211+ let r = run_with_external ( "print(get_answer())" , & [ "get_answer" ] , handler) . await ;
1212+ assert_eq ! ( r. exit_code, 0 ) ;
1213+ assert_eq ! ( r. stdout, "42\n " ) ;
1214+ }
1215+
1216+ #[ tokio:: test]
1217+ async fn test_external_fn_with_args ( ) {
1218+ let handler: PythonExternalFnHandler = Arc :: new ( |_name, args, _kwargs| {
1219+ Box :: pin ( async move {
1220+ let a = match & args[ 0 ] {
1221+ MontyObject :: Int ( i) => * i,
1222+ _ => 0 ,
1223+ } ;
1224+ let b = match & args[ 1 ] {
1225+ MontyObject :: Int ( i) => * i,
1226+ _ => 0 ,
1227+ } ;
1228+ ExternalResult :: Return ( MontyObject :: Int ( a + b) )
1229+ } )
1230+ } ) ;
1231+ let r = run_with_external ( "print(add(3, 4))" , & [ "add" ] , handler) . await ;
1232+ assert_eq ! ( r. exit_code, 0 ) ;
1233+ assert_eq ! ( r. stdout, "7\n " ) ;
1234+ }
1235+
1236+ #[ tokio:: test]
1237+ async fn test_external_fn_with_kwargs ( ) {
1238+ let handler: PythonExternalFnHandler = Arc :: new ( |_name, _args, kwargs| {
1239+ Box :: pin ( async move {
1240+ for ( k, v) in & kwargs {
1241+ if let ( MontyObject :: String ( key) , MontyObject :: String ( val) ) = ( k, v) {
1242+ if key == "name" {
1243+ return ExternalResult :: Return ( MontyObject :: String ( format ! (
1244+ "hello {val}"
1245+ ) ) ) ;
1246+ }
1247+ }
1248+ }
1249+ ExternalResult :: Return ( MontyObject :: String ( "hello unknown" . into ( ) ) )
1250+ } )
1251+ } ) ;
1252+ let r = run_with_external ( "print(greet(name='world'))" , & [ "greet" ] , handler) . await ;
1253+ assert_eq ! ( r. exit_code, 0 ) ;
1254+ assert_eq ! ( r. stdout, "hello world\n " ) ;
1255+ }
1256+
1257+ #[ tokio:: test]
1258+ async fn test_external_fn_error ( ) {
1259+ let handler: PythonExternalFnHandler = Arc :: new ( |_name, _args, _kwargs| {
1260+ Box :: pin ( async {
1261+ ExternalResult :: Error ( MontyException :: new (
1262+ ExcType :: RuntimeError ,
1263+ Some ( "something went wrong" . into ( ) ) ,
1264+ ) )
1265+ } )
1266+ } ) ;
1267+ let r = run_with_external ( "fail()" , & [ "fail" ] , handler) . await ;
1268+ assert_eq ! ( r. exit_code, 1 ) ;
1269+ assert ! ( r. stderr. contains( "RuntimeError" ) ) ;
1270+ assert ! ( r. stderr. contains( "something went wrong" ) ) ;
1271+ }
1272+
1273+ #[ tokio:: test]
1274+ async fn test_external_fn_caught_error ( ) {
1275+ let handler: PythonExternalFnHandler = Arc :: new ( |_name, _args, _kwargs| {
1276+ Box :: pin ( async {
1277+ ExternalResult :: Error ( MontyException :: new (
1278+ ExcType :: ValueError ,
1279+ Some ( "bad value" . into ( ) ) ,
1280+ ) )
1281+ } )
1282+ } ) ;
1283+ let r = run_with_external (
1284+ "try:\n fail()\n except ValueError as e:\n print(f'caught: {e}')" ,
1285+ & [ "fail" ] ,
1286+ handler,
1287+ )
1288+ . await ;
1289+ assert_eq ! ( r. exit_code, 0 ) ;
1290+ assert ! ( r. stdout. contains( "caught:" ) ) ;
1291+ assert ! ( r. stdout. contains( "bad value" ) ) ;
1292+ }
1293+
1294+ #[ tokio:: test]
1295+ async fn test_external_fn_multiple_calls ( ) {
1296+ let counter = Arc :: new ( std:: sync:: atomic:: AtomicI64 :: new ( 0 ) ) ;
1297+ let counter_clone = counter. clone ( ) ;
1298+ let handler: PythonExternalFnHandler = Arc :: new ( move |_name, _args, _kwargs| {
1299+ let c = counter_clone. clone ( ) ;
1300+ Box :: pin ( async move {
1301+ let val = c. fetch_add ( 1 , std:: sync:: atomic:: Ordering :: SeqCst ) ;
1302+ ExternalResult :: Return ( MontyObject :: Int ( val) )
1303+ } )
1304+ } ) ;
1305+ let r = run_with_external (
1306+ "a = next_id()\n b = next_id()\n c = next_id()\n print(a, b, c)" ,
1307+ & [ "next_id" ] ,
1308+ handler,
1309+ )
1310+ . await ;
1311+ assert_eq ! ( r. exit_code, 0 ) ;
1312+ assert_eq ! ( r. stdout, "0 1 2\n " ) ;
1313+ }
1314+
1315+ #[ tokio:: test]
1316+ async fn test_external_fn_returns_string ( ) {
1317+ let handler: PythonExternalFnHandler = Arc :: new ( |_name, args, _kwargs| {
1318+ Box :: pin ( async move {
1319+ let input = match & args[ 0 ] {
1320+ MontyObject :: String ( s) => s. clone ( ) ,
1321+ _ => String :: new ( ) ,
1322+ } ;
1323+ ExternalResult :: Return ( MontyObject :: String ( input. to_uppercase ( ) ) )
1324+ } )
1325+ } ) ;
1326+ let r = run_with_external ( "print(upper('hello'))" , & [ "upper" ] , handler) . await ;
1327+ assert_eq ! ( r. exit_code, 0 ) ;
1328+ assert_eq ! ( r. stdout, "HELLO\n " ) ;
1329+ }
1330+
1331+ #[ tokio:: test]
1332+ async fn test_external_fn_dispatches_by_name ( ) {
1333+ let handler: PythonExternalFnHandler = Arc :: new ( |name, _args, _kwargs| {
1334+ Box :: pin ( async move {
1335+ let result = match name. as_str ( ) {
1336+ "get_x" => MontyObject :: Int ( 10 ) ,
1337+ "get_y" => MontyObject :: Int ( 20 ) ,
1338+ _ => MontyObject :: None ,
1339+ } ;
1340+ ExternalResult :: Return ( result)
1341+ } )
1342+ } ) ;
1343+ let r = run_with_external ( "print(get_x() + get_y())" , & [ "get_x" , "get_y" ] , handler) . await ;
1344+ assert_eq ! ( r. exit_code, 0 ) ;
1345+ assert_eq ! ( r. stdout, "30\n " ) ;
1346+ }
11141347}
0 commit comments