Skip to content

Commit e8dc4b4

Browse files
Shushant Singhprabhakk-mw
Shushant Singh
authored andcommittedAug 14, 2024
Introduces MAGIC command parsing for MATLAB Kernels.
1 parent d723a2b commit e8dc4b4

19 files changed

+1649
-70
lines changed
 

‎README.md

+11-8
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

+120-60
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
)

‎src/jupyter_matlab_kernel/magic_execution_engine.py

+613
Large diffs are not rendered by default.
+27
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
# Copyright 2024 The MathWorks, Inc.
2+
3+
import importlib
4+
from pathlib import Path
5+
6+
7+
def get_magic_names():
8+
"""
9+
Lists the names of all the magic commands.
10+
11+
Returns:
12+
[str]: All the available magics.
13+
"""
14+
module_name = __name__
15+
module_spec = importlib.util.find_spec(module_name)
16+
magic_names = []
17+
if module_spec and module_spec.origin:
18+
magic_path = Path(module_spec.origin).parent / "magics"
19+
magic_names = [
20+
s.replace(".py", "")
21+
for s in [
22+
f.name
23+
for f in magic_path.iterdir()
24+
if f.is_file() and f.name.endswith(".py")
25+
]
26+
]
27+
return magic_names
+73
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
# Magic Commands for MATLAB Kernel
2+
3+
You can use magic commands with the MATLAB kernel. You can use the predefined magic commands in this folder, and you can implement your own by following the steps below.
4+
5+
## Get Started
6+
7+
Magic commands for the MATLAB kernel are prefixed with two percentage symbols `%%` without whitespaces. For example, to list available magic commands, run `%%lsmagic`
8+
9+
Note that magic commands will only work at the beginning of cells, and will not work with MATLAB variables.
10+
11+
The magic commands `help` and `file` accept additional parameters. For example, to display information about a magic command, run `%%help` followed by the name of the magic as an argument: `%%help time`
12+
13+
This table lists the predefined magic commands you can use:
14+
15+
16+
|Name|Description|Additional Parameters|Constraints|Example command|
17+
|---|---|---|---|---|
18+
|lsmagic|List predefined magic commands.|||`%%lsmagic`|
19+
|help|Display information about provided magic command. | Name of magic command.|| `%%help file`|
20+
|time|Display time taken to execute a cell.|||`%%time`|
21+
|file|Save contents of cell as a file in the notebook folder. You can use this command to define and save new functions. For details, see the section below on how to [Create New Functions Using the %%file Magic Command](#create-new-functions-using-the-the-file-magic-command)|Name of saved file|The file magic command will save the contents of the cell, but not execute them in MATLAB|`%%file myfile.m`|
22+
23+
24+
To request a new magic command, [create an issue](https://github.com/mathworks/jupyter-matlab-proxy/issues/new/choose).
25+
26+
## Create Your Own Magic Commands
27+
28+
To implement your own magic commands, follow these steps. You can use the predefined magic commands as examples.
29+
30+
1. In the `magics` folder, create a Python file with the name of your new magic command, for example `<new_magic_name>.py`.
31+
2. Create a child class that inherits from the `MATLABMagic` class located in `jupyter_matlab_kernel/magics/base/matlab_magic.py` and modify these function members:
32+
1. info_about_magic
33+
2. skip_matlab_execution
34+
3. before_cell_execute
35+
4. after_cell_execute
36+
5. do_complete
37+
For details about these fields, see the descriptions in the `MATLABMagic` class.
38+
3. Add tests for your magic command in the `tests/unit/jupyter_matlab_kernel/magics` folder.
39+
40+
## Create New Functions Using the the %%file Magic Command
41+
42+
In a notebook cell you can define MATLAB functions that are scoped to that cell. To define a function scoped to all the cells in a notebook, you can use the `%%file` magic command. Define a new function and save it as a MATLAB `.m` file, using the name of the function as the file name. For example, to create a function called `myAdditionFunction(x, y)`, follow these steps:
43+
44+
1. In a notebook cell, use the `%%file` command and define the function.
45+
46+
```
47+
%%file myAdditionFunction.m
48+
49+
function addition = myAdditionFunction(x, y)
50+
addition = x + y;
51+
end
52+
```
53+
54+
2. Run the cell to create a file called `myAdditionFunction.m` in the same folder as your notebook.
55+
56+
57+
3. You can then use this function in other cells of the notebook.
58+
59+
```
60+
addition = myAdditionFunction(3, 4);
61+
disp(addition)
62+
```
63+
64+
Note: to use your function in MATLAB, remember to set your MATLAB search path to the folder containing your notebook. For more information on setting the search path, see [Change Folders on Search Path](https://www.mathworks.com/help/matlab/matlab_env/add-remove-or-reorder-folders-on-the-search-path.html).
65+
66+
67+
68+
69+
---
70+
71+
Copyright 2024 The MathWorks, Inc.
72+
73+
---
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,148 @@
1+
# Copyright 2024 The MathWorks, Inc.
2+
3+
from jupyter_matlab_kernel import mwi_logger
4+
5+
_logger = mwi_logger.get()
6+
7+
8+
class MATLABMagic:
9+
"""
10+
This class serves as the base class for all magic classes.
11+
Derived classes must override the following data members and methods:
12+
13+
1. info_about_magic: Provides details about the magic, including but not limited to basic information, usage, limitations, and examples.
14+
2. skip_matlab_execution (default: False): A boolean variable that determines whether the cell should be executed in MATLAB.
15+
3. before_cell_execute: This function gets executed before the execution of cell in MATLAB.
16+
4. after_cell_execute: This function gets executed after the execution of cell in MATALB.
17+
5. do_complete (optional): Provides tab completion suggestions for the parameters of magics.
18+
19+
Refer to the respective docstrings of these members and methods for an in-depth description.
20+
21+
Args:
22+
parameters ([str]): The parameters passed with the magic commands.
23+
cell_code (str): The code contained in the cell which was executed by the user.
24+
magic_position_from_top (int): The execution order of the magic command from the top of the cell.
25+
execution_count (int): The execution count of the cell in which the magic was executed.
26+
line_number (int): The line number within the cell where the magic command is located.
27+
28+
Example:
29+
For magic code execution:
30+
magic_object = <magic_name>(["param1", "param2"], "a = 1;\nb = 2", 1, 1)
31+
magic_object.before_cell_execute()
32+
magic_object.after_cell_execute()
33+
For tab completion:
34+
magic_object = <magic_name>()
35+
magic_object.do_complete(["par"], 1, 2)
36+
"""
37+
38+
info_about_magic = "No information available"
39+
skip_matlab_execution = False
40+
41+
def __init__(
42+
self,
43+
parameters=[],
44+
cell_code="",
45+
magic_position_from_top=1,
46+
execution_count=1,
47+
line_number=1,
48+
logger=_logger,
49+
):
50+
self.parameters = parameters
51+
self.cell_code = cell_code
52+
self.magic_position_from_top = magic_position_from_top
53+
self.execution_count = execution_count
54+
self.line_number = line_number
55+
self.logger = logger
56+
57+
def before_cell_execute(self):
58+
"""
59+
Gets executed before the execution of MATLAB code.
60+
61+
Raises:
62+
MagicError: Error raised are displayed in the notebook.
63+
64+
Yields:
65+
dict: The next output of the magic.
66+
The dictionary must contain a key called "type".
67+
For example:
68+
1. To display execution result:
69+
{
70+
"type": "execute_result",
71+
"mimetype": ["text/plain", "text/html"],
72+
"value": [output, f"<html><body>{output}</body></html>"],
73+
}
74+
2. To display warnings:
75+
{
76+
"type": "execute_result",
77+
"mimetype": ["text/html"],
78+
"value": [f"<html><body><p style='color:orange;'>warning: {warning}</p></body></html>"],
79+
}
80+
3. To modify kernel:
81+
{
82+
"type": "modify_kernel",
83+
"murl": new_url,
84+
"headers": new_headers,
85+
}
86+
default: Empty dict ({}).
87+
"""
88+
yield {}
89+
90+
def after_cell_execute(self):
91+
"""
92+
Gets executed after the execution of MATLAB code.
93+
94+
Raises:
95+
MagicError: Error raised are displayed in the notebook.
96+
97+
Yields:
98+
dict: The next output of the magic.
99+
The dictionary must contain a key called "type".
100+
For example:
101+
1. To display execution result:
102+
{
103+
"type": "execute_result",
104+
"mimetype": ["text/plain", "text/html"],
105+
"value": [output, f"<html><body>{output}</body></html>"],
106+
}
107+
2. To display warnings:
108+
{
109+
"type": "execute_result",
110+
"mimetype": ["text/html"],
111+
"value": [f"<html><body><p style='color:orange;'>warning: {warning}</p></body></html>"],
112+
}
113+
3. To modify kernel:
114+
{
115+
"type": "modify_kernel",
116+
"murl": new_url,
117+
"headers": new_headers,
118+
}
119+
default: Empty dict ({}).
120+
"""
121+
yield {}
122+
123+
@classmethod
124+
def get_info_about_magic(cls):
125+
"""
126+
Returns (string): The information about the magic to be displayed to the user.
127+
"""
128+
return cls.info_about_magic
129+
130+
def should_skip_matlab_execution(self):
131+
"""
132+
Returns (boolean): States whether the magic blocks cell from being executed in MATLAB.
133+
"""
134+
return self.skip_matlab_execution
135+
136+
def do_complete(self, parameters, parameter_pos, cursor_pos):
137+
"""
138+
Used to give suggestions for tab completions
139+
140+
Args:
141+
parameters ([str]): List of parameters which were passed along with magic command.
142+
parameter_pos (int): The index of the parameter for which the tab completion was requested.
143+
cursor_pos (int): The position of the cursor in the parameter from where the tab completion was requested.
144+
145+
Returns:
146+
[str]: An array of string containing all the possible suggestions.
147+
"""
148+
return []
+38
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
# Copyright 2024 The MathWorks, Inc.
2+
3+
from jupyter_matlab_kernel.magics.base.matlab_magic import MATLABMagic
4+
from jupyter_matlab_kernel.mwi_exceptions import MagicError
5+
6+
7+
class file(MATLABMagic):
8+
9+
info_about_magic = """Save contents of cell to a specified file in the notebook folder.
10+
Example:
11+
%%file myfile.m
12+
Save contents of cell to a file named myfile.m in the notebook folder."""
13+
14+
skip_matlab_execution = True
15+
16+
def before_cell_execute(self):
17+
if len(self.parameters) < 1:
18+
raise MagicError("The file magic expects the name of a file as a argument.")
19+
elif len(self.parameters) > 1:
20+
raise MagicError("The file magic expects a single file name as a argument.")
21+
cell_code = self.cell_code.split("\n")
22+
# Remove the lines of code before file magic command.
23+
cell_code = "\n".join(cell_code[self.line_number :])
24+
if cell_code == "":
25+
raise MagicError("The cell is empty.")
26+
try:
27+
with open(str(self.parameters[0]), "w") as file:
28+
file.write(cell_code)
29+
except Exception as e:
30+
raise MagicError(
31+
f"An error occurred while creating or writing to the file '{self.parameters[0]}':\n{e}"
32+
) from e
33+
output = f"File {self.parameters[0]} created successfully."
34+
yield {
35+
"type": "execute_result",
36+
"mimetype": ["text/plain", "text/html"],
37+
"value": [output, f"<html><body><pre>{output}</pre></body></html>"],
38+
}
+59
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
# Copyright 2024 The MathWorks, Inc.
2+
3+
from jupyter_matlab_kernel.magics.base.matlab_magic import MATLABMagic
4+
from jupyter_matlab_kernel.magic_helper import get_magic_names
5+
from jupyter_matlab_kernel.mwi_exceptions import MagicError
6+
import importlib
7+
8+
9+
class help(MATLABMagic):
10+
11+
info_about_magic = "Provide information about the specified magic command."
12+
13+
def before_cell_execute(self):
14+
if len(self.parameters) != 1:
15+
raise MagicError(
16+
"The help magic expects a single magic name as a argument."
17+
)
18+
magic_name = self.parameters[0]
19+
output = ""
20+
magics_info = self.__get_help(magic_name)
21+
if magics_info:
22+
output = output + f"{magic_name} Magic: {magics_info}\n"
23+
if output == "":
24+
raise MagicError(f"The Magic {magic_name} does not exist.")
25+
yield {
26+
"type": "execute_result",
27+
"mimetype": ["text/plain", "text/html"],
28+
"value": [output, f"<html><body><pre>{output}</pre></body></html>"],
29+
}
30+
31+
def do_complete(self, parameters, parameter_pos, cursor_pos):
32+
matches = []
33+
if parameter_pos == 1:
34+
if cursor_pos == 0:
35+
matches = get_magic_names()
36+
else:
37+
matches = [
38+
s
39+
for s in get_magic_names()
40+
if s.startswith(parameters[0][:cursor_pos])
41+
]
42+
return matches
43+
44+
def __get_help(self, module_name):
45+
46+
full_module_name = f"jupyter_matlab_kernel.magics.{module_name}"
47+
print(full_module_name)
48+
49+
try:
50+
module = importlib.import_module(full_module_name)
51+
52+
if hasattr(module, module_name):
53+
magic_class = getattr(module, module_name)
54+
return magic_class.get_info_about_magic()
55+
else:
56+
return None
57+
except Exception as e:
58+
self.logger.error(f"Error using help magic: {e}")
59+
return None
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
# Copyright 2024 The MathWorks, Inc.
2+
3+
from jupyter_matlab_kernel.magics.base.matlab_magic import MATLABMagic
4+
from jupyter_matlab_kernel.magic_helper import get_magic_names
5+
from jupyter_matlab_kernel.mwi_exceptions import MagicError
6+
7+
8+
class lsmagic(MATLABMagic):
9+
10+
info_about_magic = "List available magic commands."
11+
12+
def before_cell_execute(self):
13+
if len(self.parameters) != 0:
14+
raise MagicError("The lsmagic magic does not expect any arguments.")
15+
display_magics = ["%%" + s for s in get_magic_names()]
16+
output = f"Available magic commands: {display_magics}."
17+
yield {
18+
"type": "execute_result",
19+
"mimetype": ["text/plain", "text/html"],
20+
"value": [output, f"<html><body>{output}</body></html>"],
21+
}
+43
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
# Copyright 2024 The MathWorks, Inc.
2+
3+
import time as timer
4+
5+
from jupyter_matlab_kernel.magics.base.matlab_magic import MATLABMagic
6+
from jupyter_matlab_kernel.mwi_exceptions import MagicError
7+
8+
9+
class time(MATLABMagic):
10+
11+
info_about_magic = "Display time taken to execute a cell."
12+
start_time = None
13+
14+
def format_duration(self, seconds):
15+
intervals = [
16+
("hours", 3600),
17+
("minutes", 60),
18+
("seconds", 1),
19+
("milliseconds", 1e-3),
20+
]
21+
22+
for name, count in intervals:
23+
if seconds >= count:
24+
value = seconds / count
25+
return f"{value:.2f} {name}"
26+
27+
return f"{seconds * 1e3:.2f} milliseconds"
28+
29+
def before_cell_execute(self):
30+
if len(self.parameters) != 0:
31+
raise MagicError("time magic does not expect any arguments.")
32+
self.start_time = timer.time()
33+
yield {}
34+
35+
def after_cell_execute(self):
36+
elapsed_time = timer.time() - self.start_time
37+
formatted_duration = self.format_duration(elapsed_time)
38+
output = f"Execution of the cell took {formatted_duration} to run."
39+
yield {
40+
"type": "execute_result",
41+
"mimetype": ["text/plain", "text/html"],
42+
"value": [output, f"<html><body>{output}</body></html>"],
43+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
# Copyright 2024 The MathWorks, Inc.
2+
3+
4+
class MagicError(Exception):
5+
"""Custom exception for errors inside magic class
6+
7+
Args:
8+
message (string): Error message to be displayed
9+
"""
10+
11+
def __init__(self, message=None):
12+
if message is None:
13+
message = "An uncaught error occurred during magic execution."
14+
super().__init__(message)
15+
16+
17+
class MagicExecutionEngineError(Exception):
18+
"""Custom exception for error in Magic Execution Engine.
19+
20+
Args:
21+
message (string): Error message to be displayed
22+
"""
23+
24+
def __init__(self, message=None):
25+
if message is None:
26+
message = "An uncaught error occurred in the Magic Execution Engine."
27+
super().__init__(message)
28+
29+
30+
class MATLABConnectionError(Exception):
31+
"""
32+
A connection error occurred while connecting to MATLAB.
33+
34+
Args:
35+
message (string): Error message to be displayed
36+
"""
37+
38+
def __init__(self, message=None):
39+
if message is None:
40+
message = 'Error connecting to MATLAB. Check the status of MATLAB by clicking the "Open MATLAB" button. Retry after ensuring MATLAB is running successfully'
41+
super().__init__(message)

‎tests/integration/test_matlab_integration.py

+11-1
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
# Copyright 2023 The MathWorks, Inc.
1+
# Copyright 2023-2024 The MathWorks, Inc.
22
# Integration tests with real MATLAB in the loop
33

44
import os
@@ -103,6 +103,16 @@ def test_matlab_kernel_peaks(self):
103103
"No figure was generated in output",
104104
)
105105

106+
def test_magics(self):
107+
"""Validates if '%%lsmagic' commands lists the available magics in the cell output"""
108+
_, output_msgs = self._run_code(code="%%lsmagic")
109+
output_text = self._get_output_text(output_msgs)
110+
self.assertIn("Available magic commands:", output_text)
111+
self.assertIn("%%lsmagic", output_text)
112+
self.assertIn("%%file", output_text)
113+
self.assertIn("%%time", output_text)
114+
self.assertIn("%%help", output_text)
115+
106116
# ---- Utility Functions ----
107117
def _run_code(self, code, timeout=30):
108118
"""Runs code in Jupyter notebook cell"""
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
# Copyright 2024 The MathWorks, Inc.
2+
3+
import os
4+
import pytest
5+
6+
from jupyter_matlab_kernel.magics.file import file
7+
from jupyter_matlab_kernel.mwi_exceptions import MagicError
8+
9+
10+
@pytest.fixture
11+
def temp_dir(tmp_path):
12+
yield tmp_path
13+
14+
15+
@pytest.mark.parametrize(
16+
"parameters, cell_code",
17+
[
18+
pytest.param(
19+
[], "%%file\nsome_code", id="no parameters and non-empty cell_code"
20+
),
21+
pytest.param(
22+
["myfunc1.m", "myfunc2.m"],
23+
"%%file myfunc1.m myfunc2.m\nsome_code",
24+
id="2 parameters and non-empty cell_code",
25+
),
26+
pytest.param(
27+
["myfunc1.m"], "%%file myfunc1.m", id="1 parameters and empty cell_code"
28+
),
29+
],
30+
)
31+
def test_exceptions_in_file_magic(parameters, cell_code, temp_dir):
32+
parameters = [temp_dir / s for s in parameters]
33+
magic_object = file(parameters, cell_code)
34+
before_cell_executor = magic_object.before_cell_execute()
35+
with pytest.raises(MagicError):
36+
next(before_cell_executor)
37+
with pytest.raises(Exception):
38+
next(before_cell_executor)
39+
40+
41+
def test_file_creation(temp_dir):
42+
file_path = temp_dir / "myfunc1.m"
43+
magic_object = file([file_path], "%%file myfunc1.m\nmycode")
44+
before_cell_executor = magic_object.before_cell_execute()
45+
output = next(before_cell_executor)
46+
expected_output = "myfunc1.m created successfully."
47+
assert expected_output in output["value"][0]
48+
assert os.path.exists(file_path), f"File {file_path} does not exist."
49+
with pytest.raises(Exception):
50+
next(before_cell_executor)
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
1+
# Copyright 2024 The MathWorks, Inc.
2+
3+
import pytest
4+
5+
from jupyter_matlab_kernel.magics.help import help
6+
from jupyter_matlab_kernel.mwi_exceptions import MagicError
7+
8+
9+
def test_help_magic():
10+
magic_object = help(["lsmagic"])
11+
before_cell_executor = magic_object.before_cell_execute()
12+
output = next(before_cell_executor)
13+
expected_output = "List available magic commands."
14+
assert expected_output in output["value"][0]
15+
16+
17+
@pytest.mark.parametrize(
18+
"parameters",
19+
[
20+
pytest.param([], id="no magic name should throw exception"),
21+
pytest.param(
22+
["fakemagic"],
23+
id="magic which does not exist should throw exception",
24+
),
25+
pytest.param(
26+
["lsmagic", "help"],
27+
id="more than one parameter should throw exception",
28+
),
29+
],
30+
)
31+
def test_help_magic_exceptions(parameters):
32+
magic_object = help(parameters)
33+
before_cell_executor = magic_object.before_cell_execute()
34+
with pytest.raises(MagicError):
35+
next(before_cell_executor)
36+
37+
38+
@pytest.mark.parametrize(
39+
"parameters, parameter_pos, cursor_pos, expected_output",
40+
[
41+
pytest.param(
42+
["ls"],
43+
1,
44+
1,
45+
{"lsmagic"},
46+
id="ls as parameter with parameter and cursor position as 1",
47+
),
48+
pytest.param(
49+
[""],
50+
1,
51+
1,
52+
{"lsmagic", "help", "file", "time"},
53+
id="no parameter with parameter and cursor position as 1",
54+
),
55+
pytest.param(
56+
["t"],
57+
2,
58+
1,
59+
set([]),
60+
id="t as parameter with parameter position as 2 and cursor position as 1",
61+
),
62+
pytest.param(
63+
["magic"],
64+
1,
65+
4,
66+
set([]),
67+
id="magic as parameter with parameter position as 1 and cursor position as 4",
68+
),
69+
],
70+
)
71+
def test_do_complete_in_help_magic(
72+
parameters, parameter_pos, cursor_pos, expected_output
73+
):
74+
magic_object = help()
75+
output = magic_object.do_complete(parameters, parameter_pos, cursor_pos)
76+
assert expected_output.issubset(set(output))
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
# Copyright 2024 The MathWorks, Inc.
2+
3+
import pytest
4+
5+
from jupyter_matlab_kernel.magics.lsmagic import lsmagic
6+
from jupyter_matlab_kernel.mwi_exceptions import MagicError
7+
8+
9+
def test_lsmagic_output():
10+
magic_object = lsmagic()
11+
before_cell_executor = magic_object.before_cell_execute()
12+
output = next(before_cell_executor)
13+
expected_outputs = ["Available magic commands:", "%%lsmagic", "%%help"]
14+
assert all(
15+
expected_output in output["value"][0] for expected_output in expected_outputs
16+
)
17+
with pytest.raises(Exception):
18+
next(before_cell_executor)
19+
20+
21+
def test_lsmagic_with_parameters():
22+
magic_object = lsmagic(["parameter1"])
23+
before_cell_executor = magic_object.before_cell_execute()
24+
with pytest.raises(MagicError):
25+
next(before_cell_executor)
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
# Copyright 2024 The MathWorks, Inc.
2+
3+
import pytest
4+
5+
from jupyter_matlab_kernel.magics.time import time
6+
from jupyter_matlab_kernel.mwi_exceptions import MagicError
7+
8+
9+
def test_time_output():
10+
magic_object = time()
11+
before_cell_executor = magic_object.before_cell_execute()
12+
next(before_cell_executor)
13+
with pytest.raises(Exception):
14+
next(before_cell_executor)
15+
after_cell_executor = magic_object.after_cell_execute()
16+
output = next(after_cell_executor)
17+
expected_output = "Execution of the cell took"
18+
assert expected_output in output["value"][0]
19+
with pytest.raises(Exception):
20+
next(after_cell_executor)
21+
22+
23+
def test_time_with_parameters():
24+
magic_object = time(["parameter1"])
25+
before_cell_executor = magic_object.before_cell_execute()
26+
with pytest.raises(MagicError):
27+
next(before_cell_executor)
28+
29+
30+
@pytest.mark.parametrize(
31+
"seconds, expected",
32+
[
33+
pytest.param(3600, "1.00 hours", id="1 hours"),
34+
pytest.param(3599, "59.98 minutes", id="59.98 minutes"),
35+
pytest.param(180, "3.00 minutes", id="3 minutes"),
36+
pytest.param(60, "1.00 minutes", id="1 minute"),
37+
pytest.param(45, "45.00 seconds", id="45 seconds"),
38+
pytest.param(1, "1.00 seconds", id="1 second"),
39+
pytest.param(0.5, "500.00 milliseconds", id="500 milliseconds"),
40+
pytest.param(0.05, "50.00 milliseconds", id="50 milliseconds"),
41+
pytest.param(0.001, "1.00 milliseconds", id="1 millisecond"),
42+
pytest.param(0, "0.00 milliseconds", id="0 milliseconds"),
43+
pytest.param(0.0005, "0.50 milliseconds", id="0.5 milliseconds"),
44+
],
45+
)
46+
def test_time_format_duration(seconds, expected):
47+
magic_object = time()
48+
assert magic_object.format_duration(seconds) == expected
49+
50+
51+
if __name__ == "__main__":
52+
pytest.main()

‎tests/unit/jupyter_matlab_kernel/test_kernel.py

+3-1
Original file line numberDiff line numberDiff line change
@@ -3,10 +3,12 @@
33
# This file contains tests for jupyter_matlab_kernel.kernel
44
import mocks.mock_jupyter_server as MockJupyterServer
55
import pytest
6-
from jupyter_matlab_kernel.kernel import MATLABConnectionError, start_matlab_proxy
6+
from jupyter_matlab_kernel.kernel import start_matlab_proxy
77
from jupyter_server import serverapp
88
from mocks.mock_jupyter_server import MockJupyterServerFixture
99

10+
from jupyter_matlab_kernel.mwi_exceptions import MATLABConnectionError
11+
1012

1113
def test_start_matlab_proxy_without_jupyter_server():
1214
"""
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,223 @@
1+
# Copyright 2024 The MathWorks, Inc.
2+
3+
import pytest
4+
5+
from jupyter_matlab_kernel.magic_execution_engine import (
6+
MagicExecutionEngine,
7+
get_completion_result_for_magics,
8+
)
9+
10+
from jupyter_matlab_kernel.mwi_exceptions import MagicExecutionEngineError
11+
12+
13+
@pytest.mark.parametrize(
14+
"cell_code, skip_cell_execution",
15+
[
16+
pytest.param(
17+
"%%lsmagic",
18+
True,
19+
id="magic command without any MATLAB code does not require cell execution",
20+
),
21+
pytest.param(
22+
"%%lsmagic\n%%help lsmagic",
23+
True,
24+
id="magic commands without any MATLAB code does not require cell execution",
25+
),
26+
pytest.param(
27+
"%%lsmagic\na=1",
28+
False,
29+
id="magic command with MATLAB code requires cell execution",
30+
),
31+
pytest.param(
32+
"%% not_magic", False, id="Section creator requires cell execution"
33+
),
34+
pytest.param("%#", False, id="Special commands requires cell execution"),
35+
pytest.param(
36+
"% This is a comment", False, id="A comment also triggers cell execution"
37+
),
38+
pytest.param(
39+
"%! Some special command",
40+
False,
41+
id="Special command followed by some line requires cell execution",
42+
),
43+
pytest.param("%%", False, id="Only two percent signs require cell execution"),
44+
pytest.param(
45+
"%% ",
46+
False,
47+
id="Two percent signs followed by a whitespace require cell execution",
48+
),
49+
pytest.param(
50+
"%Some code",
51+
False,
52+
id="Percent directly followed by character requires cell execution",
53+
),
54+
],
55+
)
56+
def test_cell_code_which_can_trigger_matlab_execution(cell_code, skip_cell_execution):
57+
magic_executor = MagicExecutionEngine()
58+
process_before_cell_execution = magic_executor.process_before_cell_execution(
59+
cell_code, 1
60+
)
61+
for output in process_before_cell_execution:
62+
assert isinstance(output, dict)
63+
assert magic_executor.skip_cell_execution() is skip_cell_execution
64+
65+
66+
@pytest.mark.parametrize(
67+
"cell_code",
68+
[
69+
pytest.param("%%lsmagic\na=1", id="correct magic definition no error"),
70+
pytest.param(
71+
"%%lsmagic\n%%time\na=1",
72+
id="consecutive magic commands on top of the cell no error",
73+
),
74+
pytest.param(
75+
"a=1\n%%lsmagic",
76+
id="magic not on top of the cell should not error and be treated as comments",
77+
),
78+
pytest.param(
79+
"%%lsmagic\na=1\n%%time",
80+
id="magic followed by MATLAB code followed by magic should not error as the second magic is treated as a comment",
81+
),
82+
],
83+
)
84+
def test_correct_magic_definition(cell_code):
85+
magic_executor = MagicExecutionEngine()
86+
process_before_cell_execution = magic_executor.process_before_cell_execution(
87+
cell_code, 1
88+
)
89+
process_after_cell_execution = magic_executor.process_after_cell_execution()
90+
for output in process_before_cell_execution:
91+
assert isinstance(output, dict)
92+
for output in process_after_cell_execution:
93+
assert isinstance(output, dict)
94+
95+
96+
@pytest.mark.parametrize(
97+
"cell_code",
98+
[
99+
pytest.param("%%mymagic", id="invalid magic should error"),
100+
pytest.param("%%lsmagic help", id="magic with invalid parameter should error"),
101+
],
102+
)
103+
def test_incorrect_magic_definition(cell_code):
104+
magic_executor = MagicExecutionEngine()
105+
process_before_cell_execution = magic_executor.process_before_cell_execution(
106+
cell_code, 1
107+
)
108+
process_after_cell_execution = magic_executor.process_after_cell_execution()
109+
with pytest.raises(MagicExecutionEngineError):
110+
for output in process_before_cell_execution:
111+
assert isinstance(output, dict)
112+
for output in process_after_cell_execution:
113+
assert isinstance(output, dict)
114+
115+
116+
@pytest.mark.parametrize(
117+
"cell_code, cursor_pos, expected_output, expected_start, expected_end",
118+
[
119+
pytest.param(
120+
"%%",
121+
2,
122+
{"lsmagic", "help", "time", "file"},
123+
2,
124+
2,
125+
id="Two percentage without whitespace suggests all magic commands",
126+
),
127+
pytest.param("%%ls", 3, {"lsmagic"}, 2, 3, id="%%ls suggests lsmagic"),
128+
pytest.param(
129+
"%%lsmagi",
130+
4,
131+
{"lsmagic"},
132+
2,
133+
4,
134+
id="%%lsmagi at cursor postion 4 replaces lsmagic from 2nd position to 4th position",
135+
),
136+
pytest.param(
137+
" %%",
138+
3,
139+
{"lsmagic", "help", "time", "file"},
140+
3,
141+
3,
142+
id="%% preceeded by whitespace suggests all magic commands",
143+
),
144+
pytest.param(
145+
"%%help ",
146+
7,
147+
{"lsmagic", "help", "time", "file"},
148+
7,
149+
7,
150+
id="completion request after magic commands are passed to respective magic functions",
151+
),
152+
pytest.param(
153+
"%%help ",
154+
10,
155+
{"lsmagic", "help", "time", "file"},
156+
10,
157+
10,
158+
id="whitespace after a valid magic command does not affect the completion result",
159+
),
160+
pytest.param(
161+
"%%lsmagic",
162+
9,
163+
{"lsmagic"},
164+
2,
165+
9,
166+
id="complete magic name still suggests the magic name for completion",
167+
),
168+
pytest.param(
169+
"%%lsmagic ",
170+
10,
171+
set([]),
172+
10,
173+
10,
174+
id="completion request after magic commands returns empty matches",
175+
),
176+
pytest.param(
177+
"%%lsmagic abc",
178+
13,
179+
set([]),
180+
10,
181+
13,
182+
id="parameter completion request in magic parameters returns empty matches",
183+
),
184+
pytest.param(
185+
"%%fakemagic a",
186+
13,
187+
set([]),
188+
12,
189+
13,
190+
id="parameter completion request for invalid magic command returns empty matches",
191+
),
192+
],
193+
)
194+
def test_get_completion_result_for_magics(
195+
cell_code, cursor_pos, expected_output, expected_start, expected_end
196+
):
197+
output = get_completion_result_for_magics(cell_code, cursor_pos)
198+
assert expected_output.issubset(set(output["matches"]))
199+
assert expected_start == output["start"]
200+
assert expected_end == output["end"]
201+
202+
203+
@pytest.mark.parametrize(
204+
"cell_code, cursor_pos",
205+
[
206+
pytest.param("%", 1, id="single percent treated as comment returns None"),
207+
pytest.param(
208+
"a=1\n%%h",
209+
7,
210+
id=" don't shows suggestions if preceded by MATLAB code",
211+
),
212+
pytest.param("pea", 3, id="MATLAB code so return None"),
213+
pytest.param("%% l", 4, id="Section creator should return None"),
214+
pytest.param(
215+
"%% ",
216+
3,
217+
id="%% followed by a whitespace are considered Section creators. Return None",
218+
),
219+
],
220+
)
221+
def test_no_output_in_get_completion_result_for_magics(cell_code, cursor_pos):
222+
output = get_completion_result_for_magics(cell_code, cursor_pos)
223+
assert output is None
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
# Copyright 2024 The MathWorks, Inc.
2+
3+
from jupyter_matlab_kernel.magic_helper import get_magic_names
4+
5+
6+
def test_get_magic_names():
7+
output = get_magic_names()
8+
expected_output = {"lsmagic", "help", "time"}
9+
assert expected_output.issubset(set(output))
10+
11+
12+
def test_get_magic_names_only_outputs_py_files():
13+
output = get_magic_names()
14+
unexpected_output = {"README.md"}
15+
assert not unexpected_output.issubset(set(output))

0 commit comments

Comments
 (0)
Please sign in to comment.