1- # Copyright 2023-2024 The MathWorks, Inc.
1+ # Copyright 2023-2025 The MathWorks, Inc.
22# Helper functions to communicate with matlab-proxy and MATLAB
33
44import http
@@ -179,6 +179,13 @@ async def send_shutdown_request_to_matlab(self):
179179 )
180180
181181 async def send_interrupt_request_to_matlab (self ):
182+ """Send an interrupt request to MATLAB to stop current execution.
183+
184+ The interrupt request is sent through the control channel using a specific message format.
185+
186+ Raises:
187+ HTTPError: If the interrupt request fails or matlab-proxy communication errors occur
188+ """
182189 self .logger .debug ("Sending interrupt request to MATLAB" )
183190 req_body = {
184191 "messages" : {
@@ -201,6 +208,24 @@ async def send_interrupt_request_to_matlab(self):
201208 resp .raise_for_status ()
202209
203210 async def _send_feval_request_to_matlab (self , http_client , fname , nargout , * args ):
211+ """Execute a MATLAB function call (feval) through the matlab-proxy.
212+
213+ Sends a function evaluation request to MATLAB, handling path setup and synchronous execution.
214+
215+ Args:
216+ http_client (aiohttp.ClientSession): HTTP client for sending the request
217+ fname (str): Name of the MATLAB function to call
218+ nargout (int): Number of output arguments expected
219+ *args: Variable arguments to pass to the MATLAB function
220+
221+ Returns:
222+ list: Results from the MATLAB function execution if successful
223+ Empty list if no outputs or nargout=0
224+
225+ Raises:
226+ MATLABConnectionError: If MATLAB connection is lost or response is invalid
227+ Exception: If function execution fails or is interrupted by user
228+ """
204229 self .logger .debug ("Sending FEval request to MATLAB" )
205230 # Add the MATLAB code shipped with kernel to the Path
206231 path = [str (pathlib .Path (__file__ ).parent / "matlab" )]
@@ -264,7 +289,36 @@ async def _send_feval_request_to_matlab(self, http_client, fname, nargout, *args
264289 self .logger .error ("Error occurred during communication with matlab-proxy" )
265290 raise resp .raise_for_status ()
266291
292+ async def send_eval_request_to_matlab (self , mcode ):
293+ """Send an evaluation request to MATLAB using the shell client.
294+
295+ Args:
296+ mcode (str): MATLAB code to be evaluated
297+
298+ Returns:
299+ dict: The evaluation response from MATLAB containing results or error information
300+
301+ Raises:
302+ MATLABConnectionError: If MATLAB connection is not available
303+ HTTPError: If there is an error in communication with matlab-proxy
304+ """
305+ return await self ._send_eval_request_to_matlab (self ._http_shell_client , mcode )
306+
267307 async def _send_eval_request_to_matlab (self , http_client , mcode ):
308+ """Internal method to send and process an evaluation request to MATLAB.
309+
310+ Args:
311+ http_client (aiohttp.ClientSession): HTTP client to use for the request
312+ mcode (str): MATLAB code to be evaluated
313+
314+ Returns:
315+ dict: The evaluation response containing results or error information
316+ from the MATLAB execution
317+
318+ Raises:
319+ MATLABConnectionError: If MATLAB connection is not available or response is invalid
320+ HTTPError: If there is an error in communication with matlab-proxy
321+ """
268322 self .logger .debug ("Sending Eval request to MATLAB" )
269323 # Add the MATLAB code shipped with kernel to the Path
270324 path = str (pathlib .Path (__file__ ).parent / "matlab" )
@@ -286,6 +340,7 @@ async def _send_eval_request_to_matlab(self, http_client, mcode):
286340 self .logger .debug (f"Response:\n { response_data } " )
287341 try :
288342 eval_response = response_data ["messages" ]["EvalResponse" ][0 ]
343+
289344 except KeyError :
290345 # In certain cases when the HTTPResponse is received, it does not
291346 # contain the expected data. In these cases most likely MATLAB has
@@ -296,54 +351,27 @@ async def _send_eval_request_to_matlab(self, http_client, mcode):
296351 )
297352 raise MATLABConnectionError ()
298353
299- # If the eval request succeeded, return the json decoded result.
300- if not eval_response ["isError" ]:
301- result_filepath = eval_response ["responseStr" ].strip ()
302-
303- # If the filepath in the response is not empty, read the result from
304- # file and delete the file.
305- if result_filepath != "" :
306- self .logger .debug (f"Found file with results: { result_filepath } " )
307- self .logger .debug ("Reading contents of the file" )
308- with open (result_filepath , "r" ) as f :
309- result = f .read ().strip ()
310- self .logger .debug ("Reading completed" )
311- try :
312- import os
313-
314- self .logger .debug (f"Deleting file: { result_filepath } " )
315- os .remove (result_filepath )
316- except Exception :
317- self .logger .error ("Deleting file failed" )
318- else :
319- self .logger .debug ("No result in EvalResponse" )
320- result = ""
321-
322- # If result is empty, populate dummy json
323- if result == "" :
324- result = "[]"
325- return json .loads (result )
326-
327- # Handle the error cases
328- if eval_response ["messageFaults" ]:
329- # This happens when "Interrupt Kernel" is issued from a different
330- # kernel. There may be other cases also.
331- self .logger .error (
332- f'Error during execution of Eval request in MATLAB:\n { eval_response ["messageFaults" ][0 ]["message" ]} '
333- )
334- error_message = (
335- "Failed to execute. Operation may have been interrupted by user."
336- )
337- else :
338- # This happens when "Interrupt Kernel" is issued from the same kernel.
339- # The responseStr contains the error message
340- error_message = eval_response ["responseStr" ].strip ()
341- raise Exception (error_message )
354+ return eval_response
355+
342356 else :
343357 self .logger .error ("Error during communication with matlab-proxy" )
344358 raise resp .raise_for_status ()
345359
346360 async def _send_jupyter_request_to_matlab (self , request_type , inputs , http_client ):
361+ """Process and send a Jupyter request to MATLAB using either feval or eval execution.
362+
363+ Args:
364+ request_type (str): Type of request (execute, complete, shutdown)
365+ inputs (list): List of input arguments for the request
366+ http_client (aiohttp.ClientSession): HTTP client to use for the request
367+
368+ Returns:
369+ dict: Response from MATLAB containing results of the request execution
370+
371+ Raises:
372+ MATLABConnectionError: If MATLAB connection is not available
373+ Exception: If request execution fails or is interrupted
374+ """
347375 execution_request_type = "feval"
348376
349377 inputs .insert (0 , request_type )
@@ -353,10 +381,14 @@ async def _send_jupyter_request_to_matlab(self, request_type, inputs, http_clien
353381 f"Using { execution_request_type } request type for communication with EC"
354382 )
355383
384+ resp = None
356385 if execution_request_type == "feval" :
357386 resp = await self ._send_feval_request_to_matlab (
358387 http_client , "processJupyterKernelRequest" , 1 , * inputs
359388 )
389+
390+ # The 'else' condition is an artifact and is present here incase we ever want to test
391+ # eval execution.
360392 else :
361393 user_mcode = inputs [2 ]
362394 # Construct a string which can be evaluated in MATLAB. For example
@@ -376,6 +408,66 @@ async def _send_jupyter_request_to_matlab(self, request_type, inputs, http_clien
376408 args = args + "," + str (cursor_pos )
377409
378410 eval_mcode = f"processJupyterKernelRequest({ args } )"
379- resp = await self ._send_eval_request_to_matlab (http_client , eval_mcode )
411+ eval_response = await self ._send_eval_request_to_matlab (
412+ http_client , eval_mcode
413+ )
414+ resp = await self ._read_eval_response_from_file (eval_response )
380415
381416 return resp
417+
418+ async def _read_eval_response_from_file (self , eval_response ):
419+ """Read and process MATLAB evaluation results from a response file.
420+
421+ Args:
422+ eval_response (dict): Response dictionary from MATLAB eval request containing
423+ file path and error information
424+
425+ Returns:
426+ dict: JSON decoded results from the response file
427+
428+ Raises:
429+ Exception: If evaluation failed or was interrupted by user
430+ """
431+ # If the eval request succeeded, return the json decoded result.
432+ if not eval_response ["isError" ]:
433+ result_filepath = eval_response ["responseStr" ].strip ()
434+
435+ # If the filepath in the response is not empty, read the result from
436+ # file and delete the file.
437+ if result_filepath != "" :
438+ self .logger .debug (f"Found file with results: { result_filepath } " )
439+ self .logger .debug ("Reading contents of the file" )
440+ with open (result_filepath , "r" ) as f :
441+ result = f .read ().strip ()
442+ self .logger .debug ("Reading completed" )
443+ try :
444+ import os
445+
446+ self .logger .debug (f"Deleting file: { result_filepath } " )
447+ os .remove (result_filepath )
448+ except Exception :
449+ self .logger .error ("Deleting file failed" )
450+ else :
451+ self .logger .debug ("No result in EvalResponse" )
452+ result = ""
453+
454+ # If result is empty, populate dummy json
455+ if result == "" :
456+ result = "[]"
457+ return json .loads (result )
458+
459+ # Handle the error cases
460+ if eval_response ["messageFaults" ]:
461+ # This happens when "Interrupt Kernel" is issued from a different
462+ # kernel. There may be other cases also.
463+ self .logger .error (
464+ f'Error during execution of Eval request in MATLAB:\n { eval_response ["messageFaults" ][0 ]["message" ]} '
465+ )
466+ error_message = (
467+ "Failed to execute. Operation may have been interrupted by user."
468+ )
469+ else :
470+ # This happens when "Interrupt Kernel" is issued from the same kernel.
471+ # The responseStr contains the error message
472+ error_message = eval_response ["responseStr" ].strip ()
473+ raise Exception (error_message )
0 commit comments