Skip to content

Commit e8dc4b4

Browse files
Shushant Singhprabhakk-mw
authored andcommitted
Introduces MAGIC command parsing for MATLAB Kernels.
1 parent d723a2b commit e8dc4b4

19 files changed

+1649
-70
lines changed

README.md

Lines changed: 11 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
# MATLAB Integration _for Jupyter_
22

3-
[![GitHub Workflow Status](https://img.shields.io/github/actions/workflow/status/mathworks/jupyter-matlab-proxy/run-tests.yml?branch=main&logo=github)](https://github.com/mathworks/jupyter-matlab-proxy/actions) [![PyPI badge](https://img.shields.io/pypi/v/jupyter-matlab-proxy.svg?logo=pypi)](https://pypi.python.org/pypi/jupyter-matlab-proxy) [![codecov](https://codecov.io/gh/mathworks/jupyter-matlab-proxy/branch/main/graph/badge.svg?token=ZW3SESKCSS)](https://codecov.io/gh/mathworks/jupyter-matlab-proxy) [![Downloads](https://static.pepy.tech/personalized-badge/jupyter-matlab-proxy?period=month&units=international_system&left_color=grey&right_color=blue&left_text=PyPI%20downloads/month)](https://pepy.tech/project/jupyter-matlab-proxy)
3+
[![PyPI badge](https://img.shields.io/pypi/v/jupyter-matlab-proxy.svg?logo=pypi)](https://pypi.python.org/pypi/jupyter-matlab-proxy) [![codecov](https://codecov.io/gh/mathworks/jupyter-matlab-proxy/branch/main/graph/badge.svg?token=ZW3SESKCSS)](https://codecov.io/gh/mathworks/jupyter-matlab-proxy) [![Downloads](https://static.pepy.tech/personalized-badge/jupyter-matlab-proxy?period=month&units=international_system&left_color=grey&right_color=blue&left_text=PyPI%20downloads/month)](https://pepy.tech/project/jupyter-matlab-proxy)
44

55

66

@@ -34,7 +34,7 @@ From your Jupyter notebook or JupyterLab, you can also open the MATLAB developme
3434
- Linux®
3535
- MacOS
3636
- Windows® (supported from [v0.6.0](https://github.com/mathworks/jupyter-matlab-proxy/releases/tag/v0.6.0)).
37-
- Windows Subsystem for Linux (WSL 2) [Installation Guide](./install_guides/wsl2/README.md).
37+
- Windows Subsystem for Linux (WSL 2) [Installation Guide](https://github.com/mathworks/jupyter-matlab-proxy/blob/main/install_guides/wsl2/README.md).
3838

3939
* Python versions: 3.8 | 3.9 | 3.10 | 3.11
4040

@@ -148,11 +148,16 @@ This opens a Jupyter notebook that supports MATLAB.
148148
<p align="center"><img width="600" src="https://github.com/mathworks/jupyter-matlab-proxy/raw/main/img/jupyterlab-notebook.png"></p>
149149

150150

151-
- When you execute MATLAB code in a notebook for the first time, enter your MATLAB license information in the dialog box that appears. See [Licensing](https://github.com/mathworks/matlab-proxy/blob/main/MATLAB-Licensing-Info.md) for details. The MATLAB session can take a few minutes to start.
152-
- Multiple notebooks running on a Jupyter server share the underlying MATLAB process, so executing code in one notebook affects the workspace in others. If you work in several notebooks simultaneously, be aware that they share a workspace.
153-
- With MATLAB R2022b and later, you can define a local function at the end of the cell where you want to call it:
151+
### Notes
152+
153+
- **Licensing:** When you execute MATLAB code in a notebook for the first time, enter your MATLAB license information in the dialog box that appears. For details, see [Licensing](https://github.com/mathworks/matlab-proxy/blob/main/MATLAB-Licensing-Info.md). The MATLAB session can take a few minutes to start.
154+
- **Multiple notebooks:** Multiple notebooks running on a Jupyter server share the underlying MATLAB process, so executing code in one notebook affects the workspace in others. If you work in several notebooks simultaneously, be aware that they share a workspace.
155+
- **Local functions:** with MATLAB R2022b and later, you can define a local function at the end of the cell where you want to call it:
154156
<p><img width="350" src="https://github.com/mathworks/jupyter-matlab-proxy/raw/main/img/local_functions.png"></p>
155-
For technical details about how the MATLAB kernel works, see [MATLAB Kernel for Jupyter](https://github.com/mathworks/jupyter-matlab-proxy/blob/main/src/jupyter_matlab_kernel/README.md).
157+
158+
- **Magic Commands:** You can use predefined magic commands in a Jupyter notebook with the MATLAB kernel, and you can also implement your own. To see a list of predefined magic commands, run `%%lsmagic`. For details about using magic commands, see [Magic Commands for MATLAB Kernel](https://github.com/mathworks/jupyter-matlab-proxy/blob/main/src/jupyter_matlab_kernel/magics/README.md).
159+
160+
- **Kernel:** For technical details about the MATLAB kernel, see [MATLAB Kernel for Jupyter](https://github.com/mathworks/jupyter-matlab-proxy/blob/main/src/jupyter_matlab_kernel/README.md).
156161

157162
## Open MATLAB in a Browser
158163

@@ -189,8 +194,6 @@ This opens an untitled `.m` file where you can write MATLAB code with syntax hig
189194
* Currently, this package allows you to edit MATLAB `.m` files but not to execute them.
190195
* To open a new MATLAB `.m` file, you can also use the JupyterLab command palette. Press `CTRL+SHIFT+C`, then type `New MATLAB File` and press `Enter`.
191196

192-
193-
194197
## Limitations
195198

196199
* This package has limitations. For example, it does not support certain MATLAB commands. For details, see [Limitations](https://github.com/mathworks/jupyter-matlab-proxy/blob/main/limitations.md).

src/jupyter_matlab_kernel/kernel.py

Lines changed: 120 additions & 60 deletions
Original file line numberDiff line numberDiff line change
@@ -6,34 +6,26 @@
66
import sys
77
import time
88

9-
# Import Dependencies
9+
# Import Third-Party Dependencies
1010
import ipykernel.kernelbase
1111
import psutil
1212
import requests
13-
from matlab_proxy import settings as mwi_settings
14-
from matlab_proxy import util as mwi_util
1513
from requests.exceptions import HTTPError
1614

15+
# Import Dependencies
1716
from jupyter_matlab_kernel import mwi_comm_helpers, mwi_logger
17+
from jupyter_matlab_kernel.magic_execution_engine import (
18+
MagicExecutionEngine,
19+
get_completion_result_for_magics,
20+
)
21+
from jupyter_matlab_kernel.mwi_exceptions import MATLABConnectionError
22+
from matlab_proxy import settings as mwi_settings
23+
from matlab_proxy import util as mwi_util
1824

1925
_MATLAB_STARTUP_TIMEOUT = mwi_settings.get_process_startup_timeout()
2026
_logger = mwi_logger.get()
2127

2228

23-
class MATLABConnectionError(Exception):
24-
"""
25-
A connection error occurred while connecting to MATLAB.
26-
27-
Args:
28-
message (string): Error message to be displayed
29-
"""
30-
31-
def __init__(self, message=None):
32-
if message is None:
33-
message = 'Error connecting to MATLAB. Check the status of MATLAB by clicking the "Open MATLAB" button. Retry after ensuring MATLAB is running successfully'
34-
super().__init__(message)
35-
36-
3729
def is_jupyter_testing_enabled():
3830
"""
3931
Checks if testing mode is enabled
@@ -174,7 +166,7 @@ def start_matlab_proxy(logger=_logger):
174166
break
175167

176168
# Error out if the server is not found!
177-
if found_nb_server == False:
169+
if not found_nb_server:
178170
logger.error("Jupyter server associated with this MATLABKernel not found.")
179171
raise MATLABConnectionError(
180172
"""
@@ -224,7 +216,7 @@ def start_matlab_proxy(logger=_logger):
224216
return matlab_proxy_url, nb_server["base_url"], headers
225217

226218
logger.error(
227-
f"MATLABKernel could not communicate with matlab-proxy through Jupyter server"
219+
"MATLABKernel could not communicate with matlab-proxy through Jupyter server"
228220
)
229221
logger.error(f"Jupyter server:\n{nb_server}")
230222
raise MATLABConnectionError(
@@ -267,6 +259,8 @@ def __init__(self, *args, **kwargs):
267259
# multiple kernels which are running simultaneously
268260
self.log.debug(f"Initializing kernel with id: {self.ident}")
269261
self.log = self.log.getChild(f"{self.ident}")
262+
# Initialize the Magic Execution Engine.
263+
self.magic_engine = MagicExecutionEngine(self.log)
270264

271265
try:
272266
# Start matlab-proxy using the jupyter-matlab-proxy registered endpoint.
@@ -312,6 +306,29 @@ async def interrupt_request(self, stream, ident, parent):
312306

313307
self.session.send(stream, "interrupt_reply", content, parent, ident=ident)
314308

309+
def modify_kernel(self, states_to_modify):
310+
"""
311+
Used to modify MATLAB Kernel state
312+
Args:
313+
states_to_modify (dict): A key value pair of all the states to be modified.
314+
315+
"""
316+
self.log.debug(f"Modifying the kernel with {states_to_modify}")
317+
for key, value in states_to_modify.items():
318+
if hasattr(self, key):
319+
self.log.debug(f"set the value of {key} to {value}")
320+
setattr(self, key, value)
321+
322+
def handle_magic_output(self, output, outputs=None):
323+
if output["type"] == "modify_kernel":
324+
self.modify_kernel(output)
325+
else:
326+
self.display_output(output)
327+
if outputs is not None and not self.startup_checks_completed:
328+
# Outputs are cleared after startup_check.
329+
# Storing the magic outputs to display them after startup_check completes.
330+
outputs.append(output)
331+
315332
def do_execute(
316333
self,
317334
code,
@@ -328,44 +345,71 @@ def do_execute(
328345
"""
329346
self.log.debug(f"Received execution request from Jupyter with code:\n{code}")
330347
try:
348+
accumulated_magic_outputs = []
349+
performed_startup_checks = False
350+
351+
for output in self.magic_engine.process_before_cell_execution(
352+
code, self.execution_count
353+
):
354+
self.handle_magic_output(output, accumulated_magic_outputs)
355+
356+
skip_cell_execution = self.magic_engine.skip_cell_execution()
357+
self.log.debug(f"Skipping cell execution is set to {skip_cell_execution}")
358+
331359
# Complete one-time startup checks before sending request to MATLAB.
332360
# Blocking call, returns after MATLAB is started.
333-
if not self.startup_checks_completed:
334-
self.perform_startup_checks()
335-
self.display_output(
336-
{
337-
"type": "stream",
338-
"content": {
339-
"name": "stdout",
340-
"text": "Executing ...",
341-
},
342-
}
361+
if not skip_cell_execution:
362+
if not self.startup_checks_completed:
363+
self.perform_startup_checks()
364+
self.display_output(
365+
{
366+
"type": "stream",
367+
"content": {
368+
"name": "stdout",
369+
"text": "Executing ...",
370+
},
371+
}
372+
)
373+
if accumulated_magic_outputs:
374+
self.display_output(
375+
{"type": "clear_output", "content": {"wait": False}}
376+
)
377+
performed_startup_checks = True
378+
self.startup_checks_completed = True
379+
380+
if performed_startup_checks and accumulated_magic_outputs:
381+
for output in accumulated_magic_outputs:
382+
self.display_output(output)
383+
384+
# Perform execution and categorization of outputs in MATLAB. Blocks
385+
# until execution results are received from MATLAB.
386+
outputs = mwi_comm_helpers.send_execution_request_to_matlab(
387+
self.murl, self.headers, code, self.ident, self.log
343388
)
344-
self.startup_checks_completed = True
345389

346-
# Perform execution and categorization of outputs in MATLAB. Blocks
347-
# until execution results are received from MATLAB.
348-
outputs = mwi_comm_helpers.send_execution_request_to_matlab(
349-
self.murl, self.headers, code, self.ident, self.log
350-
)
390+
if performed_startup_checks and not accumulated_magic_outputs:
391+
self.display_output(
392+
{"type": "clear_output", "content": {"wait": False}}
393+
)
351394

352-
self.log.debug(
353-
"Received outputs after execution in MATLAB. Clearing output area"
354-
)
395+
self.log.debug(
396+
"Received outputs after execution in MATLAB. Clearing output area"
397+
)
398+
399+
# Display all the outputs produced during the execution of code.
400+
for idx in range(len(outputs)):
401+
data = outputs[idx]
402+
self.log.debug(f"Displaying output {idx+1}:\n{data}")
355403

356-
# Clear the output area of the current cell. This removes any previous
357-
# outputs before publishing new outputs.
358-
self.display_output({"type": "clear_output", "content": {"wait": False}})
404+
# Ignore empty values returned from MATLAB.
405+
if not data:
406+
continue
407+
self.display_output(data)
359408

360-
# Display all the outputs produced during the execution of code.
361-
for idx in range(len(outputs)):
362-
data = outputs[idx]
363-
self.log.debug(f"Displaying output {idx+1}:\n{data}")
409+
# Execute post execution of MAGICs
410+
for output in self.magic_engine.process_after_cell_execution():
411+
self.handle_magic_output(output)
364412

365-
# Ignore empty values returned from MATLAB.
366-
if not data:
367-
continue
368-
self.display_output(data)
369413
except Exception as e:
370414
self.log.error(
371415
f"Exception occurred while processing execution request:\n{e}"
@@ -380,8 +424,12 @@ def do_execute(
380424
# checks for subsequent execution requests
381425
self.startup_checks_completed = False
382426

427+
# Clearing lingering message "Executing..." before displaying the error message
428+
if performed_startup_checks and not accumulated_magic_outputs:
429+
self.display_output(
430+
{"type": "clear_output", "content": {"wait": False}}
431+
)
383432
# Send the exception message to the user.
384-
self.display_output({"type": "clear_output", "content": {"wait": False}})
385433
self.display_output(
386434
{
387435
"type": "stream",
@@ -421,19 +469,31 @@ def do_complete(self, code, cursor_pos):
421469

422470
# Fetch tab completion results. Blocks untils either tab completion
423471
# results are received from MATLAB or communication with MATLAB fails.
424-
try:
425-
completion_results = mwi_comm_helpers.send_completion_request_to_matlab(
426-
self.murl, self.headers, code, cursor_pos, self.log
427-
)
428-
except (MATLABConnectionError, HTTPError) as e:
429-
self.log.error(
430-
f"Exception occurred while sending shutdown request to MATLAB:\n{e}"
431-
)
472+
473+
magic_completion_results = get_completion_result_for_magics(
474+
code, cursor_pos, self.log
475+
)
432476

433477
self.log.debug(
434-
f"Received completion results from MATLAB:\n{completion_results}"
478+
f"Received Completion results from MAGIC:\n{magic_completion_results}"
435479
)
436480

481+
if magic_completion_results:
482+
completion_results = magic_completion_results
483+
else:
484+
try:
485+
completion_results = mwi_comm_helpers.send_completion_request_to_matlab(
486+
self.murl, self.headers, code, cursor_pos, self.log
487+
)
488+
except (MATLABConnectionError, HTTPError) as e:
489+
self.log.error(
490+
f"Exception occurred while sending shutdown request to MATLAB:\n{e}"
491+
)
492+
493+
self.log.debug(
494+
f"Received completion results from MATLAB:\n{completion_results}"
495+
)
496+
437497
return {
438498
"status": "ok",
439499
"matches": completion_results["matches"],
@@ -551,7 +611,7 @@ def perform_startup_checks(self):
551611
"type": "stream",
552612
"content": {
553613
"name": "stdout",
554-
"text": f"Starting MATLAB ...\n",
614+
"text": "Starting MATLAB ...\n",
555615
},
556616
}
557617
)

0 commit comments

Comments
 (0)