Skip to content

Commit e49be6f

Browse files
rashedmytPrabhakar Kumar
authored andcommitted
Updates MATLABKernel to use aiohttp instead of requests for HTTP communication.
1 parent f77567b commit e49be6f

File tree

10 files changed

+539
-468
lines changed

10 files changed

+539
-468
lines changed

pyproject.toml

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,7 @@ classifiers = [
4343
# Existing installations where jupyter-matlab-proxy is being deployed would not get this update.
4444
# It is safe to update simpervisor to v1.0.0 while keeping jupyter-server-proxy at its existing level.
4545
dependencies = [
46+
"aiohttp",
4647
"jupyter-server-proxy",
4748
"simpervisor>=1.0.0",
4849
"matlab-proxy>=0.16.0",
@@ -55,7 +56,15 @@ dependencies = [
5556
Homepage = "https://github.com/mathworks/jupyter-matlab-proxy"
5657

5758
[project.optional-dependencies]
58-
dev = ["black", "pytest", "pytest-cov", "jupyter-kernel-test", "pytest-playwright"]
59+
dev = [
60+
"black",
61+
"jupyter-kernel-test",
62+
"pytest",
63+
"pytest-aiohttp",
64+
"pytest-asyncio",
65+
"pytest-cov",
66+
"pytest-playwright"
67+
]
5968

6069
[project.entry-points.jupyter_serverproxy_servers]
6170
matlab = "jupyter_matlab_proxy:setup_matlab"
@@ -107,6 +116,7 @@ npm = ["jlpm"]
107116
[tool.pytest.ini_options]
108117
minversion = "6.0"
109118
addopts = "-ra -q"
119+
asyncio_mode = "auto"
110120
testpaths = ["tests"]
111121
filterwarnings = ["ignore::DeprecationWarning", "ignore::RuntimeWarning"]
112122

src/jupyter_matlab_kernel/kernel.py

Lines changed: 72 additions & 60 deletions
Original file line numberDiff line numberDiff line change
@@ -2,25 +2,28 @@
22
# Implementation of MATLAB Kernel
33

44
# Import Python Standard Library
5+
import asyncio
6+
import http
57
import os
68
import sys
79
import time
810

9-
# Import Third-Party Dependencies
11+
# Import Dependencies
12+
import aiohttp
13+
import aiohttp.client_exceptions
1014
import ipykernel.kernelbase
1115
import psutil
1216
import requests
13-
from requests.exceptions import HTTPError
17+
from matlab_proxy import settings as mwi_settings
18+
from matlab_proxy import util as mwi_util
1419

15-
# Import Dependencies
16-
from jupyter_matlab_kernel import mwi_comm_helpers, mwi_logger
20+
from jupyter_matlab_kernel import mwi_logger
1721
from jupyter_matlab_kernel.magic_execution_engine import (
1822
MagicExecutionEngine,
1923
get_completion_result_for_magics,
2024
)
25+
from jupyter_matlab_kernel.mwi_comm_helpers import MWICommHelper
2126
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
2427

2528
_MATLAB_STARTUP_TIMEOUT = mwi_settings.get_process_startup_timeout()
2629
_logger = mwi_logger.get()
@@ -101,7 +104,7 @@ def _start_matlab_proxy_using_jupyter(url, headers, logger=_logger):
101104
logger.debug(f"Received status code: {resp.status_code}")
102105

103106
return (
104-
resp.status_code == requests.codes.OK
107+
resp.status_code == http.HTTPStatus.OK
105108
and matlab_proxy_index_page_identifier in resp.text
106109
)
107110

@@ -223,7 +226,7 @@ def start_matlab_proxy(logger=_logger):
223226
"""
224227
Error: MATLAB Kernel could not communicate with MATLAB.
225228
Reason: Possibly due to invalid jupyter security tokens.
226-
"""
229+
"""
227230
)
228231

229232

@@ -242,12 +245,8 @@ class MATLABKernel(ipykernel.kernelbase.Kernel):
242245
}
243246

244247
# MATLAB Kernel state
245-
murl = ""
246-
is_matlab_licensed: bool = False
247-
matlab_status = ""
248-
matlab_proxy_has_error: bool = False
248+
kernel_id = ""
249249
server_base_url = ""
250-
headers = dict()
251250
startup_error = None
252251
startup_checks_completed: bool = False
253252

@@ -257,21 +256,26 @@ def __init__(self, *args, **kwargs):
257256

258257
# Update log instance with kernel id. This helps in identifying logs from
259258
# multiple kernels which are running simultaneously
260-
self.log.debug(f"Initializing kernel with id: {self.ident}")
261-
self.log = self.log.getChild(f"{self.ident}")
259+
self.kernel_id = self.ident
260+
self.log.debug(f"Initializing kernel with id: {self.kernel_id}")
261+
self.log = self.log.getChild(f"{self.kernel_id}")
262+
262263
# Initialize the Magic Execution Engine.
263264
self.magic_engine = MagicExecutionEngine(self.log)
264265

265266
try:
266267
# Start matlab-proxy using the jupyter-matlab-proxy registered endpoint.
267-
self.murl, self.server_base_url, self.headers = start_matlab_proxy(self.log)
268-
(
269-
self.is_matlab_licensed,
270-
self.matlab_status,
271-
self.matlab_proxy_has_error,
272-
) = mwi_comm_helpers.fetch_matlab_proxy_status(
273-
self.murl, self.headers, self.log
268+
murl, self.server_base_url, headers = start_matlab_proxy(self.log)
269+
270+
# Using asyncio.get_event_loop for shell_loop as io_loop variable is
271+
# not yet initialized because start() is called after the __init__
272+
# is completed.
273+
shell_loop = asyncio.get_event_loop()
274+
control_loop = self.control_thread.io_loop.asyncio_loop
275+
self.mwi_comm_helper = MWICommHelper(
276+
self.kernel_id, murl, shell_loop, control_loop, headers, self.log
274277
)
278+
shell_loop.run_until_complete(self.mwi_comm_helper.connect())
275279
except MATLABConnectionError as err:
276280
self.startup_error = err
277281

@@ -286,9 +290,7 @@ async def interrupt_request(self, stream, ident, parent):
286290
self.log.debug("Received interrupt request from Jupyter")
287291
try:
288292
# Send interrupt request to MATLAB
289-
mwi_comm_helpers.send_interrupt_request_to_matlab(
290-
self.murl, self.headers, self.log
291-
)
293+
await self.mwi_comm_helper.send_interrupt_request_to_matlab()
292294

293295
# Set the response to interrupt request.
294296
content = {"status": "ok"}
@@ -329,7 +331,7 @@ def handle_magic_output(self, output, outputs=None):
329331
# Storing the magic outputs to display them after startup_check completes.
330332
outputs.append(output)
331333

332-
def do_execute(
334+
async def do_execute(
333335
self,
334336
code,
335337
silent,
@@ -360,7 +362,7 @@ def do_execute(
360362
# Blocking call, returns after MATLAB is started.
361363
if not skip_cell_execution:
362364
if not self.startup_checks_completed:
363-
self.perform_startup_checks()
365+
await self.perform_startup_checks()
364366
self.display_output(
365367
{
366368
"type": "stream",
@@ -383,8 +385,8 @@ def do_execute(
383385

384386
# Perform execution and categorization of outputs in MATLAB. Blocks
385387
# 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
388+
outputs = await self.mwi_comm_helper.send_execution_request_to_matlab(
389+
code
388390
)
389391

390392
if performed_startup_checks and not accumulated_magic_outputs:
@@ -414,9 +416,12 @@ def do_execute(
414416
self.log.error(
415417
f"Exception occurred while processing execution request:\n{e}"
416418
)
417-
if isinstance(e, HTTPError):
418-
# If exception is an HTTPError, it means MATLAB is unavailable.
419-
# Replace the HTTPError with MATLABConnectionError to give
419+
if isinstance(e, aiohttp.client_exceptions.ClientError):
420+
# Log the ClientError for debugging
421+
self.log.error(e)
422+
423+
# If exception is an ClientError, it means MATLAB is unavailable.
424+
# Replace the ClientError with MATLABConnectionError to give
420425
# meaningful error message to the user
421426
e = MATLABConnectionError()
422427

@@ -446,7 +451,7 @@ def do_execute(
446451
"user_expressions": {},
447452
}
448453

449-
def do_complete(self, code, cursor_pos):
454+
async def do_complete(self, code, cursor_pos):
450455
"""
451456
Used by ipykernel infrastructure for tab completion. For more info, look
452457
at https://jupyter-client.readthedocs.io/en/stable/messaging.html#completion
@@ -482,12 +487,17 @@ def do_complete(self, code, cursor_pos):
482487
completion_results = magic_completion_results
483488
else:
484489
try:
485-
completion_results = mwi_comm_helpers.send_completion_request_to_matlab(
486-
self.murl, self.headers, code, cursor_pos, self.log
490+
completion_results = (
491+
await self.mwi_comm_helper.send_completion_request_to_matlab(
492+
code, cursor_pos
493+
)
487494
)
488-
except (MATLABConnectionError, HTTPError) as e:
495+
except (
496+
MATLABConnectionError,
497+
aiohttp.client_exceptions.ClientResponseError,
498+
) as e:
489499
self.log.error(
490-
f"Exception occurred while sending shutdown request to MATLAB:\n{e}"
500+
f"Exception occurred while sending completion request to MATLAB:\n{e}"
491501
)
492502

493503
self.log.debug(
@@ -504,15 +514,15 @@ def do_complete(self, code, cursor_pos):
504514
},
505515
}
506516

507-
def do_is_complete(self, code):
517+
async def do_is_complete(self, code):
508518
# TODO: Seems like indentation rules. https://jupyter-client.readthedocs.io/en/stable/messaging.html#code-completeness
509519
return super().do_is_complete(code)
510520

511-
def do_inspect(self, code, cursor_pos, detail_level=0, omit_sections=...):
521+
async def do_inspect(self, code, cursor_pos, detail_level=0, omit_sections=...):
512522
# TODO: Implement Shift+Tab functionality. Can be used to provide any contextual information.
513523
return super().do_inspect(code, cursor_pos, detail_level, omit_sections)
514524

515-
def do_history(
525+
async def do_history(
516526
self,
517527
hist_access_type,
518528
output,
@@ -530,13 +540,15 @@ def do_history(
530540
hist_access_type, output, raw, session, start, stop, n, pattern, unique
531541
)
532542

533-
def do_shutdown(self, restart):
543+
async def do_shutdown(self, restart):
534544
self.log.debug("Received shutdown request from Jupyter")
535545
try:
536-
mwi_comm_helpers.send_shutdown_request_to_matlab(
537-
self.murl, self.headers, self.ident, self.log
538-
)
539-
except (MATLABConnectionError, HTTPError) as e:
546+
await self.mwi_comm_helper.send_shutdown_request_to_matlab()
547+
await self.mwi_comm_helper.disconnect()
548+
except (
549+
MATLABConnectionError,
550+
aiohttp.client_exceptions.ClientResponseError,
551+
) as e:
540552
self.log.error(
541553
f"Exception occurred while sending shutdown request to MATLAB:\n{e}"
542554
)
@@ -545,13 +557,13 @@ def do_shutdown(self, restart):
545557

546558
# Helper functions
547559

548-
def perform_startup_checks(self):
560+
async def perform_startup_checks(self):
549561
"""
550562
One time checks triggered during the first execution request. Displays
551563
login window if matlab is not licensed using matlab-proxy.
552564
553565
Raises:
554-
HTTPError, MATLABConnectionError: Occurs when matlab-proxy is not started or kernel cannot
566+
ClientError, MATLABConnectionError: Occurs when matlab-proxy is not started or kernel cannot
555567
communicate with MATLAB.
556568
"""
557569
self.log.debug("Performing startup checks")
@@ -561,10 +573,10 @@ def perform_startup_checks(self):
561573
raise self.startup_error
562574

563575
(
564-
self.is_matlab_licensed,
565-
self.matlab_status,
566-
self.matlab_proxy_has_error,
567-
) = mwi_comm_helpers.fetch_matlab_proxy_status(self.murl, self.headers)
576+
is_matlab_licensed,
577+
matlab_status,
578+
matlab_proxy_has_error,
579+
) = await self.mwi_comm_helper.fetch_matlab_proxy_status()
568580

569581
# Display iframe containing matlab-proxy to show login window if MATLAB
570582
# is not licensed using matlab-proxy. The iframe is removed after MATLAB
@@ -576,7 +588,7 @@ def perform_startup_checks(self):
576588
# as other browser based Jupyter clients.
577589
#
578590
# TODO: Find a workaround for users to be able to use our Jupyter kernel in VS Code.
579-
if not self.is_matlab_licensed:
591+
if not is_matlab_licensed:
580592
self.log.debug(
581593
"MATLAB is not licensed. Displaying HTML output to enable licensing."
582594
)
@@ -596,11 +608,11 @@ def perform_startup_checks(self):
596608
self.log.debug("Waiting until MATLAB is started")
597609
timeout = 0
598610
while (
599-
self.matlab_status != "up"
611+
matlab_status != "up"
600612
and timeout != _MATLAB_STARTUP_TIMEOUT
601-
and not self.matlab_proxy_has_error
613+
and not matlab_proxy_has_error
602614
):
603-
if self.is_matlab_licensed:
615+
if is_matlab_licensed:
604616
if timeout == 0:
605617
self.log.debug("Licensing completed. Clearing output area")
606618
self.display_output(
@@ -618,10 +630,10 @@ def perform_startup_checks(self):
618630
timeout += 1
619631
time.sleep(1)
620632
(
621-
self.is_matlab_licensed,
622-
self.matlab_status,
623-
self.matlab_proxy_has_error,
624-
) = mwi_comm_helpers.fetch_matlab_proxy_status(self.murl, self.headers)
633+
is_matlab_licensed,
634+
matlab_status,
635+
matlab_proxy_has_error,
636+
) = await self.mwi_comm_helper.fetch_matlab_proxy_status()
625637

626638
# If MATLAB is not available after 15 seconds of licensing information
627639
# being available either through user input or through matlab-proxy cache,
@@ -632,7 +644,7 @@ def perform_startup_checks(self):
632644
)
633645
raise MATLABConnectionError
634646

635-
if self.matlab_proxy_has_error:
647+
if matlab_proxy_has_error:
636648
self.log.error("matlab-proxy encountered error.")
637649
raise MATLABConnectionError
638650

0 commit comments

Comments
 (0)