diff --git a/mu/modes/python3.py b/mu/modes/python3.py
index 87612b6fa..9ccaa8f46 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,21 @@
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):
+ os.makedirs(kernel_dir, exist_ok=True)
+ 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 +79,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 +99,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..43bf726b8 100644
--- a/tests/modes/test_python3.py
+++ b/tests/modes/test_python3.py
@@ -4,13 +4,43 @@
"""
import sys
import os
-from mu.modes.python3 import PythonMode, KernelRunner
+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
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_with(mock_dir, 511)
+ 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 +64,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 +82,27 @@ 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:
+ 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():
"""
Ensure the stop_kernel method eventually emits the kernel_finished