Skip to content

Commit 6adacba

Browse files
rashedmytPrabhakar Kumar
authored andcommitted
Introducing support for MATLAB R2023b
fixes #50
1 parent e2029ed commit 6adacba

File tree

7 files changed

+117
-28
lines changed

7 files changed

+117
-28
lines changed

limitations.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -22,11 +22,11 @@ This package has some constraints and limitations:
2222
|![truncation-issue](https://github.com/mathworks/jupyter-matlab-proxy/raw/main/img/truncation-issue.png)|
2323
|-|
2424

25-
* Handles for graphics objects are scoped to a single cell. For example:
25+
* Handles for graphics objects are scoped to a single cell in MATLAB versions <= R2023a. For example:
2626
|![invalid-handle](https://github.com/mathworks/jupyter-matlab-proxy/raw/main/img/invalid-handle.png)|
2727
|-|
2828

29-
* Graphics functions like `gca, gcf, gco, gcbo, gcbf, clf, cla`, which access `current` handles, are scoped to a single notebook cell. The following example illustrates this:
29+
* Graphics functions like `gca, gcf, gco, gcbo, gcbf, clf, cla`, which access `current` handles, are scoped to a single notebook cell in MATLAB versions <= R2023a. The following example illustrates this:
3030
|![gca-issue](https://github.com/mathworks/jupyter-matlab-proxy/raw/main/img/gca-issue.png)|
3131
|-|
3232

src/jupyter_matlab_kernel/kernel.py

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -293,7 +293,7 @@ def do_execute(
293293
# Perform execution and categorization of outputs in MATLAB. Blocks
294294
# until execution results are received from MATLAB.
295295
outputs = mwi_comm_helpers.send_execution_request_to_matlab(
296-
self.murl, self.headers, code
296+
self.murl, self.headers, code, self.ident
297297
)
298298

299299
# Clear the output area of the current cell. This removes any previous
@@ -399,7 +399,9 @@ def do_history(
399399
)
400400

401401
def do_shutdown(self, restart):
402-
# TODO: Implement clean-up
402+
mwi_comm_helpers.send_shutdown_request_to_matlab(
403+
self.murl, self.headers, self.ident
404+
)
403405
return super().do_shutdown(restart)
404406

405407
# Helper functions

src/jupyter_matlab_kernel/matlab/+jupyter/execute.m

Lines changed: 58 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
% change without any prior notice. Usage of these undocumented APIs outside of
44
% these files is not supported.
55

6-
function result = execute(code)
6+
function result = execute(code, kernelId)
77
% EXECUTE A helper function for handling execution of MATLAB code and post-processing
88
% the outputs to conform to Jupyter API. We use the Live Editor API for majority
99
% of the work.
@@ -17,13 +17,13 @@
1717
% Embed user MATLAB code in a try-catch block for MATLAB versions less than R2022b.
1818
% This is will disable inbuilt ErrorRecovery mechanism. Any exceptions created in
1919
% user code would be handled by +jupyter/getOrStashExceptions.m
20-
if verLessThan('matlab', '9.13')
20+
if isMATLABReleaseOlderThan("R2022b")
2121
code = sprintf(['try\n'...
22-
'%s\n'...
23-
'catch JupyterKernelME\n'...
24-
'jupyter.getOrStashExceptions(JupyterKernelME)\n'...
25-
'clear JupyterKernelME\n'...
26-
'end'], code);
22+
'%s\n'...
23+
'catch JupyterKernelME\n'...
24+
'jupyter.getOrStashExceptions(JupyterKernelME)\n'...
25+
'clear JupyterKernelME\n'...
26+
'end'], code);
2727
end
2828

2929
% Value that needs to be shown in the error message when a particular error
@@ -32,17 +32,16 @@
3232
fileToShowErrors = 'Notebook';
3333

3434
% Prepare the input for the Live Editor API.
35-
jsonedRegionList = jsonencode(struct(...
36-
'regionLineNumber',1,...
37-
'regionString',code,...
38-
'regionNumber',0,...
39-
'endOfSection',true,...
40-
'sectionNumber',1));
35+
% 'requestId' - char array - UUID
36+
% 'editorId' - char array - unique identifier usually corresponding to a file.
4137
request = struct('requestId', 'jupyter_matlab_kernel',...
42-
'regionArray', jsonedRegionList,...
38+
'editorId', kernelId,...
4339
'fullText', code,...
4440
'fullFilePath', fileToShowErrors);
4541

42+
% Update additional fields in the request based on MATLAB and LiveEditor API version.
43+
request = updateRequest(request, code);
44+
4645
% Disable Hotlinks in the output captured. The hotlinks do not have a purpose
4746
% in Jupyter notebooks.
4847
hotlinksPreviousState = feature('hotlinks','off');
@@ -54,6 +53,49 @@
5453
% Post-process the outputs to conform to Jupyter API.
5554
result = processOutputs(resp.outputs);
5655

56+
% Helper function to update fields in the request based on MATLAB and LiveEditor
57+
% API version.
58+
function request = updateRequest(request, code)
59+
% Support for MATLAB version <= R2023a.
60+
if isMATLABReleaseOlderThan("R2023b")
61+
request = updateRequestFromBefore23b(request, code);
62+
else
63+
% Support for MATLAB version >= R2023b.
64+
65+
% To maintain backwards compatibility, each case in the switch
66+
% encodes conversion from the version number in the case
67+
% to the current version.
68+
switch matlab.internal.editor.getApiVersion('synchronous')
69+
case 1
70+
request = updateRequestFromVersion1(request, code);
71+
otherwise
72+
error("Invalid API version. Create an issue at https://github.com/mathworks/jupyter-matlab-proxy for further support.");
73+
end
74+
end
75+
76+
% Helper function to update fields in the request for MATLAB versions less than
77+
% R2023b
78+
function request = updateRequestFromBefore23b(request, code)
79+
jsonedRegionList = jsonencode(struct(...
80+
'regionLineNumber',1,...
81+
'regionString',code,...
82+
'regionNumber',0,...
83+
'endOfSection',true,...
84+
'sectionNumber',1));
85+
request.regionArray = jsonedRegionList;
86+
87+
% Helper function to update fields in the request when LiveEditor API version is 1.
88+
function request = updateRequestFromVersion1(request, code)
89+
request.sectionBoundaries = [];
90+
request.startLine = 1;
91+
request.endLine = numel(splitlines(code));
92+
93+
% Allow figures to be used across Notebook cells. Cleanup needs to be
94+
% done explicitly when the kernel shutsdown.
95+
request.shouldResetState = false;
96+
request.shouldDoFullCleanup = false;
97+
98+
% Helper function to process different types of outputs given by LiveEditor API.
5799
function result = processOutputs(outputs)
58100
result =cell(1,length(outputs));
59101
figureTrackingMap = containers.Map;
@@ -137,11 +179,11 @@
137179

138180
if isempty(webwindow)
139181
url = 'toolbox/matlab/codetools/liveeditor/index.html';
140-
182+
141183
% MATLAB versions R2020b and R2021a requires specifying the base url.
142184
% Not doing so results in the URL not being loaded with the error
143185
%"Not found. Request outside of context root".
144-
if verLessThan('matlab','9.11')
186+
if isMATLABReleaseOlderThan("R2021b")
145187
url = strcat(getenv("MWI_BASE_URL"), '/', url);
146188
end
147189
webwindow = matlab.internal.cef.webwindow(connector.getUrl(url));
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
% IMPORTANT NOTICE:
2+
% This file may contain calls to MathWorks internal APIs which are subject to
3+
% change without any prior notice. Usage of these undocumented APIs outside of
4+
% these files is not supported.
5+
6+
function output = shutdown(kernelId)
7+
% SHUTDOWN A helper function to perform cleanup activities when a kernel shuts down.
8+
9+
% Copyright 2023 The MathWorks, Inc.
10+
11+
import matlab.internal.editor.SynchronousEvaluationOutputsService
12+
13+
% Perform LiveEditor state cleanup of a given notebook in MATLAB versions >= R2023b
14+
if ~isMATLABReleaseOlderThan("R2023b")
15+
SynchronousEvaluationOutputsService.cleanup(kernelId);
16+
end
17+
18+
output = {};
19+
end

src/jupyter_matlab_kernel/matlab/processJupyterKernelRequest.m

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,9 +11,12 @@
1111
% on value of input request_type
1212
% - "execute"
1313
% - string - MATLAB code to be executed
14+
% - string - ID of the kernel
1415
% - "complete"
1516
% - string - MATLAB code
1617
% - number - cursor position
18+
% - "shutdown"
19+
% - string - ID of the kernel
1720
% Outputs:
1821
% - cell array on struct
1922
% - type - string - jupyter output type. Supported values are
@@ -47,10 +50,14 @@
4750
try
4851
switch(request_type)
4952
case 'execute'
50-
output = jupyter.execute(code);
53+
kernelId = varargin{2};
54+
output = jupyter.execute(code, kernelId);
5155
case 'complete'
5256
cursorPosition = varargin{2};
5357
output = jupyter.complete(code, cursorPosition);
58+
case 'shutdown'
59+
kernelId = varargin{1};
60+
output = jupyter.shutdown(kernelId);
5461
end
5562
catch ME
5663
% The code withing try block should be exception safe. In case anything we

src/jupyter_matlab_kernel/mwi_comm_helpers.py

Lines changed: 18 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -52,23 +52,23 @@ def check_licensing_status(data):
5252
return licensing_status
5353

5454

55-
def send_execution_request_to_matlab(url, headers, code):
55+
def send_execution_request_to_matlab(url, headers, code, kernelid):
5656
"""
5757
Evaluate MATLAB code and capture results.
5858
5959
Args:
60-
kernelid (string): A unique kernel identifier.
6160
url (string): Url of matlab-proxy server
6261
headers (dict): HTTP headers required for communicating with matlab-proxy
6362
code (string): MATLAB code to be evaluated
63+
kernelid (string): A unique kernel identifier.
6464
6565
Returns:
6666
List(dict): list of outputs captured during evaluation.
6767
6868
Raises:
6969
HTTPError: Occurs when connection to matlab-proxy cannot be established.
7070
"""
71-
return _send_jupyter_request_to_matlab(url, headers, "execute", [code])
71+
return _send_jupyter_request_to_matlab(url, headers, "execute", [code, kernelid])
7272

7373

7474
def send_completion_request_to_matlab(url, headers, code, cursor_pos):
@@ -101,6 +101,21 @@ def send_completion_request_to_matlab(url, headers, code, cursor_pos):
101101
return _send_jupyter_request_to_matlab(url, headers, "complete", [code, cursor_pos])
102102

103103

104+
def send_shutdown_request_to_matlab(url, headers, kernelid):
105+
"""
106+
Perform cleanup tasks related to kernel shutdown.
107+
108+
Args:
109+
url (string): Url of matlab-proxy server
110+
headers (dict): HTTP headers required for communicating with matlab-proxy
111+
kernelid (string): A unique kernel identifier.
112+
113+
Raises:
114+
HTTPError: Occurs when connection to matlab-proxy cannot be established.
115+
"""
116+
return _send_jupyter_request_to_matlab(url, headers, "shutdown", [kernelid])
117+
118+
104119
def send_interrupt_request_to_matlab(url, headers):
105120
req_body = {
106121
"messages": {

tests/unit/jupyter_matlab_kernel/test_mwi_comm_helpers.py

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -103,8 +103,9 @@ def mock_post(*args, **kwargs):
103103
url = ""
104104
headers = {}
105105
code = "placeholder for code"
106+
kernelid = ""
106107
with pytest.raises(HTTPError) as exceptionInfo:
107-
send_execution_request_to_matlab(url, headers, code)
108+
send_execution_request_to_matlab(url, headers, code, kernelid)
108109
assert mock_exception_message in str(exceptionInfo.value)
109110

110111

@@ -133,8 +134,9 @@ def mock_post(*args, **kwargs):
133134
url = ""
134135
headers = {}
135136
code = "placeholder for code"
137+
kernelid = ""
136138
with pytest.raises(HTTPError) as exceptionInfo:
137-
send_execution_request_to_matlab(url, headers, code)
139+
send_execution_request_to_matlab(url, headers, code, kernelid)
138140
assert str(exceptionInfo.value) == ""
139141

140142

@@ -175,8 +177,9 @@ def mock_post(*args, **kwargs):
175177
url = ""
176178
headers = {}
177179
code = "placeholder for code"
180+
kernelid = ""
178181
with pytest.raises(Exception) as exceptionInfo:
179-
send_execution_request_to_matlab(url, headers, code)
182+
send_execution_request_to_matlab(url, headers, code, kernelid)
180183
assert "Operation may have interrupted by user" in str(exceptionInfo.value)
181184

182185

@@ -212,8 +215,9 @@ def mock_post(*args, **kwargs):
212215
url = ""
213216
headers = {}
214217
code = "placeholder for code"
218+
kernelid = ""
215219
try:
216-
outputs = send_execution_request_to_matlab(url, headers, code)
220+
outputs = send_execution_request_to_matlab(url, headers, code, kernelid)
217221
except Exception as exceptionInfo:
218222
pytest.fail("Unexpected failured in execution request")
219223

0 commit comments

Comments
 (0)