From 1630b65a1b5b38e8fb7f3b4387208415aaccad94 Mon Sep 17 00:00:00 2001 From: devdanzin <74280297+devdanzin@users.noreply.github.com> Date: Wed, 15 Dec 2021 20:17:24 -0300 Subject: [PATCH 1/2] Create and use kernel spec for the REPL. --- mu/modes/python3.py | 61 ++++++++++++++++++++++--------------- tests/modes/test_python3.py | 59 +++++++++++++++++++++++++++++++++-- 2 files changed, 92 insertions(+), 28 deletions(-) diff --git a/mu/modes/python3.py b/mu/modes/python3.py index 87612b6fa..07d6e583e 100644 --- a/mu/modes/python3.py +++ b/mu/modes/python3.py @@ -19,11 +19,14 @@ import sys import os import logging +import json +from mu.config import DATA_DIR from mu.modes.base import BaseMode from mu.modes.api import PYTHON3_APIS, SHARED_APIS, PI_APIS from mu.resources import load_icon from mu.interface.panes import CHARTS from ..virtual_environment import venv +from jupyter_client import kernelspec from qtconsole.manager import QtKernelManager from qtconsole.client import QtKernelClient from PyQt5.QtCore import QObject, QThread, pyqtSignal @@ -32,30 +35,25 @@ logger = logging.getLogger(__name__) -class MuKernelManager(QtKernelManager): - def start_kernel(self, **kw): - """Starts a kernel on this host in a separate process. - - Subclassed to allow checking that the kernel uses the same Python as - Mu itself. - """ - kernel_cmd, kw = self.pre_start_kernel(**kw) - cmd_interpreter = kernel_cmd[0] - if cmd_interpreter != venv.interpreter: - self.log.debug( - "Wrong interpreter selected to run REPL: %s", kernel_cmd - ) - self.log.debug( - "Using default interpreter to run REPL instead: %s", - cmd_interpreter, - ) - cmd_interpreter = venv.interpreter - kernel_cmd[0] = cmd_interpreter - - # launch the kernel subprocess - self.log.debug("Starting kernel: %s", kernel_cmd) - self.kernel = self._launch_kernel(kernel_cmd, **kw) - self.post_start_kernel(**kw) +def make_kernel_spec(kernel_dir): + try: + if not os.path.exists(kernel_dir): + os.mkdir(kernel_dir) + except FileExistsError: + pass + spec = { + "argv": [ + venv.interpreter, + "-m", + "ipykernel_launcher", + "-f", + "{connection_file}", + ], + "display_name": "Mu's Python 3 Kernel", + "language": "python", + } + with open(os.path.join(kernel_dir, "kernel.json"), "w") as kernel_json: + json.dump(spec, kernel_json) class KernelRunner(QObject): @@ -85,6 +83,7 @@ def __init__(self, kernel_name, cwd, envars): self.kernel_name = kernel_name self.cwd = cwd self.envars = dict(envars) + self.kernel_dir = os.path.join(DATA_DIR, "kernel") def start_kernel(self): """ @@ -104,9 +103,21 @@ def start_kernel(self): if k != "PYTHONPATH": os.environ[k] = v - self.repl_kernel_manager = MuKernelManager() + self.repl_kernel_manager = QtKernelManager() + if self.kernel_name not in kernelspec.find_kernel_specs(): + make_kernel_spec(self.kernel_dir) + kernelspec.install_kernel_spec( + self.kernel_dir, self.kernel_name, user=True + ) self.repl_kernel_manager.kernel_name = self.kernel_name self.repl_kernel_manager.start_kernel() + + if self.repl_kernel_manager.kernel_spec.argv[0] != venv.interpreter: + logger.debug( + "Wrong interpreter selected to run REPL: %s", + self.repl_kernel_manager.kernel_spec.argv[0], + ) + self.repl_kernel_client = self.repl_kernel_manager.client() self.kernel_started.emit( self.repl_kernel_manager, self.repl_kernel_client diff --git a/tests/modes/test_python3.py b/tests/modes/test_python3.py index abfe55f18..4cf2a2c29 100644 --- a/tests/modes/test_python3.py +++ b/tests/modes/test_python3.py @@ -4,13 +4,42 @@ """ import sys import os -from mu.modes.python3 import PythonMode, KernelRunner +from mu.modes.python3 import PythonMode, KernelRunner, make_kernel_spec from mu.modes.api import PYTHON3_APIS, SHARED_APIS, PI_APIS from mu.virtual_environment import venv from unittest import mock +def test_make_kernel_spec(): + """ + Test the generation of a kernel spec's kernel.json works as intended. + """ + mock_python = "/home/user/bin/python" + mock_dir = os.path.join("/home/user/config", "kernel") + with mock.patch("builtins.open", mock.mock_open()) as opener: + with mock.patch("mu.modes.python3.venv.interpreter", mock_python): + with mock.patch("mu.modes.python3.DATA_DIR", mock_dir): + with mock.patch("os.mkdir") as mkdir: + with mock.patch("json.dump") as dump: + make_kernel_spec(mock_dir) + mkdir.assert_called_once_with(mock_dir) + json_path = os.path.join(mock_dir, "kernel.json") + opener.assert_called_once_with(json_path, "w") + expected = { + "argv": [ + mock_python, + "-m", + "ipykernel_launcher", + "-f", + "{connection_file}", + ], + "display_name": "Mu's Python 3 Kernel", + "language": "python", + } + dump.assert_called_once_with(expected, opener()) + + def test_kernel_runner_start_kernel(): """ Ensure the start_kernel method eventually emits the kernel_started signal @@ -34,8 +63,12 @@ def test_kernel_runner_start_kernel(): mock_kernel_manager_class = mock.MagicMock() mock_kernel_manager_class.return_value = mock_kernel_manager with mock.patch("mu.modes.python3.os", mock_os), mock.patch( - "mu.modes.python3.MuKernelManager", mock_kernel_manager_class - ), mock.patch("sys.platform", "darwin"): + "mu.modes.python3.QtKernelManager", mock_kernel_manager_class + ), mock.patch("sys.platform", "darwin"), mock.patch( + "mu.modes.python3.make_kernel_spec" + ), mock.patch( + "jupyter_client.kernelspec.install_kernel_spec" + ): kr.start_kernel() mock_os.chdir.assert_called_once_with("/a/path/to/mu_code") assert mock_os.environ["name"] == "value" @@ -48,6 +81,26 @@ def test_kernel_runner_start_kernel(): ) +def test_kernel_runner_kernel_spec_creation(): + """ + Test the creation and use of a sample kernel. + """ + kernel_name = "test_kernel_name" + kr = KernelRunner( + kernel_name=kernel_name, cwd="/a/path/to/mu_code", envars=[] + ) + try: + with mock.patch("mu.modes.python3.os.chdir"): + kr.start_kernel() + assert kr.kernel_started + assert kr.repl_kernel_manager.kernel_name == "test_kernel_name" + assert kr.repl_kernel_manager.kernel_spec.argv[0] == venv.interpreter + finally: + kr.repl_kernel_manager.kernel_spec_manager.remove_kernel_spec( + kernel_name + ) + + def test_kernel_runner_stop_kernel(): """ Ensure the stop_kernel method eventually emits the kernel_finished From 0bf1da579caa4814a720148199ec5bcf71cafc59 Mon Sep 17 00:00:00 2001 From: devdanzin <74280297+devdanzin@users.noreply.github.com> Date: Wed, 15 Dec 2021 21:28:00 -0300 Subject: [PATCH 2/2] Fix make_kernel_spec to create whole path, avoid failing test on spec removal. --- mu/modes/python3.py | 6 +----- tests/modes/test_python3.py | 10 ++++++---- 2 files changed, 7 insertions(+), 9 deletions(-) diff --git a/mu/modes/python3.py b/mu/modes/python3.py index 07d6e583e..9ccaa8f46 100644 --- a/mu/modes/python3.py +++ b/mu/modes/python3.py @@ -36,11 +36,7 @@ def make_kernel_spec(kernel_dir): - try: - if not os.path.exists(kernel_dir): - os.mkdir(kernel_dir) - except FileExistsError: - pass + os.makedirs(kernel_dir, exist_ok=True) spec = { "argv": [ venv.interpreter, diff --git a/tests/modes/test_python3.py b/tests/modes/test_python3.py index 4cf2a2c29..43bf726b8 100644 --- a/tests/modes/test_python3.py +++ b/tests/modes/test_python3.py @@ -4,6 +4,7 @@ """ import sys import os +from jupyter_client import kernelspec from mu.modes.python3 import PythonMode, KernelRunner, make_kernel_spec from mu.modes.api import PYTHON3_APIS, SHARED_APIS, PI_APIS from mu.virtual_environment import venv @@ -23,7 +24,7 @@ def test_make_kernel_spec(): with mock.patch("os.mkdir") as mkdir: with mock.patch("json.dump") as dump: make_kernel_spec(mock_dir) - mkdir.assert_called_once_with(mock_dir) + mkdir.assert_called_with(mock_dir, 511) json_path = os.path.join(mock_dir, "kernel.json") opener.assert_called_once_with(json_path, "w") expected = { @@ -96,9 +97,10 @@ def test_kernel_runner_kernel_spec_creation(): assert kr.repl_kernel_manager.kernel_name == "test_kernel_name" assert kr.repl_kernel_manager.kernel_spec.argv[0] == venv.interpreter finally: - kr.repl_kernel_manager.kernel_spec_manager.remove_kernel_spec( - kernel_name - ) + if kernel_name in kernelspec.find_kernel_specs(): + kr.repl_kernel_manager.kernel_spec_manager.remove_kernel_spec( + kernel_name + ) def test_kernel_runner_stop_kernel():