diff --git a/spyder/api/shellconnect/mixins.py b/spyder/api/shellconnect/mixins.py
index d7e221663ed..0dba7925677 100644
--- a/spyder/api/shellconnect/mixins.py
+++ b/spyder/api/shellconnect/mixins.py
@@ -32,8 +32,6 @@ def on_ipython_console_available(self):
ipyconsole.sig_shellwidget_changed.connect(self.set_shellwidget)
ipyconsole.sig_shellwidget_created.connect(self.add_shellwidget)
ipyconsole.sig_shellwidget_deleted.connect(self.remove_shellwidget)
- ipyconsole.sig_external_spyder_kernel_connected.connect(
- self.on_connection_to_external_spyder_kernel)
@on_plugin_teardown(plugin=Plugins.IPythonConsole)
def on_ipython_console_teardown(self):
@@ -43,8 +41,6 @@ def on_ipython_console_teardown(self):
ipyconsole.sig_shellwidget_changed.disconnect(self.set_shellwidget)
ipyconsole.sig_shellwidget_created.disconnect(self.add_shellwidget)
ipyconsole.sig_shellwidget_deleted.disconnect(self.remove_shellwidget)
- ipyconsole.sig_external_spyder_kernel_connected.disconnect(
- self.on_connection_to_external_spyder_kernel)
# ---- Public API
# -------------------------------------------------------------------------
@@ -110,16 +106,3 @@ def get_widget_for_shellwidget(self, shellwidget):
The widget corresponding to the shellwidget, or None if not found.
"""
return self.get_widget().get_widget_for_shellwidget(shellwidget)
-
- def on_connection_to_external_spyder_kernel(self, shellwidget):
- """
- Actions to take when the IPython console connects to an
- external Spyder kernel.
-
- Parameters
- ----------
- shellwidget: spyder.plugins.ipyconsole.widgets.shell.ShellWidget
- The shell widget that was connected to the external Spyder
- kernel.
- """
- pass
diff --git a/spyder/app/tests/test_mainwindow.py b/spyder/app/tests/test_mainwindow.py
index 3442d286f1d..6c74bc91b5a 100644
--- a/spyder/app/tests/test_mainwindow.py
+++ b/spyder/app/tests/test_mainwindow.py
@@ -58,6 +58,7 @@
DebuggerWidgetActions, DebuggerToolbarActions)
from spyder.plugins.help.widgets import ObjectComboBox
from spyder.plugins.help.tests.test_plugin import check_text
+from spyder.plugins.ipythonconsole.utils.kernel_handler import KernelHandler
from spyder.plugins.layout.layouts import DefaultLayouts
from spyder.plugins.toolbar.api import ApplicationToolbars
from spyder.py3compat import PY2, qbytearray_to_str, to_text_string
@@ -120,6 +121,12 @@ def test_leaks(main_window, qtbot):
Many other ways of leaking exist but are not covered here.
"""
+ def wait_all_shutdown():
+ objects = gc.get_objects()
+ for o in objects:
+ if isinstance(o, KernelHandler):
+ o.wait_shutdown_thread()
+
def ns_fun(main_window, qtbot):
# Wait until the window is fully up
shell = main_window.ipyconsole.get_current_shellwidget()
@@ -129,7 +136,7 @@ def ns_fun(main_window, qtbot):
# Count initial objects
# Only one of each should be present, but because of many leaks,
# this is most likely not the case. Here only closing is tested
- shell.wait_all_shutdown()
+ wait_all_shutdown()
gc.collect()
objects = gc.get_objects()
n_code_editor_init = 0
@@ -160,8 +167,7 @@ def ns_fun(main_window, qtbot):
main_window.ipyconsole.restart()
# Wait until the shells are closed
- shell = main_window.ipyconsole.get_current_shellwidget()
- shell.wait_all_shutdown()
+ wait_all_shutdown()
return n_shell_init, n_code_editor_init
n_shell_init, n_code_editor_init = ns_fun(main_window, qtbot)
diff --git a/spyder/plugins/ipythonconsole/comms/kernelcomm.py b/spyder/plugins/ipythonconsole/comms/kernelcomm.py
index 82f0f16e8c1..bcbbaf8dc63 100644
--- a/spyder/plugins/ipythonconsole/comms/kernelcomm.py
+++ b/spyder/plugins/ipythonconsole/comms/kernelcomm.py
@@ -37,6 +37,16 @@ def __init__(self):
# Register handlers
self.register_call_handler('_async_error', self._async_error)
+ def is_open(self, comm_id=None):
+ """Check to see if the comm is open."""
+ valid_comms = [
+ comm for comm in self._comms
+ if self._comms[comm]['status'] in ['opening', 'ready']
+ ]
+ if comm_id is None:
+ return len(valid_comms) > 0
+ return comm_id in valid_comms
+
@contextmanager
def comm_channel_manager(self, comm_id, queue_message=False):
"""Use control_channel instead of shell_channel."""
@@ -63,7 +73,7 @@ def _set_call_return_value(self, call_dict, data, is_error=False):
super(KernelComm, self)._set_call_return_value(
call_dict, data, is_error)
- def remove(self, comm_id=None):
+ def remove(self, comm_id=None, only_closing=False):
"""
Remove the comm without notifying the other side.
@@ -71,6 +81,8 @@ def remove(self, comm_id=None):
"""
id_list = self.get_comm_id_list(comm_id)
for comm_id in id_list:
+ if only_closing and self._comms[comm_id]['status'] != 'closing':
+ continue
del self._comms[comm_id]
def close(self, comm_id=None):
diff --git a/spyder/plugins/ipythonconsole/plugin.py b/spyder/plugins/ipythonconsole/plugin.py
index 5a9933b8640..4ce0e52d88f 100644
--- a/spyder/plugins/ipythonconsole/plugin.py
+++ b/spyder/plugins/ipythonconsole/plugin.py
@@ -138,16 +138,6 @@ class IPythonConsole(SpyderDockablePlugin):
The shellwigdet.
"""
- sig_external_spyder_kernel_connected = Signal(object)
- """
- This signal is emitted when we connect to an external Spyder kernel.
-
- Parameters
- ----------
- shellwidget: spyder.plugins.ipyconsole.widgets.shell.ShellWidget
- The shellwigdet that was connected to the kernel.
- """
-
sig_render_plain_text_requested = Signal(str)
"""
This signal is emitted to request a plain text help render.
@@ -216,8 +206,6 @@ def on_initialize(self):
widget.sig_shellwidget_created.connect(self.sig_shellwidget_created)
widget.sig_shellwidget_deleted.connect(self.sig_shellwidget_deleted)
widget.sig_shellwidget_changed.connect(self.sig_shellwidget_changed)
- widget.sig_external_spyder_kernel_connected.connect(
- self.sig_external_spyder_kernel_connected)
widget.sig_render_plain_text_requested.connect(
self.sig_render_plain_text_requested)
widget.sig_render_rich_text_requested.connect(
diff --git a/spyder/plugins/ipythonconsole/tests/test_ipythonconsole.py b/spyder/plugins/ipythonconsole/tests/test_ipythonconsole.py
index c83e068e82f..b616853ca21 100644
--- a/spyder/plugins/ipythonconsole/tests/test_ipythonconsole.py
+++ b/spyder/plugins/ipythonconsole/tests/test_ipythonconsole.py
@@ -50,6 +50,7 @@
from spyder.plugins.help.tests.test_plugin import check_text
from spyder.plugins.help.utils.sphinxify import CSS_PATH
from spyder.plugins.ipythonconsole.plugin import IPythonConsole
+from spyder.plugins.ipythonconsole.utils import stdfile
from spyder.plugins.ipythonconsole.utils.style import create_style_class
from spyder.plugins.ipythonconsole.widgets import ClientWidget
from spyder.utils.programs import get_temp_dir
@@ -159,9 +160,9 @@ def __getattr__(self, attr):
# Instruct the console to not use a stderr file
no_stderr_file = request.node.get_closest_marker('no_stderr_file')
if no_stderr_file:
- test_no_stderr = 'True'
+ test_no_stderr = True
else:
- test_no_stderr = ''
+ test_no_stderr = False
# Use the automatic backend if requested
auto_backend = request.node.get_closest_marker('auto_backend')
@@ -211,8 +212,8 @@ def __getattr__(self, attr):
# Create the console and a new client and set environment
os.environ['IPYCONSOLE_TESTING'] = 'True'
- os.environ['IPYCONSOLE_TEST_DIR'] = test_dir
- os.environ['IPYCONSOLE_TEST_NO_STDERR'] = test_no_stderr
+ stdfile.IPYCONSOLE_TEST_DIR = test_dir
+ stdfile.IPYCONSOLE_TEST_NO_STDERR = test_no_stderr
window = MainWindowMock()
console = IPythonConsole(parent=window, configuration=configuration)
@@ -288,8 +289,8 @@ def get_plugin(name):
# Close
console.on_close()
os.environ.pop('IPYCONSOLE_TESTING')
- os.environ.pop('IPYCONSOLE_TEST_DIR')
- os.environ.pop('IPYCONSOLE_TEST_NO_STDERR')
+ stdfile.IPYCONSOLE_TEST_DIR = None
+ stdfile.IPYCONSOLE_TEST_NO_STDERR = False
if os.name == 'nt' or known_leak:
# Do not test for leaks
@@ -1306,7 +1307,8 @@ def test_set_elapsed_time(ipyconsole, qtbot):
# Set time to 2 minutes ago.
client.t0 -= 120
with qtbot.waitSignal(client.timer.timeout, timeout=5000):
- ipyconsole.get_widget().set_client_elapsed_time(client)
+ client.timer.timeout.connect(client.show_time)
+ client.timer.start(1000)
assert ('00:02:00' in main_widget.time_label.text() or
'00:02:01' in main_widget.time_label.text())
@@ -1401,10 +1403,9 @@ def test_kernel_crash(ipyconsole, qtbot):
ipyconsole.create_new_client()
# Assert that the console is showing an error
- qtbot.waitUntil(lambda: ipyconsole.get_clients()[-1].is_error_shown,
- timeout=6000)
error_client = ipyconsole.get_clients()[-1]
- assert error_client.is_error_shown
+ qtbot.waitUntil(lambda: bool(error_client.error_text), timeout=6000)
+ assert error_client.error_text
# Assert the error contains the text we expect
webview = error_client.infowidget
@@ -1636,10 +1637,17 @@ def test_pdb_ignore_lib(ipyconsole, qtbot, show_lib):
control.setFocus()
# Tests assume inline backend
+ qtbot.wait(1000)
ipyconsole.set_conf('pdb_ignore_lib', not show_lib, section="debugger")
+ qtbot.wait(1000)
with qtbot.waitSignal(shell.executed):
shell.execute('%debug print()')
+ with qtbot.waitSignal(shell.executed):
+ shell.execute(
+ '"value = " + str(get_ipython().pdb_session.pdb_ignore_lib)')
+ assert "value = " + str(not show_lib) in control.toPlainText()
+
qtbot.keyClicks(control, '!s')
with qtbot.waitSignal(shell.executed):
qtbot.keyClick(control, Qt.Key_Enter)
@@ -2412,14 +2420,12 @@ def test_old_kernel_version(ipyconsole, qtbot):
"""
# Set a false _spyder_kernels_version in the cached kernel
w = ipyconsole.get_widget()
- # create new client so PYTEST_CURRENT_TEST is the same
- w.create_new_client()
# Wait until the window is fully up
shell = ipyconsole.get_current_shellwidget()
qtbot.waitUntil(
lambda: shell._prompt_html is not None, timeout=SHELL_TIMEOUT)
- kc = w._cached_kernel_properties[-1][2]
+ kc = w._cached_kernel_properties[-1].kernel_client
kc.start_channels()
kc.execute("get_ipython()._spyder_kernels_version = ('1.0.0', '')")
# Cleanup the kernel_client so it can be used again
@@ -2435,8 +2441,10 @@ def test_old_kernel_version(ipyconsole, qtbot):
client = w.get_current_client()
# Make sure an error is shown
- qtbot.waitUntil(lambda: client.error_text is not None)
- assert '1.0.0' in client.error_text
+ control = client.get_control()
+ qtbot.waitUntil(
+ lambda: "1.0.0" in control.toPlainText(), timeout=SHELL_TIMEOUT)
+ assert "conda install spyder" in control.toPlainText()
def test_run_script(ipyconsole, qtbot, tmp_path):
diff --git a/spyder/plugins/ipythonconsole/utils/kernel_handler.py b/spyder/plugins/ipythonconsole/utils/kernel_handler.py
new file mode 100644
index 00000000000..ee2e912e29c
--- /dev/null
+++ b/spyder/plugins/ipythonconsole/utils/kernel_handler.py
@@ -0,0 +1,404 @@
+# -*- coding: utf-8 -*-
+#
+# Copyright © Spyder Project Contributors
+# Licensed under the terms of the MIT License
+# (see spyder/__init__.py for details)
+
+"""Kernel handler."""
+
+# Standard library imports
+import os
+import os.path as osp
+import re
+from threading import Lock
+import uuid
+
+# Third-party imports
+from jupyter_core.paths import jupyter_runtime_dir
+from qtpy.QtCore import QThread
+from zmq.ssh import tunnel as zmqtunnel
+
+# Local imports
+from spyder.api.translations import get_translation
+from spyder.plugins.ipythonconsole.utils.manager import SpyderKernelManager
+from spyder.plugins.ipythonconsole.utils.client import SpyderKernelClient
+from spyder.plugins.ipythonconsole.utils.ssh import openssh_tunnel
+from spyder.plugins.ipythonconsole.utils.stdfile import StdFile
+
+
+# Localization
+_ = get_translation("spyder")
+
+PERMISSION_ERROR_MSG = _(
+ "The directory {} is not writable and it is required to create IPython "
+ "consoles. Please make it writable."
+)
+
+if os.name == "nt":
+ ssh_tunnel = zmqtunnel.paramiko_tunnel
+else:
+ ssh_tunnel = openssh_tunnel
+
+
+class KernelHandler:
+ """
+ A class to handle the kernel in several ways and store kernel connection
+ information.
+ """
+
+ def __init__(
+ self,
+ connection_file,
+ kernel_manager=None,
+ kernel_client=None,
+ stderr_obj=None,
+ stdout_obj=None,
+ fault_obj=None,
+ known_spyder_kernel=False,
+ hostname=None,
+ sshkey=None,
+ password=None,
+ ):
+ # Connection Informations
+ self.connection_file = connection_file
+ self.kernel_manager = kernel_manager
+ self.kernel_client = kernel_client
+ self.stderr_obj = stderr_obj
+ self.stdout_obj = stdout_obj
+ self.fault_obj = fault_obj
+ self.known_spyder_kernel = known_spyder_kernel
+ self.hostname = hostname
+ self.sshkey = sshkey
+ self.password = password
+
+ # Comm
+ self.kernel_comm = None
+
+ # Internal
+ self.shutdown_thread = None
+ self._shutdown_lock = Lock()
+
+ @staticmethod
+ def new_connection_file():
+ """
+ Generate a new connection file
+
+ Taken from jupyter_client/console_app.py
+ Licensed under the BSD license
+ """
+ # Check if jupyter_runtime_dir exists (Spyder addition)
+ if not osp.isdir(jupyter_runtime_dir()):
+ try:
+ os.makedirs(jupyter_runtime_dir())
+ except (IOError, OSError):
+ return None
+ cf = ""
+ while not cf:
+ ident = str(uuid.uuid4()).split("-")[-1]
+ cf = os.path.join(jupyter_runtime_dir(), "kernel-%s.json" % ident)
+ cf = cf if not os.path.exists(cf) else ""
+ return cf
+
+ @staticmethod
+ def tunnel_to_kernel(
+ connection_info, hostname, sshkey=None, password=None, timeout=10
+ ):
+ """
+ Tunnel connections to a kernel via ssh.
+
+ Remote ports are specified in the connection info ci.
+ """
+ lports = zmqtunnel.select_random_ports(5)
+ rports = (
+ connection_info["shell_port"],
+ connection_info["iopub_port"],
+ connection_info["stdin_port"],
+ connection_info["hb_port"],
+ connection_info["control_port"],
+ )
+ remote_ip = connection_info["ip"]
+ for lp, rp in zip(lports, rports):
+ ssh_tunnel(lp, rp, hostname, remote_ip, sshkey, password, timeout)
+ return tuple(lports)
+
+ @classmethod
+ def new_from_spec(cls, kernel_spec):
+ """
+ Create a new kernel.
+
+ Might raise all kinds of exceptions
+ """
+ connection_file = cls.new_connection_file()
+ if connection_file is None:
+ raise RuntimeError(
+ PERMISSION_ERROR_MSG.format(jupyter_runtime_dir())
+ )
+
+ stderr_obj = StdFile(connection_file, ".stderr")
+ stdout_obj = StdFile(connection_file, ".stdout")
+ fault_obj = StdFile(connection_file, ".fault")
+
+ # Kernel manager
+ kernel_manager = SpyderKernelManager(
+ connection_file=connection_file,
+ config=None,
+ autorestart=True,
+ )
+
+ kernel_manager._kernel_spec = kernel_spec
+
+ kernel_manager.start_kernel(
+ stderr=stderr_obj.handle,
+ stdout=stdout_obj.handle,
+ env=kernel_spec.env,
+ )
+
+ # Kernel client
+ kernel_client = kernel_manager.client()
+
+ # Increase time (in seconds) to detect if a kernel is alive.
+ # See spyder-ide/spyder#3444.
+ kernel_client.hb_channel.time_to_dead = 25.0
+
+ return cls(
+ connection_file=connection_file,
+ kernel_manager=kernel_manager,
+ kernel_client=kernel_client,
+ stderr_obj=stderr_obj,
+ stdout_obj=stdout_obj,
+ fault_obj=fault_obj,
+ known_spyder_kernel=True,
+ )
+
+ @classmethod
+ def from_connection_file(
+ cls, connection_file, hostname=None, sshkey=None, password=None
+ ):
+ """Create kernel for given connection file."""
+ new_kernel = cls(
+ connection_file,
+ hostname=hostname,
+ sshkey=sshkey,
+ password=password,
+ )
+ # Get new kernel_client
+ new_kernel.init_kernel_client()
+ return new_kernel
+
+ def init_kernel_client(self):
+ """Create kernel client."""
+ kernel_client = SpyderKernelClient(
+ connection_file=self.connection_file
+ )
+
+ # This is needed for issue spyder-ide/spyder#9304.
+ try:
+ kernel_client.load_connection_file()
+ except Exception as e:
+ raise RuntimeError(
+ _(
+ "An error occurred while trying to load "
+ "the kernel connection file. The error "
+ "was:\n\n"
+ )
+ + str(e)
+ )
+
+ if self.hostname is not None:
+ try:
+ connection_info = dict(
+ ip=kernel_client.ip,
+ shell_port=kernel_client.shell_port,
+ iopub_port=kernel_client.iopub_port,
+ stdin_port=kernel_client.stdin_port,
+ hb_port=kernel_client.hb_port,
+ control_port=kernel_client.control_port,
+ )
+
+ (
+ kernel_client.shell_port,
+ kernel_client.iopub_port,
+ kernel_client.stdin_port,
+ kernel_client.hb_port,
+ kernel_client.control_port,
+ ) = self.tunnel_to_kernel(
+ connection_info, self.hostname, self.sshkey, self.password
+ )
+ except Exception as e:
+ raise RuntimeError(
+ _("Could not open ssh tunnel. The error was:\n\n")
+ + str(e)
+ )
+ self.kernel_client = kernel_client
+
+ def close(self, shutdown_kernel=True, now=False):
+ """Close kernel"""
+ if self.kernel_comm is not None:
+ self.kernel_comm.close()
+
+ if shutdown_kernel and self.kernel_manager is not None:
+ km = self.kernel_manager
+ km.stop_restarter()
+
+ if now:
+ km.shutdown_kernel(now=True)
+ self.after_shutdown()
+ else:
+ shutdown_thread = QThread(None)
+ shutdown_thread.run = self._thread_shutdown_kernel
+ shutdown_thread.start()
+ shutdown_thread.finished.connect(self.after_shutdown)
+ self.shutdown_thread = shutdown_thread
+
+ if (
+ self.kernel_client is not None
+ and self.kernel_client.channels_running
+ ):
+ self.kernel_client.stop_channels()
+
+ def after_shutdown(self):
+ """Cleanup after shutdown"""
+ if self.kernel_comm is not None:
+ self.kernel_comm.remove(only_closing=True)
+ self.shutdown_thread = None
+
+ def _thread_shutdown_kernel(self):
+ """Shutdown kernel."""
+ with self._shutdown_lock:
+ # Avoid calling shutdown_kernel on the same manager twice
+ # from different threads to avoid crash.
+ if self.kernel_manager.shutting_down:
+ return
+ self.kernel_manager.shutting_down = True
+ try:
+ self.kernel_manager.shutdown_kernel()
+ except Exception:
+ # kernel was externally killed
+ pass
+
+ def wait_shutdown_thread(self):
+ """Wait shutdown thread."""
+ thread = self.shutdown_thread
+ if thread is None:
+ return
+ if thread.isRunning():
+ try:
+ thread.kernel_manager._kill_kernel()
+ except Exception:
+ pass
+ thread.quit()
+ thread.wait()
+
+ def copy(self):
+ """Copy kernel."""
+ # Copy kernel infos
+ new_kernel = self.__class__(
+ connection_file=self.connection_file,
+ kernel_manager=self.kernel_manager,
+ known_spyder_kernel=self.known_spyder_kernel,
+ hostname=self.hostname,
+ sshkey=self.sshkey,
+ password=self.password,
+ )
+
+ # Copy std file
+ if self.stderr_obj is not None:
+ new_kernel.stderr_obj = self.stderr_obj.copy()
+ if self.stdout_obj is not None:
+ new_kernel.stdout_obj = self.stdout_obj.copy()
+ if self.fault_obj is not None:
+ new_kernel.fault_obj = self.fault_obj.copy()
+
+ # Get new kernel_client
+ new_kernel.init_kernel_client()
+ return new_kernel
+
+ def remove_files(self):
+ """Remove std files."""
+ for obj in [self.stderr_obj, self.stderr_obj, self.fault_obj]:
+ if obj is not None:
+ obj.remove()
+
+ def open_comm(self, kernel_comm):
+ """Open kernel comm"""
+ kernel_comm.open_comm(self.kernel_client)
+ self.kernel_comm = kernel_comm
+
+ def replace_std_files(self):
+ """Replace std files."""
+ for obj in [self.stderr_obj, self.stderr_obj, self.fault_obj]:
+ if obj is None:
+ continue
+ obj.remove()
+ fn = obj.filename
+ m = re.match(r"(.+_)(\d+)(.[a-z]+)", fn)
+ if m:
+ # Already a replaced file
+ path, n, ext = m.groups()
+ obj.filename = path + str(1 + int(n)) + ext
+ continue
+ m = re.match(r"(.+)(.[a-z]+)", fn)
+ if m:
+ # First replaced file
+ path, ext = m.groups()
+ obj.filename = path + "_1" + ext
+ continue
+ # No extension, should not happen
+ obj.filename += "_1"
+
+ def get_fault_filename(self):
+ """Get faulthandler filename"""
+ if self.fault_obj is None:
+ return
+ return self.fault_obj.filename
+
+ def get_fault_text(self):
+ """Get a fault from a previous session."""
+
+ if self.fault_obj is None:
+ return
+ fault = self.fault_obj.get_contents()
+ if not fault:
+ return
+
+ thread_regex = (
+ r"(Current thread|Thread) "
+ r"(0x[\da-f]+) \(most recent call first\):"
+ r"(?:.|\r\n|\r|\n)+?(?=Current thread|Thread|\Z)")
+ # Keep line for future improvments
+ # files_regex = r"File \"([^\"]+)\", line (\d+) in (\S+)"
+
+ main_re = "Main thread id:(?:\r\n|\r|\n)(0x[0-9a-f]+)"
+ main_id = 0
+ for match in re.finditer(main_re, fault):
+ main_id = int(match.group(1), base=16)
+
+ system_re = ("System threads ids:"
+ "(?:\r\n|\r|\n)(0x[0-9a-f]+(?: 0x[0-9a-f]+)+)")
+ ignore_ids = []
+ start_idx = 0
+ for match in re.finditer(system_re, fault):
+ ignore_ids = [int(i, base=16) for i in match.group(1).split()]
+ start_idx = match.span()[1]
+ text = ""
+ for idx, match in enumerate(re.finditer(thread_regex, fault)):
+ if idx == 0:
+ text += fault[start_idx:match.span()[0]]
+ thread_id = int(match.group(2), base=16)
+ if thread_id != main_id:
+ if thread_id in ignore_ids:
+ continue
+ if "wurlitzer.py" in match.group(0):
+ # Wurlitzer threads are launched later
+ continue
+ text += "\n" + match.group(0) + "\n"
+ else:
+ try:
+ pattern = (r".*(?:/IPython/core/interactiveshell\.py|"
+ r"\\IPython\\core\\interactiveshell\.py).*")
+ match_internal = next(re.finditer(pattern, match.group(0)))
+ end_idx = match_internal.span()[0]
+ except StopIteration:
+ end_idx = None
+ text += "\nMain thread:\n" + match.group(0)[:end_idx] + "\n"
+ return text
diff --git a/spyder/plugins/ipythonconsole/utils/ssh.py b/spyder/plugins/ipythonconsole/utils/ssh.py
index dde51e4d8bb..87aef9b1285 100644
--- a/spyder/plugins/ipythonconsole/utils/ssh.py
+++ b/spyder/plugins/ipythonconsole/utils/ssh.py
@@ -9,9 +9,8 @@
import atexit
import os
-from qtpy.QtWidgets import QMessageBox
-if not os.name == 'nt':
- import pexpect
+from qtpy.QtWidgets import QApplication, QMessageBox
+import pexpect
from spyder.config.base import _
@@ -20,7 +19,7 @@ def _stop_tunnel(cmd):
pexpect.run(cmd)
-def openssh_tunnel(self, lport, rport, server, remoteip='127.0.0.1',
+def openssh_tunnel(lport, rport, server, remoteip='127.0.0.1',
keyfile=None, password=None, timeout=0.4):
"""
We decided to replace pyzmq's openssh_tunnel method to work around
@@ -68,7 +67,8 @@ def openssh_tunnel(self, lport, rport, server, remoteip='127.0.0.1',
question = _("The authenticity of host %s can't be "
"established. Are you sure you want to continue "
"connecting?") % host
- reply = QMessageBox.question(self, _('Warning'), question,
+ reply = QMessageBox.question(QApplication.activeWindow(),
+ _('Warning'), question,
QMessageBox.Yes | QMessageBox.No,
QMessageBox.No)
if reply == QMessageBox.Yes:
diff --git a/spyder/plugins/ipythonconsole/utils/stdfile.py b/spyder/plugins/ipythonconsole/utils/stdfile.py
index c0cb22fa454..01cc262d343 100644
--- a/spyder/plugins/ipythonconsole/utils/stdfile.py
+++ b/spyder/plugins/ipythonconsole/utils/stdfile.py
@@ -21,26 +21,29 @@
from spyder.utils.programs import get_temp_dir
-def std_filename(connection_file, extension, std_dir=None):
+# For testing
+IPYCONSOLE_TEST_DIR = None
+IPYCONSOLE_TEST_NO_STDERR = False
+
+
+def std_filename(connection_file, extension):
"""Filename to save kernel output."""
json_file = osp.basename(connection_file)
file = json_file.split('.json')[0] + extension
- if std_dir is not None:
- file = osp.join(std_dir, file)
- else:
- try:
- file = osp.join(get_temp_dir(), file)
- except (IOError, OSError):
- file = None
- return file
+ if IPYCONSOLE_TEST_DIR is not None:
+ return osp.join(IPYCONSOLE_TEST_DIR, file)
+ try:
+ return osp.join(get_temp_dir(), file)
+ except (IOError, OSError):
+ return None
class StdFile:
- def __init__(self, connection_file, extension=None, std_dir=None):
+ def __init__(self, connection_file, extension=None):
if extension is None:
self.filename = connection_file
else:
- self.filename = std_filename(connection_file, extension, std_dir)
+ self.filename = std_filename(connection_file, extension)
self._mtime = 0
self._cursor = 0
self._handle = None
@@ -48,6 +51,8 @@ def __init__(self, connection_file, extension=None, std_dir=None):
@property
def handle(self):
"""Get handle to file."""
+ if IPYCONSOLE_TEST_NO_STDERR:
+ return None
if self._handle is None and self.filename is not None:
# Needed to prevent any error that could appear.
# See spyder-ide/spyder#6267.
@@ -67,10 +72,13 @@ def remove(self):
if self._handle is not None:
self._handle.close()
os.remove(self.filename)
- self._handle = None
except Exception:
pass
+ self._handle = None
+ self._mtime = 0
+ self._cursor = 0
+
def get_contents(self):
"""Get the contents of the std kernel file."""
try:
diff --git a/spyder/plugins/ipythonconsole/widgets/client.py b/spyder/plugins/ipythonconsole/widgets/client.py
index 232aef08fc7..4760ab725ff 100644
--- a/spyder/plugins/ipythonconsole/widgets/client.py
+++ b/spyder/plugins/ipythonconsole/widgets/client.py
@@ -17,28 +17,27 @@
import logging
import os
import os.path as osp
-import re
from string import Template
import time
+import traceback
# Third party imports (qtpy)
from qtpy.QtCore import QUrl, QTimer, Signal, Slot, QThread
-from qtpy.QtWidgets import (QMessageBox, QVBoxLayout, QWidget)
+from qtpy.QtWidgets import QVBoxLayout, QWidget
# Local imports
from spyder.api.translations import get_translation
from spyder.api.widgets.mixins import SpyderWidgetMixin
from spyder.config.base import (
- get_home_dir, get_module_source_path, running_under_pytest)
+ get_home_dir, get_module_source_path, get_conf_path)
from spyder.utils.icon_manager import ima
from spyder.utils import sourcecode
from spyder.utils.image_path_manager import get_image_path
from spyder.utils.installers import InstallerIPythonKernelError
-from spyder.utils.encoding import get_coding
from spyder.utils.environ import RemoteEnvDialog
from spyder.utils.palette import QStylePalette
from spyder.utils.qthelpers import add_actions, DialogManager
-from spyder.py3compat import to_text_string
+from spyder.plugins.ipythonconsole import SpyderKernelError
from spyder.plugins.ipythonconsole.widgets import ShellWidget
from spyder.widgets.collectionseditor import CollectionsEditor
from spyder.widgets.mixins import SaveHistoryMixin
@@ -89,53 +88,40 @@ class ClientWidget(QWidget, SaveHistoryMixin, SpyderWidgetMixin):
'# *** Spyder Python Console History Log ***', ]
def __init__(self, parent, id_,
- history_filename, config_options,
- additional_options, interpreter_versions,
- connection_file=None, hostname=None,
+ config_options,
+ additional_options,
+ interpreter_versions,
context_menu_actions=(),
menu_actions=None,
- is_external_kernel=False,
- is_spyder_kernel=True,
given_name=None,
give_focus=True,
options_button=None,
- show_elapsed_time=False,
- reset_warning=True,
- ask_before_restart=True,
- ask_before_closing=False,
- css_path=None,
handlers={},
- stderr_obj=None,
- stdout_obj=None,
- fault_obj=None,
initial_cwd=None):
super(ClientWidget, self).__init__(parent)
- SaveHistoryMixin.__init__(self, history_filename)
+ SaveHistoryMixin.__init__(self, get_conf_path('history.py'))
# --- Init attrs
self.container = parent
self.id_ = id_
- self.connection_file = connection_file
- self.hostname = hostname
self.menu_actions = menu_actions
- self.is_external_kernel = is_external_kernel
self.given_name = given_name
- self.show_elapsed_time = show_elapsed_time
- self.reset_warning = reset_warning
- self.ask_before_restart = ask_before_restart
- self.ask_before_closing = ask_before_closing
self.initial_cwd = initial_cwd
# --- Other attrs
+ self.kernel_handler = None
+ self.hostname = None
+ self.show_elapsed_time = self.get_conf('show_elapsed_time')
+ self.reset_warning = self.get_conf('show_reset_namespace_warning')
self.context_menu_actions = context_menu_actions
self.options_button = options_button
self.history = []
self.allow_rename = True
- self.is_error_shown = False
self.error_text = None
self.restart_thread = None
self.give_focus = give_focus
+ css_path = self.get_conf('css_path', section='appearance')
if css_path is None:
self.css_path = CSS_PATH
else:
@@ -147,8 +133,6 @@ def __init__(self, parent, id_,
ipyclient=self,
additional_options=additional_options,
interpreter_versions=interpreter_versions,
- is_external_kernel=is_external_kernel,
- is_spyder_kernel=is_spyder_kernel,
handlers=handlers,
local_kernel=True
)
@@ -158,7 +142,6 @@ def __init__(self, parent, id_,
# To keep a reference to the page to be displayed
# in infowidget
self.info_page = None
- self._before_prompt_is_ready()
# Elapsed time
self.t0 = time.monotonic()
@@ -179,17 +162,7 @@ def __init__(self, parent, id_,
self.dialog_manager = DialogManager()
# --- Standard files handling
- self.stderr_obj = stderr_obj
- self.stdout_obj = stdout_obj
- self.fault_obj = fault_obj
self.std_poll_timer = None
- if self.stderr_obj is not None or self.stdout_obj is not None:
- self.std_poll_timer = QTimer(self)
- self.std_poll_timer.timeout.connect(self.poll_std_file_change)
- self.std_poll_timer.setInterval(1000)
- self.std_poll_timer.start()
- self.shellwidget.executed.connect(self.poll_std_file_change)
-
self.start_successful = False
def __del__(self):
@@ -200,10 +173,9 @@ def __del__(self):
self.restart_thread.wait()
# ----- Private methods ---------------------------------------------------
- def _before_prompt_is_ready(self, show_loading_page=True):
+ def _before_prompt_is_ready(self):
"""Configuration before kernel is connected."""
- if show_loading_page:
- self._show_loading_page()
+ self._show_loading_page()
self.shellwidget.sig_prompt_ready.connect(
self._when_prompt_is_ready)
# If remote execution, the loading page should be hidden as well
@@ -212,7 +184,11 @@ def _before_prompt_is_ready(self, show_loading_page=True):
def _when_prompt_is_ready(self):
"""Configuration after the prompt is shown."""
+ if self.error_text:
+ # an error occured during startup, but after the prompt was sent
+ return
self.start_successful = True
+
# To hide the loading page
self._hide_loading_page()
@@ -271,30 +247,6 @@ def _hide_loading_page(self):
self.set_info_page()
self.shellwidget.show()
- def _read_stderr(self):
- """Read the stderr file of the kernel."""
- # We need to read stderr_file as bytes to be able to
- # detect its encoding with chardet
- f = open(self.stderr_file, 'rb')
-
- try:
- stderr_text = f.read()
-
- # This is needed to avoid showing an empty error message
- # when the kernel takes too much time to start.
- # See spyder-ide/spyder#8581.
- if not stderr_text:
- return ''
-
- # This is needed since the stderr file could be encoded
- # in something different to utf-8.
- # See spyder-ide/spyder#4191.
- encoding = get_coding(stderr_text)
- stderr_text = to_text_string(stderr_text, encoding)
- return stderr_text
- finally:
- f.close()
-
def _show_mpl_backend_errors(self):
"""
Show possible errors when setting the selected Matplotlib backend.
@@ -407,11 +359,43 @@ def _set_initial_cwd_in_kernel(self):
# ----- Public API --------------------------------------------------------
@property
- def kernel_id(self):
- """Get kernel id."""
- if self.connection_file is not None:
- json_file = osp.basename(self.connection_file)
- return json_file.split('.json')[0]
+ def connection_file(self):
+ if self.kernel_handler is None:
+ return None
+ return self.kernel_handler.connection_file
+
+ @property
+ def stderr_obj(self):
+ if self.kernel_handler is None:
+ return None
+ return self.kernel_handler.stderr_obj
+
+ @property
+ def stdout_obj(self):
+ if self.kernel_handler is None:
+ return None
+ return self.kernel_handler.stdout_obj
+
+ def start_std_poll(self):
+ """Start polling std files"""
+ self.std_poll_timer = QTimer(self)
+ self.std_poll_timer.timeout.connect(self.poll_std_file_change)
+ self.std_poll_timer.setInterval(1000)
+ self.std_poll_timer.start()
+ self.shellwidget.executed.connect(self.poll_std_file_change)
+
+ def connect_kernel(self, kernel_handler):
+ """Connect kernel to client using our handler."""
+ self._before_prompt_is_ready()
+ self.kernel_handler = kernel_handler
+ if (
+ kernel_handler.stderr_obj is not None
+ or kernel_handler.stdout_obj is not None
+ ):
+ self.start_std_poll()
+
+ # Actually do the connection
+ self.shellwidget.connect_kernel(kernel_handler)
def remove_std_files(self, is_last_client=True):
"""Remove stderr_file associated with the client."""
@@ -421,13 +405,8 @@ def remove_std_files(self, is_last_client=True):
pass
if self.std_poll_timer is not None:
self.std_poll_timer.stop()
- if is_last_client:
- if self.stderr_obj is not None:
- self.stderr_obj.remove()
- if self.stdout_obj is not None:
- self.stdout_obj.remove()
- if self.fault_obj is not None:
- self.fault_obj.remove()
+ if is_last_client and self.kernel_handler is not None:
+ self.kernel_handler.remove_files()
@Slot()
def poll_std_file_change(self):
@@ -461,12 +440,10 @@ def poll_std_file_change(self):
self.shellwidget._append_plain_text(
'\n' + stdout, before_prompt=True)
- def configure_shellwidget(self, give_focus=True):
+ def connect_shellwidget_signals(self):
"""Configure shellwidget after kernel is connected."""
- self.give_focus = give_focus
-
# Set exit callback
- self.shellwidget.set_exit_callback()
+ self.shellwidget.exit_requested.connect(self.exit_callback)
# To save history
self.shellwidget.executing.connect(self.add_to_history)
@@ -503,23 +480,6 @@ def configure_shellwidget(self, give_focus=True):
# To sync with working directory toolbar
self.shellwidget.executed.connect(self.shellwidget.update_cwd)
- self.send_kernel_configuration()
-
- def send_kernel_configuration(self):
- """Send kernel configuration to kernel."""
-
- # To apply style
- self.set_color_scheme(self.shellwidget.syntax_style, reset=False)
-
- # Enable faulthandler
- if self.fault_obj is not None:
- # To display faulthandler
- self.shellwidget.call_kernel().enable_faulthandler(
- self.fault_obj.filename)
-
- # Give a chance to plugins to configure the kernel
- self.shellwidget.sig_config_kernel_requested.emit()
-
def add_to_history(self, command):
"""Add command to history"""
if self.shellwidget.is_debugging():
@@ -541,6 +501,12 @@ def stop_button_click_handler(self):
def show_kernel_error(self, error):
"""Show kernel initialization errors in infowidget."""
+ if isinstance(error, Exception):
+ if isinstance(error, SpyderKernelError):
+ error = error.args[0]
+ else:
+ error = _("The error is:
"
+ "{}").format(traceback.format_exc())
self.error_text = error
if self.is_benign_error(error):
@@ -571,9 +537,6 @@ def show_kernel_error(self, error):
self.shellwidget.hide()
self.infowidget.show()
- # Tell the client we're in error mode
- self.is_error_shown = True
-
# Stop shellwidget
self.shellwidget.shutdown()
self.remove_std_files(is_last_client=False)
@@ -630,10 +593,6 @@ def get_control(self):
else:
return self.shellwidget._control
- def get_kernel(self):
- """Get kernel associated with this client"""
- return self.shellwidget.kernel_manager
-
def add_actions_to_context_menu(self, menu):
"""Add actions to IPython widget context menu"""
add_actions(menu, self.context_menu_actions)
@@ -682,8 +641,9 @@ def shutdown(self, is_last_client):
self.restart_thread.quit()
self.restart_thread.wait()
shutdown_kernel = (
- is_last_client and not self.is_external_kernel
- and not self.is_error_shown)
+ is_last_client and not self.shellwidget.is_external_kernel
+ and not self.error_text
+ )
self.shellwidget.shutdown(shutdown_kernel)
self.remove_std_files(shutdown_kernel)
@@ -705,58 +665,47 @@ def restart_kernel(self):
Licensed under the BSD license
"""
sw = self.shellwidget
+ if sw.is_external_kernel:
+ sw._append_plain_text(
+ _('Cannot restart a kernel not started by Spyder\n'),
+ before_prompt=True
+ )
+ return
- if not running_under_pytest() and self.ask_before_restart:
- message = _('Are you sure you want to restart the kernel?')
- buttons = QMessageBox.Yes | QMessageBox.No
- result = QMessageBox.question(self, _('Restart kernel?'),
- message, buttons)
- else:
- result = None
-
- if (result == QMessageBox.Yes or
- running_under_pytest() or
- not self.ask_before_restart):
- if sw.kernel_manager:
- if self.infowidget is not None:
- if self.infowidget.isVisible():
- self.infowidget.hide()
-
- if self._abort_kernel_restart():
- sw.spyder_kernel_comm.close()
- return
+ if self.infowidget is not None:
+ if self.infowidget.isVisible():
+ self.infowidget.hide()
- self._show_loading_page()
+ # Close comm
+ sw.spyder_kernel_comm.close()
- # Close comm
- sw.spyder_kernel_comm.close()
+ if self._abort_kernel_restart():
+ return
- # Stop autorestart mechanism
- sw.kernel_manager.stop_restarter()
- sw.kernel_manager.autorestart = False
+ # Stop autorestart mechanism
+ sw.kernel_manager.stop_restarter()
+ sw.kernel_manager.autorestart = False
- # Reconfigure client before the new kernel is connected again.
- self._before_prompt_is_ready(show_loading_page=False)
+ # Reconfigure client before the new kernel is connected again.
+ self._before_prompt_is_ready()
- # Create and run restarting thread
- if (self.restart_thread is not None
- and self.restart_thread.isRunning()):
- self.restart_thread.finished.disconnect()
- self.restart_thread.quit()
- self.restart_thread.wait()
- self.restart_thread = QThread(None)
- self.restart_thread.run = self._restart_thread_main
- self.restart_thread.error = None
- self.restart_thread.finished.connect(
- lambda: self._finalise_restart(True))
- self.restart_thread.start()
+ # Replace std files to avoid catching old kernel errors
+ self.kernel_handler.replace_std_files()
- else:
- sw._append_plain_text(
- _('Cannot restart a kernel not started by Spyder\n'),
- before_prompt=True
- )
- self._hide_loading_page()
+ # Create and run restarting thread
+ if (
+ self.restart_thread is not None
+ and self.restart_thread.isRunning()
+ ):
+ self.restart_thread.finished.disconnect()
+ self.restart_thread.quit()
+ self.restart_thread.wait()
+ self.restart_thread = QThread(None)
+ self.restart_thread.run = self._restart_thread_main
+ self.restart_thread.error = None
+ self.restart_thread.finished.connect(
+ lambda: self._finalise_restart(True))
+ self.restart_thread.start()
def _restart_thread_main(self):
"""Restart the kernel in a thread."""
@@ -772,7 +721,7 @@ def _finalise_restart(self, reset=False):
sw = self.shellwidget
if self._abort_kernel_restart():
- sw.spyder_kernel_comm.close()
+ sw.spyder_kernel_comm.remove()
return
if self.restart_thread and self.restart_thread.error is not None:
@@ -781,15 +730,15 @@ def _finalise_restart(self, reset=False):
before_prompt=True
)
else:
- if self.fault_obj is not None:
- fault = self.fault_obj.get_contents()
- if fault:
- fault = self.filter_fault(fault)
- self.shellwidget._append_plain_text(
- '\n' + fault, before_prompt=True)
+ fault = self.kernel_handler.get_fault_text()
+ if fault:
+ self.shellwidget._append_plain_text(
+ '\n' + fault, before_prompt=True)
# Reset Pdb state and reopen comm
- sw._pdb_recursion_level = 0
+ sw.reset_kernel_state()
+
+ # Reopen comm
sw.spyder_kernel_comm.remove()
try:
sw.spyder_kernel_comm.open_comm(sw.kernel_client)
@@ -802,64 +751,17 @@ def _finalise_restart(self, reset=False):
sw.kernel_manager.autorestart = True
sw.kernel_manager.start_restarter()
- # For spyder-ide/spyder#6235, IPython was changing the
- # setting of %colors on windows by assuming it was using a
- # dark background. This corrects it based on the scheme.
- self.set_color_scheme(sw.syntax_style, reset=reset)
+ if reset:
+ sw.reset(clear=True)
sw._append_html(_("
Restarting kernel...
"),
before_prompt=True)
sw.insert_horizontal_ruler()
- self.send_kernel_configuration()
+ sw.send_spyder_kernel_configuration()
- self._hide_loading_page()
self.restart_thread = None
self.sig_execution_state_changed.emit()
- def filter_fault(self, fault):
- """Get a fault from a previous session."""
- thread_regex = (
- r"(Current thread|Thread) "
- r"(0x[\da-f]+) \(most recent call first\):"
- r"(?:.|\r\n|\r|\n)+?(?=Current thread|Thread|\Z)")
- # Keep line for future improvments
- # files_regex = r"File \"([^\"]+)\", line (\d+) in (\S+)"
-
- main_re = "Main thread id:(?:\r\n|\r|\n)(0x[0-9a-f]+)"
- main_id = 0
- for match in re.finditer(main_re, fault):
- main_id = int(match.group(1), base=16)
-
- system_re = ("System threads ids:"
- "(?:\r\n|\r|\n)(0x[0-9a-f]+(?: 0x[0-9a-f]+)+)")
- ignore_ids = []
- start_idx = 0
- for match in re.finditer(system_re, fault):
- ignore_ids = [int(i, base=16) for i in match.group(1).split()]
- start_idx = match.span()[1]
- text = ""
- for idx, match in enumerate(re.finditer(thread_regex, fault)):
- if idx == 0:
- text += fault[start_idx:match.span()[0]]
- thread_id = int(match.group(2), base=16)
- if thread_id != main_id:
- if thread_id in ignore_ids:
- continue
- if "wurlitzer.py" in match.group(0):
- # Wurlitzer threads are launched later
- continue
- text += "\n" + match.group(0) + "\n"
- else:
- try:
- pattern = (r".*(?:/IPython/core/interactiveshell\.py|"
- r"\\IPython\\core\\interactiveshell\.py).*")
- match_internal = next(re.finditer(pattern, match.group(0)))
- end_idx = match_internal.span()[0]
- except StopIteration:
- end_idx = None
- text += "\nMain thread:\n" + match.group(0)[:end_idx] + "\n"
- return text
-
@Slot(str)
def kernel_restarted_message(self, msg):
"""Show kernel restarted/died messages."""
diff --git a/spyder/plugins/ipythonconsole/widgets/main_widget.py b/spyder/plugins/ipythonconsole/widgets/main_widget.py
index 611e375dc81..96e0d2e5608 100644
--- a/spyder/plugins/ipythonconsole/widgets/main_widget.py
+++ b/spyder/plugins/ipythonconsole/widgets/main_widget.py
@@ -12,19 +12,16 @@
import os
import os.path as osp
import sys
-import traceback
-import uuid
# Third-party imports
from jupyter_client.connect import find_connection_file
-from jupyter_core.paths import jupyter_config_dir, jupyter_runtime_dir
+from jupyter_core.paths import jupyter_config_dir
from qtpy.QtCore import Signal, Slot
from qtpy.QtGui import QColor
from qtpy.QtWebEngineWidgets import WEBENGINE
from qtpy.QtWidgets import (
QApplication, QHBoxLayout, QLabel, QMessageBox, QVBoxLayout, QWidget)
from traitlets.config.loader import Config, load_pyconfig_files
-from zmq.ssh import tunnel as zmqtunnel
# Local imports
from spyder.api.config.decorators import on_conf_change
@@ -32,16 +29,14 @@
from spyder.api.widgets.main_widget import PluginMainWidget
from spyder.api.widgets.menus import MENU_SEPARATOR
from spyder.config.base import (
- get_conf_path, get_home_dir, running_under_pytest)
-from spyder.plugins.ipythonconsole import SpyderKernelError
+ get_home_dir, running_under_pytest)
+from spyder.plugins.ipythonconsole.utils.kernel_handler import KernelHandler
from spyder.plugins.ipythonconsole.utils.kernelspec import SpyderKernelSpec
-from spyder.plugins.ipythonconsole.utils.manager import SpyderKernelManager
-from spyder.plugins.ipythonconsole.utils.client import SpyderKernelClient
-from spyder.plugins.ipythonconsole.utils.ssh import openssh_tunnel
from spyder.plugins.ipythonconsole.utils.style import create_qss_style
from spyder.plugins.ipythonconsole.widgets import (
ClientWidget, ConsoleRestartDialog, COMPLETION_WIDGET_TYPE,
- KernelConnectionDialog, PageControlWidget, ShellWidget)
+ KernelConnectionDialog, PageControlWidget)
+from spyder.plugins.ipythonconsole.widgets.mixins import CachedKernelMixin
from spyder.py3compat import PY38_OR_MORE
from spyder.utils import encoding, programs, sourcecode
from spyder.utils.misc import get_error_match, remove_backslashes
@@ -49,7 +44,6 @@
from spyder.widgets.browser import FrameWebView
from spyder.widgets.findreplace import FindReplace
from spyder.widgets.tabs import Tabs
-from spyder.plugins.ipythonconsole.utils.stdfile import StdFile
# Localization
@@ -111,7 +105,7 @@ class IPythonConsoleWidgetOptionsMenuSections:
# --- Widgets
# ----------------------------------------------------------------------------
-class IPythonConsoleWidget(PluginMainWidget):
+class IPythonConsoleWidget(PluginMainWidget, CachedKernelMixin):
"""
IPython Console plugin
@@ -208,15 +202,6 @@ class IPythonConsoleWidget(PluginMainWidget):
The shellwigdet.
"""
- sig_external_spyder_kernel_connected = Signal(object)
- """
- This signal is emitted when we connect to an external Spyder kernel.
- Parameters
- ----------
- shellwidget: spyder.plugins.ipyconsole.widgets.shell.ShellWidget
- The shellwigdet that was connected to the kernel.
- """
-
sig_render_plain_text_requested = Signal(str)
"""
This signal is emitted to request a plain text help render.
@@ -261,11 +246,6 @@ class IPythonConsoleWidget(PluginMainWidget):
The new working directory path.
"""
- # Error messages
- PERMISSION_ERROR_MSG = _("The directory {} is not writable and it is "
- "required to create IPython consoles. Please "
- "make it writable.")
-
def __init__(self, name=None, plugin=None, parent=None):
super().__init__(name, plugin, parent)
@@ -276,7 +256,6 @@ def __init__(self, name=None, plugin=None, parent=None):
self.mainwindow_close = False
self.active_project_path = None
self.create_new_client_if_empty = True
- self.css_path = self.get_conf('css_path', section='appearance')
self.run_cell_filename = None
self.interrupt_action = None
self.initial_conf_options = self.get_conf_options()
@@ -291,13 +270,6 @@ def __init__(self, name=None, plugin=None, parent=None):
# Attrs for testing
self._testing = bool(os.environ.get('IPYCONSOLE_TESTING'))
- self._test_dir = os.environ.get('IPYCONSOLE_TEST_DIR')
- self._test_no_stderr = os.environ.get('IPYCONSOLE_TEST_NO_STDERR')
-
- # Create temp dir on testing to save kernel errors
- if self._test_dir:
- if not osp.isdir(osp.join(self._test_dir)):
- os.makedirs(osp.join(self._test_dir))
layout = QVBoxLayout()
layout.setSpacing(0)
@@ -366,9 +338,6 @@ def __init__(self, name=None, plugin=None, parent=None):
# See spyder-ide/spyder#11880
self._init_asyncio_patch()
- # To cache kernel properties
- self._cached_kernel_properties = None
-
# Initial value for the current working directory
self._current_working_directory = get_home_dir()
@@ -671,26 +640,6 @@ def change_client_reset_warning(value=value):
change_client_reset_warning,
value)
- @on_conf_change(option='ask_before_restart')
- def change_clients_ask_before_restart(self, value):
- for idx, client in enumerate(self.clients):
- def change_client_ask_before_restart(value=value):
- client.ask_before_restart = value
- self._change_client_conf(
- client,
- change_client_ask_before_restart,
- value)
-
- @on_conf_change(option='ask_before_closing')
- def change_clients_ask_before_closing(self, value):
- for idx, client in enumerate(self.clients):
- def change_client_ask_before_closing(value=value):
- client.ask_before_closing = value
- self._change_client_conf(
- client,
- change_client_ask_before_closing,
- value)
-
@on_conf_change(option='show_calltips')
def change_clients_show_calltips(self, value):
for idx, client in enumerate(self.clients):
@@ -875,19 +824,17 @@ def change_client_mpl_conf(o=options, c=client):
else:
self._change_client_mpl_conf(options, client)
elif restart and restart_all:
- current_ask_before_restart = client.ask_before_restart
- client.ask_before_restart = False
- client.restart_kernel()
- client.ask_before_restart = current_ask_before_restart
-
- if (((pylab_restart and current_client_backend_require_restart)
- or restart_needed) and restart_current and current_client):
- current_client_ask_before_restart = (
- current_client.ask_before_restart)
- current_client.ask_before_restart = False
- current_client.restart_kernel()
- current_client.ask_before_restart = (
- current_client_ask_before_restart)
+ self.restart_kernel(client, ask_before_restart=False)
+
+ if (
+ (
+ (pylab_restart and current_client_backend_require_restart)
+ or restart_needed
+ )
+ and restart_current
+ and current_client
+ ):
+ self.restart_kernel(current_client, ask_before_restart=False)
# ---- Private methods
# -------------------------------------------------------------------------
@@ -1002,35 +949,6 @@ def _init_asyncio_patch(self):
asyncio.set_event_loop_policy(
WindowsSelectorEventLoopPolicy())
- def _new_connection_file(self):
- """
- Generate a new connection file
-
- Taken from jupyter_client/console_app.py
- Licensed under the BSD license
- """
- # Check if jupyter_runtime_dir exists (Spyder addition)
- if not osp.isdir(jupyter_runtime_dir()):
- try:
- os.makedirs(jupyter_runtime_dir())
- except (IOError, OSError):
- return None
- cf = ''
- while not cf:
- ident = str(uuid.uuid4()).split('-')[-1]
- cf = os.path.join(jupyter_runtime_dir(), 'kernel-%s.json' % ident)
- cf = cf if not os.path.exists(cf) else ''
- return cf
-
- def _shellwidget_started(self, client):
- self.sig_shellwidget_created.emit(client.shellwidget)
-
- def _shellwidget_deleted(self, client):
- try:
- self.sig_shellwidget_deleted.emit(client.shellwidget)
- except RuntimeError:
- pass
-
@Slot()
def _create_client_for_kernel(self):
"""Create a client connected to an existing kernel"""
@@ -1038,9 +956,32 @@ def _create_client_for_kernel(self):
(connection_file, hostname, sshkey, password, ok) = connect_output
if not ok:
return
- else:
- self.create_client_for_kernel(connection_file, hostname, sshkey,
- password)
+
+ try:
+ # Fix path
+ connection_file = self.find_connection_file(connection_file)
+ except (IOError, UnboundLocalError):
+ QMessageBox.critical(self, _('IPython'),
+ _("Unable to connect to "
+ "%s") % connection_file)
+ return
+
+ self.create_client_for_kernel(
+ connection_file, hostname, sshkey, password)
+
+ def find_connection_file(self, connection_file):
+ """Fix connection file path."""
+ cf_path = osp.dirname(connection_file)
+ cf_filename = osp.basename(connection_file)
+ # To change a possible empty string to None
+ cf_path = cf_path if cf_path else None
+ connection_file = find_connection_file(filename=cf_filename,
+ path=cf_path)
+ if os.path.splitext(connection_file)[1] != ".json":
+ # There might be a file with the same id in the path.
+ connection_file = find_connection_file(
+ filename=cf_filename + ".json", path=cf_path)
+ return connection_file
# ---- Public API
# -------------------------------------------------------------------------
@@ -1062,7 +1003,7 @@ def refresh_container(self, give_focus=False):
Refresh interface depending on the current widget client available.
Refreshes corner widgets and actions as well as the info widget and
- sets the shellwdiget and client signals
+ sets the shellwidget and client signals
"""
client = None
if self.tabwidget.count():
@@ -1137,6 +1078,9 @@ def add_tab(self, client, name, filename='', give_focus=True):
client.get_control().setFocus()
self.update_tabs_text()
+ # Register client
+ self.register_client(client)
+
def move_tab(self, index_from, index_to):
"""
Move tab (tabs themselves have already been moved by the tabwidget).
@@ -1329,16 +1273,6 @@ def additional_options(self, is_pylab=False, is_sympy=False):
return options
# ---- For client widgets
- def set_client_elapsed_time(self, client):
- """Set elapsed time for slave clients."""
- related_clients = self.get_related_clients(client)
- for cl in related_clients:
- if cl.timer is not None:
- client.t0 = cl.t0
- client.timer.timeout.connect(client.show_time)
- client.timer.start(1000)
- break
-
def get_focus_client(self):
"""Return current client with focus, if any"""
widget = QApplication.focusWidget()
@@ -1371,203 +1305,119 @@ def create_new_client(self, give_focus=True, filename='', is_cython=False,
self.master_clients += 1
client_id = dict(int_id=str(self.master_clients),
str_id='A')
- std_dir = self._test_dir if self._test_dir else None
- cf, km, kc, stderr_obj, stdout_obj = self.get_new_kernel(
- is_cython, is_pylab, is_sympy, std_dir=std_dir, cache=cache)
- if cf is not None:
- fault_obj = StdFile(cf, '.fault', std_dir)
- else:
- fault_obj = None
-
- show_elapsed_time = self.get_conf('show_elapsed_time')
- reset_warning = self.get_conf('show_reset_namespace_warning')
- ask_before_restart = self.get_conf('ask_before_restart')
- ask_before_closing = self.get_conf('ask_before_closing')
- client = ClientWidget(self, id_=client_id,
- history_filename=get_conf_path('history.py'),
- config_options=self.config_options(),
- additional_options=self.additional_options(
- is_pylab=is_pylab,
- is_sympy=is_sympy),
- interpreter_versions=self.interpreter_versions(),
- connection_file=cf,
- context_menu_actions=self.context_menu_actions,
- show_elapsed_time=show_elapsed_time,
- reset_warning=reset_warning,
- given_name=given_name,
- give_focus=give_focus,
- ask_before_restart=ask_before_restart,
- ask_before_closing=ask_before_closing,
- css_path=self.css_path,
- handlers=self.registered_spyder_kernel_handlers,
- stderr_obj=stderr_obj,
- stdout_obj=stdout_obj,
- fault_obj=fault_obj,
- initial_cwd=initial_cwd)
+ client = ClientWidget(
+ self,
+ id_=client_id,
+ config_options=self.config_options(),
+ additional_options=self.additional_options(
+ is_pylab=is_pylab,
+ is_sympy=is_sympy),
+ interpreter_versions=self.interpreter_versions(),
+ context_menu_actions=self.context_menu_actions,
+ given_name=given_name,
+ give_focus=give_focus,
+ handlers=self.registered_spyder_kernel_handlers,
+ initial_cwd=initial_cwd,
+ )
+ # Add client to widget
self.add_tab(
client, name=client.get_name(), filename=filename,
give_focus=give_focus)
- if cf is None:
- error_msg = self.PERMISSION_ERROR_MSG.format(jupyter_runtime_dir())
- client.show_kernel_error(error_msg)
- return
+ # Create new kernel
+ kernel_spec = SpyderKernelSpec(
+ is_cython=is_cython,
+ is_pylab=is_pylab,
+ is_sympy=is_sympy
+ )
- self.connect_client_to_kernel(client, km, kc)
- if client.shellwidget.kernel_manager is None:
+ try:
+ kernel_handler = self.get_cached_kernel(kernel_spec, cache=cache)
+ except Exception as e:
+ client.show_kernel_error(e)
return
- self.register_client(client, give_focus=give_focus)
+
+ # Connect kernel to client
+ client.connect_kernel(kernel_handler)
return client
def create_client_for_kernel(self, connection_file, hostname, sshkey,
password):
"""Create a client connected to an existing kernel."""
- # Verifying if the connection file exists
- try:
- cf_path = osp.dirname(connection_file)
- cf_filename = osp.basename(connection_file)
- # To change a possible empty string to None
- cf_path = cf_path if cf_path else None
- connection_file = find_connection_file(filename=cf_filename,
- path=cf_path)
- if os.path.splitext(connection_file)[1] != ".json":
- # There might be a file with the same id in the path.
- connection_file = find_connection_file(
- filename=cf_filename + ".json", path=cf_path)
- except (IOError, UnboundLocalError):
- QMessageBox.critical(self, _('IPython'),
- _("Unable to connect to "
- "%s") % connection_file)
- return
-
- # Getting the master id that corresponds to the client
- # (i.e. the i in i/A)
- master_id = None
given_name = None
- is_external_kernel = True
- known_spyder_kernel = False
- slave_ord = ord('A') - 1
- kernel_manager = None
- stderr_obj = None
- stdout_obj = None
- fault_obj = None
+ master_client = None
+ related_clients = []
for cl in self.clients:
if connection_file in cl.connection_file:
- if cl.get_kernel() is not None:
- kernel_manager = cl.get_kernel()
- connection_file = cl.connection_file
- if master_id is None:
- master_id = cl.id_['int_id']
- is_external_kernel = cl.shellwidget.is_external_kernel
- known_spyder_kernel = cl.shellwidget.is_spyder_kernel
- if cl.stderr_obj:
- stderr_obj = cl.stderr_obj.copy()
- if cl.stdout_obj:
- stdout_obj = cl.stdout_obj.copy()
- if cl.fault_obj:
- fault_obj = cl.fault_obj.copy()
- given_name = cl.given_name
+ if (
+ cl.kernel_handler is not None and
+ hostname == cl.kernel_handler.hostname and
+ sshkey == cl.kernel_handler.sshkey and
+ password == cl.kernel_handler.password
+ ):
+ related_clients.append(cl)
+
+ if len(related_clients) > 0:
+ # Get master client
+ master_client = related_clients[0]
+ given_name = master_client.given_name
+ slave_ord = ord('A') - 1
+ for cl in related_clients:
new_slave_ord = ord(cl.id_['str_id'])
if new_slave_ord > slave_ord:
slave_ord = new_slave_ord
- # If we couldn't find a client with the same connection file,
- # it means this is a new master client
- if master_id is None:
+ # Set full client name
+ client_id = dict(int_id=master_client.id_['int_id'],
+ str_id=chr(slave_ord + 1))
+ else:
+ # If we couldn't find a client with the same connection file,
+ # it means this is a new master client
self.master_clients += 1
- master_id = str(self.master_clients)
- # Set full client name
- client_id = dict(int_id=master_id,
- str_id=chr(slave_ord + 1))
+ # Set full client name
+ client_id = dict(int_id=str(self.master_clients), str_id='A')
# Creating the client
- show_elapsed_time = self.get_conf('show_elapsed_time')
- reset_warning = self.get_conf('show_reset_namespace_warning')
- ask_before_restart = self.get_conf('ask_before_restart')
- client = ClientWidget(self,
- id_=client_id,
- given_name=given_name,
- history_filename=get_conf_path('history.py'),
- config_options=self.config_options(),
- additional_options=self.additional_options(),
- interpreter_versions=self.interpreter_versions(),
- connection_file=connection_file,
- context_menu_actions=self.context_menu_actions,
- hostname=hostname,
- is_external_kernel=is_external_kernel,
- is_spyder_kernel=known_spyder_kernel,
- show_elapsed_time=show_elapsed_time,
- reset_warning=reset_warning,
- ask_before_restart=ask_before_restart,
- css_path=self.css_path,
- handlers=self.registered_spyder_kernel_handlers,
- stderr_obj=stderr_obj,
- stdout_obj=stdout_obj,
- fault_obj=fault_obj)
-
- # Create kernel client
- kernel_client = SpyderKernelClient(connection_file=connection_file)
-
- # This is needed for issue spyder-ide/spyder#9304.
- try:
- kernel_client.load_connection_file()
- except Exception as e:
- QMessageBox.critical(self, _('Connection error'),
- _("An error occurred while trying to load "
- "the kernel connection file. The error "
- "was:\n\n") + str(e))
- return
+ client = ClientWidget(
+ self,
+ id_=client_id,
+ given_name=given_name,
+ config_options=self.config_options(),
+ additional_options=self.additional_options(),
+ interpreter_versions=self.interpreter_versions(),
+ context_menu_actions=self.context_menu_actions,
+ handlers=self.registered_spyder_kernel_handlers
+ )
- if hostname is not None:
- try:
- connection_info = dict(
- ip=kernel_client.ip,
- shell_port=kernel_client.shell_port,
- iopub_port=kernel_client.iopub_port,
- stdin_port=kernel_client.stdin_port,
- hb_port=kernel_client.hb_port,
- control_port=kernel_client.control_port)
- newports = self.tunnel_to_kernel(connection_info, hostname,
- sshkey, password)
- (kernel_client.shell_port,
- kernel_client.iopub_port,
- kernel_client.stdin_port,
- kernel_client.hb_port,
- kernel_client.control_port) = newports
- except Exception as e:
- QMessageBox.critical(self, _('Connection error'),
- _("Could not open ssh tunnel. The "
- "error was:\n\n") + str(e))
- return
+ # add hostname for get_name
+ client.hostname = hostname
- # Assign kernel manager and client to shellwidget
- kernel_client.start_channels()
- shellwidget = client.shellwidget
-
- if not known_spyder_kernel:
- shellwidget.sig_is_spykernel.connect(
- self.connect_external_spyder_kernel)
- shellwidget.set_kernel_client_and_manager(
- kernel_client, kernel_manager)
- shellwidget.sig_exception_occurred.connect(
- self.sig_exception_occurred)
- self.sig_shellwidget_created.emit(shellwidget)
- kernel_client.stopped_channels.connect(
- lambda: self.sig_shellwidget_deleted.emit(shellwidget))
+ # Adding a new tab for the client
+ self.add_tab(client, name=client.get_name())
# Set elapsed time, if possible
- if not is_external_kernel:
- self.set_client_elapsed_time(client)
+ if master_client is not None:
+ client.t0 = master_client.t0
+ client.timer.timeout.connect(client.show_time)
+ client.timer.start(1000)
- # Adding a new tab for the client
- self.add_tab(client, name=client.get_name())
+ try:
+ # Get new client for kernel
+ if master_client is not None:
+ kernel_handler = master_client.kernel_handler.copy()
+ else:
+ kernel_handler = KernelHandler.from_connection_file(
+ connection_file, hostname, sshkey, password)
+ except Exception as e:
+ client.show_kernel_error(e)
+ return
- # Register client
- self.register_client(client)
+ # Connect kernel
+ client.connect_kernel(kernel_handler)
def create_pylab_client(self):
"""Force creation of Pylab client"""
@@ -1581,134 +1431,6 @@ def create_cython_client(self):
"""Force creation of Cython client"""
self.create_new_client(is_cython=True, given_name="Cython")
- def get_new_kernel(self, is_cython=False, is_pylab=False,
- is_sympy=False, std_dir=None, cache=True):
- """Get a new kernel, and cache one for next time."""
- # Cache another kernel for next time.
- kernel_spec = self.create_kernel_spec(
- is_cython=is_cython,
- is_pylab=is_pylab,
- is_sympy=is_sympy
- )
-
- new_kernel = self.create_new_kernel(kernel_spec, std_dir)
-
- if new_kernel[2] is None or not cache:
- # error or remove/don't use cache if requested
- self.close_cached_kernel()
- return new_kernel
-
- # Check cached kernel has the same configuration as is being asked
- cached_kernel = None
- if self._cached_kernel_properties is not None:
- (cached_spec,
- cached_env,
- cached_argv,
- cached_dir,
- cached_kernel) = self._cached_kernel_properties
- # Call interrupt_mode so the dict will be the same
- kernel_spec.interrupt_mode
- cached_spec.interrupt_mode
- valid = (std_dir == cached_dir
- and cached_spec.__dict__ == kernel_spec.__dict__
- and kernel_spec.argv == cached_argv
- and kernel_spec.env == cached_env)
- if not valid:
- # Close the kernel
- self.close_cached_kernel()
- cached_kernel = None
-
- # Cache the new kernel
- self._cached_kernel_properties = (
- kernel_spec,
- kernel_spec.env,
- kernel_spec.argv,
- std_dir,
- new_kernel)
-
- if cached_kernel is None:
- return self.create_new_kernel(kernel_spec, std_dir)
-
- return cached_kernel
-
- def close_cached_kernel(self):
- """Close the cached kernel."""
- if self._cached_kernel_properties is None:
- return
- cached_kernel = self._cached_kernel_properties[-1]
- _, kernel_manager, _, stderr_obj, stdout_obj = cached_kernel
- kernel_manager.stop_restarter()
- kernel_manager.shutdown_kernel(now=True)
- self._cached_kernel_properties = None
- if stderr_obj:
- stderr_obj.remove()
- if stdout_obj:
- stdout_obj.remove()
-
- def create_new_kernel(self, kernel_spec, std_dir=None):
- """Create a new kernel."""
- connection_file = self._new_connection_file()
- if connection_file is None:
- return None, None, None, None, None
-
- stderr_obj = None
- stderr_handle = None
- stdout_obj = None
- stdout_handle = None
- if not self._test_no_stderr:
- stderr_obj = StdFile(connection_file, '.stderr', std_dir)
- stderr_handle = stderr_obj.handle
- stdout_obj = StdFile(connection_file, '.stdout', std_dir)
- stdout_handle = stdout_obj.handle
-
- km, kc = self.create_kernel_manager_and_kernel_client(
- connection_file,
- stderr_handle,
- stdout_handle,
- kernel_spec,
- )
- return connection_file, km, kc, stderr_obj, stdout_obj
-
- def connect_client_to_kernel(self, client, km, kc):
- """Connect a client to its kernel."""
- # An error occurred if this is True
- if isinstance(km, str) and kc is None:
- client.shellwidget.kernel_manager = None
- client.show_kernel_error(km)
- return
-
- # This avoids a recurrent, spurious NameError when running our
- # tests in our CIs
- if not self._testing:
- kc.stopped_channels.connect(
- lambda c=client: self._shellwidget_deleted(c))
-
- kc.start_channels(shell=True, iopub=True)
-
- shellwidget = client.shellwidget
- shellwidget.set_kernel_client_and_manager(kc, km)
-
- shellwidget.sig_exception_occurred.connect(
- self.sig_exception_occurred)
-
- # _shellwidget_started() – which emits sig_shellwidget_created() – must
- # be called *after* set_kernel_client_and_manager() has been called.
- # This is required so that plugins can rely on a initialized shell
- # widget in their implementation of
- # ShellConnectMainWidget.create_new_widget() (e.g. if they need to
- # communicate with the kernel using the shell widget kernel client).
- #
- # NOTE kc.started_channels() signal must not be used to emit
- # sig_shellwidget_created(): kc.start_channels()
- # (QtKernelClientMixin.start_channels() from qtconsole module) emits the
- # started_channels() signal. Slots connected to this signal are called
- # directly from within start_channels() [1], i.e. before the shell
- # widget’s initialization by set_kernel_client_and_manager().
- #
- # [1] Assuming no threads are involved, i.e. the signal-slot connection
- # type is Qt.DirectConnection.
- self._shellwidget_started(client)
-
@Slot(str)
def create_client_from_path(self, path):
"""Create a client with its cwd pointing to path."""
@@ -1740,9 +1462,9 @@ def get_client_for_file(self, filename):
break
return client
- def register_client(self, client, give_focus=True):
+ def register_client(self, client):
"""Register new client"""
- client.configure_shellwidget(give_focus=give_focus)
+ client.connect_shellwidget_signals()
# Local vars
shellwidget = client.shellwidget
@@ -1781,6 +1503,16 @@ def register_client(self, client, give_focus=True):
# Show time label
client.sig_time_label.connect(self.time_label.setText)
+ # Exception handling
+ shellwidget.sig_exception_occurred.connect(
+ self.sig_exception_occurred)
+
+ # Closing Shellwidget
+ shellwidget.sig_shellwidget_deleted.connect(
+ self.sig_shellwidget_deleted)
+ shellwidget.sig_shellwidget_created.connect(
+ self.sig_shellwidget_created)
+
def close_client(self, index=None, client=None, ask_recursive=True):
"""Close client tab from index or widget (or close current tab)"""
if not self.tabwidget.count():
@@ -1803,7 +1535,7 @@ def close_client(self, index=None, client=None, ask_recursive=True):
# and eventually ask before closing them
if not self.mainwindow_close and ask_recursive:
close_all = True
- if client.ask_before_closing:
+ if self.get_conf('ask_before_closing'):
close = QMessageBox.question(
self,
self._plugin.get_name(),
@@ -1861,8 +1593,10 @@ def close_all_clients(self):
client.close_client(is_last_client)
open_clients.remove(client)
- # Close all closing shellwidgets.
- ShellWidget.wait_all_shutdown()
+ # Wait for all KernelHandler's to shutdown.
+ for client in self.clients:
+ if client.kernel_handler:
+ client.kernel_handler.wait_shutdown_thread()
# Close cached kernel
self.close_cached_kernel()
@@ -1978,84 +1712,31 @@ def unregister_spyder_kernel_call_handler(self, handler_id):
"""
self.registered_spyder_kernel_handlers.pop(handler_id, None)
- def ssh_tunnel(self, *args, **kwargs):
- if os.name == 'nt':
- return zmqtunnel.paramiko_tunnel(*args, **kwargs)
- else:
- return openssh_tunnel(self, *args, **kwargs)
-
- def tunnel_to_kernel(self, connection_info, hostname, sshkey=None,
- password=None, timeout=10):
- """
- Tunnel connections to a kernel via ssh.
-
- Remote ports are specified in the connection info ci.
- """
- lports = zmqtunnel.select_random_ports(5)
- rports = (connection_info['shell_port'], connection_info['iopub_port'],
- connection_info['stdin_port'], connection_info['hb_port'],
- connection_info['control_port'])
- remote_ip = connection_info['ip']
- for lp, rp in zip(lports, rports):
- self.ssh_tunnel(lp, rp, hostname, remote_ip, sshkey, password,
- timeout)
- return tuple(lports)
-
- def create_kernel_spec(self, is_cython=False,
- is_pylab=False, is_sympy=False):
- """Create a kernel spec for our own kernels"""
- return SpyderKernelSpec(is_cython=is_cython,
- is_pylab=is_pylab,
- is_sympy=is_sympy)
-
- def create_kernel_manager_and_kernel_client(self, connection_file,
- stderr_handle,
- stdout_handle,
- kernel_spec):
- """Create kernel manager and client."""
- # Kernel manager
- try:
- kernel_manager = SpyderKernelManager(
- connection_file=connection_file,
- config=None,
- autorestart=True,
- )
- except SpyderKernelError as e:
- return (e.args[0], None)
- except Exception:
- error_msg = _("The error is:
"
- "{}").format(traceback.format_exc())
- return (error_msg, None)
- kernel_manager._kernel_spec = kernel_spec
+ @Slot()
+ def restart_kernel(self, client=None, ask_before_restart=True):
+ """Restart kernel of current client."""
+ if client is None:
+ client = self.get_current_client()
+ if client is None:
+ return
- # Catch any error generated when trying to start the kernel.
- # See spyder-ide/spyder#7302.
- try:
- kernel_manager.start_kernel(stderr=stderr_handle,
- stdout=stdout_handle,
- env=kernel_spec.env)
- except SpyderKernelError as e:
- return (e.args[0], None)
- except Exception:
- error_msg = _("The error is:
"
- "{}").format(traceback.format_exc())
- return (error_msg, None)
+ self.sig_switch_to_plugin_requested.emit()
- # Kernel client
- kernel_client = kernel_manager.client()
+ ask_before_restart = (
+ ask_before_restart and self.get_conf('ask_before_restart'))
- # Increase time (in seconds) to detect if a kernel is alive.
- # See spyder-ide/spyder#3444.
- kernel_client.hb_channel.time_to_dead = 25.0
+ do_restart = True
+ if ask_before_restart and not running_under_pytest():
+ message = _('Are you sure you want to restart the kernel?')
+ buttons = QMessageBox.Yes | QMessageBox.No
+ result = QMessageBox.question(
+ self, _('Restart kernel?'), message, buttons)
+ do_restart = result == QMessageBox.Yes
- return kernel_manager, kernel_client
+ if not do_restart:
+ return
- def restart_kernel(self):
- """Restart kernel of current client."""
- client = self.get_current_client()
- if client is not None:
- self.sig_switch_to_plugin_requested.emit()
- client.restart_kernel()
+ client.restart_kernel()
def reset_namespace(self):
"""Reset namespace of current client."""
@@ -2071,16 +1752,6 @@ def interrupt_kernel(self):
self.sig_switch_to_plugin_requested.emit()
client.stop_button_click_handler()
- def connect_external_spyder_kernel(self, shellwidget):
- """Connect to an external Spyder kernel."""
- shellwidget.is_spyder_kernel = True
- shellwidget.spyder_kernel_comm.open_comm(shellwidget.kernel_client)
- shellwidget.ipyclient.send_kernel_configuration()
- self.sig_shellwidget_changed.emit(shellwidget)
- self.sig_external_spyder_kernel_connected.emit(shellwidget)
-
- # ---- For running and debugging
- # -------------------------------------------------------------------------
# ---- For cells
def run_cell(self, code, cell_name, filename, run_cell_copy,
method='runcell', focus_to_editor=False):
diff --git a/spyder/plugins/ipythonconsole/widgets/mixins.py b/spyder/plugins/ipythonconsole/widgets/mixins.py
new file mode 100644
index 00000000000..0eb1e6f0c30
--- /dev/null
+++ b/spyder/plugins/ipythonconsole/widgets/mixins.py
@@ -0,0 +1,87 @@
+# -*- coding: utf-8 -*-
+#
+# Copyright © Spyder Project Contributors
+# Licensed under the terms of the MIT License
+# (see spyder/__init__.py for details)
+
+"""
+IPython Console mixins.
+"""
+
+# Local imports
+from spyder.plugins.ipythonconsole.utils.kernel_handler import KernelHandler
+
+
+class CachedKernelMixin:
+ """Cached kernel mixin."""
+
+ def __init__(self):
+ super().__init__()
+ self._cached_kernel_properties = None
+
+ def close_cached_kernel(self):
+ """Close the cached kernel."""
+ if self._cached_kernel_properties is None:
+ return
+ kernel = self._cached_kernel_properties[-1]
+ kernel.close(now=True)
+ kernel.remove_files()
+ self._cached_kernel_properties = None
+
+ def check_cached_kernel_spec(self, kernel_spec):
+ """Test if kernel_spec corresponds to the cached kernel_spec."""
+ if self._cached_kernel_properties is None:
+ return False
+ (
+ cached_spec,
+ cached_env,
+ cached_argv,
+ _,
+ ) = self._cached_kernel_properties
+
+ # Call interrupt_mode so the dict will be the same
+ kernel_spec.interrupt_mode
+ cached_spec.interrupt_mode
+
+ if "PYTEST_CURRENT_TEST" in cached_env:
+ # Make tests faster by using cached kernels
+ # hopefully the kernel will never use PYTEST_CURRENT_TEST
+ cached_env["PYTEST_CURRENT_TEST"] = (
+ kernel_spec.env["PYTEST_CURRENT_TEST"])
+ return (
+ cached_spec.__dict__ == kernel_spec.__dict__
+ and kernel_spec.argv == cached_argv
+ and kernel_spec.env == cached_env
+ )
+
+ def get_cached_kernel(self, kernel_spec, cache=True):
+ """Get a new kernel, and cache one for next time."""
+ # Cache another kernel for next time.
+ new_kernel_handler = KernelHandler.new_from_spec(kernel_spec)
+
+ if not cache:
+ # remove/don't use cache if requested
+ self.close_cached_kernel()
+ return new_kernel_handler
+
+ # Check cached kernel has the same configuration as is being asked
+ cached_kernel_handler = None
+ if self._cached_kernel_properties is not None:
+ cached_kernel_handler = self._cached_kernel_properties[-1]
+ if not self.check_cached_kernel_spec(kernel_spec):
+ # Close the kernel
+ self.close_cached_kernel()
+ cached_kernel_handler = None
+
+ # Cache the new kernel
+ self._cached_kernel_properties = (
+ kernel_spec,
+ kernel_spec.env,
+ kernel_spec.argv,
+ new_kernel_handler,
+ )
+
+ if cached_kernel_handler is None:
+ return KernelHandler.new_from_spec(kernel_spec)
+
+ return cached_kernel_handler
diff --git a/spyder/plugins/ipythonconsole/widgets/shell.py b/spyder/plugins/ipythonconsole/widgets/shell.py
index 8738039bab3..6ff112c9cbe 100644
--- a/spyder/plugins/ipythonconsole/widgets/shell.py
+++ b/spyder/plugins/ipythonconsole/widgets/shell.py
@@ -14,10 +14,9 @@
import os.path as osp
import time
from textwrap import dedent
-from threading import Lock
# Third party imports
-from qtpy.QtCore import Signal, QThread
+from qtpy.QtCore import Signal
from qtpy.QtWidgets import QMessageBox
from qtpy import QtCore, QtWidgets, QtGui
from traitlets import observe
@@ -140,7 +139,6 @@ class ShellWidget(NamepaceBrowserWidget, HelpWidget, DebuggingWidget,
# For ShellWidget
sig_focus_changed = Signal()
new_client = Signal()
- sig_is_spykernel = Signal(object)
sig_kernel_restarted_message = Signal(str)
# Kernel died and restarted (not user requested)
@@ -154,46 +152,18 @@ class ShellWidget(NamepaceBrowserWidget, HelpWidget, DebuggingWidget,
# For printing internal errors
sig_exception_occurred = Signal(dict)
- # Class array of shutdown threads
- shutdown_thread_list = []
-
# To save values and messages returned by the kernel
_kernel_is_starting = True
- # Kernel started or restarted
- sig_kernel_started = Signal()
- sig_kernel_reset = Signal()
-
# Request plugins to send additional configuration to the kernel
sig_config_kernel_requested = Signal()
- @classmethod
- def prune_shutdown_thread_list(cls):
- """Remove shutdown threads."""
- pruned_shutdown_thread_list = []
- for t in cls.shutdown_thread_list:
- try:
- if t.isRunning():
- pruned_shutdown_thread_list.append(t)
- except RuntimeError:
- pass
- cls.shutdown_thread_list = pruned_shutdown_thread_list
-
- @classmethod
- def wait_all_shutdown(cls):
- """Wait for shutdown to finish."""
- for thread in cls.shutdown_thread_list:
- if thread.isRunning():
- try:
- thread.kernel_manager._kill_kernel()
- except Exception:
- pass
- thread.quit()
- thread.wait()
- cls.shutdown_thread_list = []
+ # To notify of kernel connection / disconnection
+ sig_shellwidget_created = Signal(object)
+ sig_shellwidget_deleted = Signal(object)
def __init__(self, ipyclient, additional_options, interpreter_versions,
- is_external_kernel, is_spyder_kernel, handlers, *args, **kw):
+ handlers, *args, **kw):
# To override the Qt widget used by RichJupyterWidget
self.custom_control = ControlWidget
self.custom_page_control = PageControlWidget
@@ -205,8 +175,7 @@ def __init__(self, ipyclient, additional_options, interpreter_versions,
self.ipyclient = ipyclient
self.additional_options = additional_options
self.interpreter_versions = interpreter_versions
- self.is_external_kernel = is_external_kernel
- self.is_spyder_kernel = is_spyder_kernel
+ self.kernel_handler = None
self._cwd = ''
# Keyboard shortcuts
@@ -237,51 +206,52 @@ def __init__(self, ipyclient, additional_options, interpreter_versions,
# Show a message in our installers to explain users how to use
# modules that don't come with them.
self.show_modules_message = is_pynsist() or running_in_mac_app()
- self.shutdown_lock = Lock()
# ---- Public API ---------------------------------------------------------
- def shutdown_kernel(self):
- """Shutdown kernel."""
- with self.shutdown_lock:
- # Avoid calling shutdown_kernel on the same manager twice
- # from different threads to avoid crash.
- if self.kernel_manager.shutting_down:
- return
- self.kernel_manager.shutting_down = True
- try:
- self.kernel_manager.shutdown_kernel()
- except Exception:
- # kernel was externally killed
- pass
+ @property
+ def is_spyder_kernel(self):
+ if self.kernel_handler is None:
+ return False
+ return self.kernel_handler.known_spyder_kernel
+
+ def connect_kernel(self, kernel_handler):
+ """Connect to the kernel using our handler."""
+ # Kernel client
+ kernel_client = kernel_handler.kernel_client
+ kernel_client.stopped_channels.connect(self.notify_deleted)
+ kernel_client.start_channels()
+ self.kernel_client = kernel_client
+
+ self.kernel_manager = kernel_handler.kernel_manager
+ self.kernel_handler = kernel_handler
+
+ # Send message to kernel to check status
+ self.check_spyder_kernel()
+ self.sig_shellwidget_created.emit(self)
+
+ def notify_deleted(self):
+ """Notify that the shellwidget was deleted."""
+ self.sig_shellwidget_deleted.emit(self)
def shutdown(self, shutdown_kernel=True):
"""Shutdown connection and kernel."""
if self.shutting_down:
return
self.shutting_down = True
- if shutdown_kernel:
- if not self.kernel_manager:
- return
+ self.close_kernel(shutdown_kernel)
+ super().shutdown()
- self.interrupt_kernel()
- if self.kernel_manager:
- self.kernel_manager.stop_restarter()
- self.spyder_kernel_comm.close()
- if self.kernel_client is not None:
- self.kernel_client.stop_channels()
- if self.kernel_manager:
- shutdown_thread = QThread(None)
- shutdown_thread.kernel_manager = self.kernel_manager
- shutdown_thread.run = self.shutdown_kernel
- self.shutdown_thread_list.append(shutdown_thread)
- shutdown_thread.start()
- else:
- self.spyder_kernel_comm.close()
- if self.kernel_client is not None:
- self.kernel_client.stop_channels()
+ def close_kernel(self, shutdown_kernel=True):
+ """Close the kernel"""
+ self.kernel_handler.close(shutdown_kernel)
+
+ # Reset state
+ self.reset_kernel_state()
- self.prune_shutdown_thread_list()
- super(ShellWidget, self).shutdown()
+ def reset_kernel_state(self):
+ """Reset the kernel state."""
+ self._prompt_requested = False
+ self._pdb_recursion_level = 0
def call_kernel(self, interrupt=False, blocking=False, callback=None,
timeout=None, display_error=False):
@@ -315,29 +285,45 @@ def call_kernel(self, interrupt=False, blocking=False, callback=None,
display_error=display_error
)
- def set_kernel_client_and_manager(self, kernel_client, kernel_manager):
- """Set the kernel client and manager"""
- self.kernel_manager = kernel_manager
- self.kernel_client = kernel_client
+ @property
+ def is_external_kernel(self):
+ """Check if this is an external kernel."""
+ return self.kernel_manager is None
- # Send message to kernel to check status
- self.check_spyder_kernel()
+ def setup_spyder_kernel(self):
+ """Setup spyder kernel"""
+ self.kernel_handler.open_comm(self.spyder_kernel_comm)
- if self.is_spyder_kernel:
- # For completion
- kernel_client.control_channel.message_received.connect(
- self._dispatch)
- self.spyder_kernel_comm.open_comm(kernel_client)
+ # For completions
+ self.kernel_client.control_channel.message_received.connect(
+ self._dispatch)
# Redefine the complete method to work while debugging.
self._redefine_complete_for_dbg(self.kernel_client)
+ # Send configuration
+ self.send_spyder_kernel_configuration()
+
def pop_execute_queue(self):
"""Pop one waiting instruction."""
if self._execute_queue:
self.execute(*self._execute_queue.pop(0))
- # ---- Public API ---------------------------------------------------------
+ def send_spyder_kernel_configuration(self):
+ """Send kernel configuration to spyder kernel."""
+ # To apply style
+ self.set_color_scheme(self.syntax_style, reset=False)
+
+ ffn = self.kernel_handler.get_fault_filename()
+
+ # Enable faulthandler
+ if ffn:
+ # To display faulthandler
+ self.call_kernel().enable_faulthandler(ffn)
+
+ # Give a chance to plugins to configure the kernel
+ self.sig_config_kernel_requested.emit()
+
def interrupt_kernel(self):
"""Attempts to interrupt the running kernel."""
# Empty queue when interrupting
@@ -365,10 +351,6 @@ def execute(self, source=None, hidden=False, interactive=False):
return
super(ShellWidget, self).execute(source, hidden, interactive)
- def set_exit_callback(self):
- """Set exit callback for this shell."""
- self.exit_requested.connect(self.ipyclient.exit_callback)
-
def is_running(self):
if self.kernel_client is not None and \
self.kernel_client.channels_running:
@@ -396,29 +378,25 @@ def check_spyder_kernel_callback(self, reply):
if data is not None and 'text/plain' in data:
spyder_kernel_info = ast.literal_eval(data['text/plain'])
if not spyder_kernel_info:
- # The running_under_pytest() part can be removed when
- # spyder-kernels 3 is released. This is needed for
- # the test_conda_env_activation test
- if running_under_pytest():
- return
-
if self.is_spyder_kernel:
# spyder-kernels version < 3.0
- self.ipyclient.show_kernel_error(
+ self.append_html_message(
ERROR_SPYDER_KERNEL_VERSION_OLD.format(
SPYDER_KERNELS_MIN_VERSION,
SPYDER_KERNELS_MAX_VERSION,
SPYDER_KERNELS_CONDA,
SPYDER_KERNELS_PIP
- )
+ ),
+ before_prompt=True
)
+ self.kernel_handler.known_spyder_kernel = False
return
version, pyexec = spyder_kernel_info
if not check_version_range(version, SPYDER_KERNELS_VERSION):
+ # Development versions are acceptable
if "dev0" not in version:
- # Development versions are acceptable
- self.ipyclient.show_kernel_error(
+ self.append_html_message(
ERROR_SPYDER_KERNEL_VERSION.format(
pyexec,
version,
@@ -426,13 +404,14 @@ def check_spyder_kernel_callback(self, reply):
SPYDER_KERNELS_MAX_VERSION,
SPYDER_KERNELS_CONDA,
SPYDER_KERNELS_PIP
- )
+ ),
+ before_prompt=True
)
+ self.kernel_handler.known_spyder_kernel = False
return
- if not self.is_spyder_kernel:
- self.is_spyder_kernel = True
- self.sig_is_spykernel.emit(self)
+ self.kernel_handler.known_spyder_kernel = True
+ self.setup_spyder_kernel()
def set_cwd(self, dirname, emit_cwd_change=False):
"""
@@ -753,10 +732,9 @@ def _perform_reset(self, message):
if kernel_env.get('SPY_RUN_CYTHON') == 'True':
self.silent_execute("%reload_ext Cython")
- self.sig_kernel_reset.emit()
-
if self.is_spyder_kernel:
self.call_kernel().close_all_mpl_figures()
+ self.send_spyder_kernel_configuration()
except AttributeError:
pass
@@ -1027,7 +1005,6 @@ def _handle_execute_reply(self, msg):
# Notify that kernel has started
exec_count = msg['content'].get('execution_count', '')
if exec_count == 0 and self._kernel_is_starting:
- self.sig_kernel_started.emit()
self.ipyclient.t0 = time.monotonic()
self._kernel_is_starting = False
@@ -1058,7 +1035,6 @@ def _handle_status(self, msg):
elif state == 'idle' and msg_type == 'shutdown_request':
# This handles restarts asked by the user
self.ipyclient.t0 = time.monotonic()
- self.sig_kernel_started.emit()
else:
super()._handle_status(msg)
diff --git a/spyder/plugins/variableexplorer/plugin.py b/spyder/plugins/variableexplorer/plugin.py
index 31ec86c63e2..71c55af92ff 100644
--- a/spyder/plugins/variableexplorer/plugin.py
+++ b/spyder/plugins/variableexplorer/plugin.py
@@ -62,13 +62,3 @@ def on_preferences_available(self):
def on_preferences_teardown(self):
preferences = self.get_plugin(Plugins.Preferences)
preferences.deregister_plugin_preferences(self)
-
- # ---- Public API
- # ------------------------------------------------------------------------
- def on_connection_to_external_spyder_kernel(self, shellwidget):
- """Send namespace view settings to the kernel."""
- widget = self.get_widget_for_shellwidget(shellwidget)
- if widget is None:
- return
- widget.set_namespace_view_settings()
- widget.refresh_namespacebrowser()
diff --git a/spyder/plugins/variableexplorer/widgets/main_widget.py b/spyder/plugins/variableexplorer/widgets/main_widget.py
index a7407afeba2..4b63418f26d 100644
--- a/spyder/plugins/variableexplorer/widgets/main_widget.py
+++ b/spyder/plugins/variableexplorer/widgets/main_widget.py
@@ -412,44 +412,29 @@ def switch_widget(self, nsb, old_nsb):
def create_new_widget(self, shellwidget):
"""Create new NamespaceBrowser."""
nsb = NamespaceBrowser(self)
- nsb.sig_hide_finder_requested.connect(
- self.hide_finder)
- nsb.sig_free_memory_requested.connect(
- self.free_memory)
- nsb.sig_start_spinner_requested.connect(
- self.start_spinner)
- nsb.sig_stop_spinner_requested.connect(
- self.stop_spinner)
+ nsb.sig_hide_finder_requested.connect(self.hide_finder)
+ nsb.sig_free_memory_requested.connect(self.free_memory)
+ nsb.sig_start_spinner_requested.connect(self.start_spinner)
+ nsb.sig_stop_spinner_requested.connect(self.stop_spinner)
nsb.set_shellwidget(shellwidget)
nsb.setup()
self._set_actions_and_menus(nsb)
# To update the Variable Explorer after execution
- shellwidget.executed.connect(
- nsb.refresh_namespacebrowser)
- shellwidget.sig_kernel_started.connect(
- nsb.on_kernel_started)
- shellwidget.sig_kernel_reset.connect(
- nsb.on_kernel_started)
-
+ shellwidget.executed.connect(nsb.refresh_namespacebrowser)
+ shellwidget.sig_config_kernel_requested.connect(nsb.setup_kernel)
return nsb
def close_widget(self, nsb):
"""Close NamespaceBrowser."""
- nsb.sig_hide_finder_requested.disconnect(
- self.hide_finder)
- nsb.sig_free_memory_requested.disconnect(
- self.free_memory)
- nsb.sig_start_spinner_requested.disconnect(
- self.start_spinner)
- nsb.sig_stop_spinner_requested.disconnect(
- self.stop_spinner)
- nsb.shellwidget.executed.disconnect(
- nsb.refresh_namespacebrowser)
- nsb.shellwidget.sig_kernel_started.disconnect(
- nsb.on_kernel_started)
- nsb.shellwidget.sig_kernel_reset.disconnect(
- nsb.on_kernel_started)
+ nsb.sig_hide_finder_requested.disconnect(self.hide_finder)
+ nsb.sig_free_memory_requested.disconnect(self.free_memory)
+ nsb.sig_start_spinner_requested.disconnect(self.start_spinner)
+ nsb.sig_stop_spinner_requested.disconnect(self.stop_spinner)
+ nsb.shellwidget.executed.disconnect(nsb.refresh_namespacebrowser)
+ nsb.shellwidget.sig_config_kernel_requested.disconnect(
+ nsb.setup_kernel)
+
nsb.close()
nsb.setParent(None)
diff --git a/spyder/plugins/variableexplorer/widgets/namespacebrowser.py b/spyder/plugins/variableexplorer/widgets/namespacebrowser.py
index 19437185668..274338de046 100644
--- a/spyder/plugins/variableexplorer/widgets/namespacebrowser.py
+++ b/spyder/plugins/variableexplorer/widgets/namespacebrowser.py
@@ -161,7 +161,7 @@ def refresh_table(self):
except TypeError:
pass
- def refresh_namespacebrowser(self, interrupt=True):
+ def refresh_namespacebrowser(self, *, interrupt=True):
"""Refresh namespace browser"""
self.shellwidget.call_kernel(
interrupt=interrupt,
@@ -173,15 +173,15 @@ def refresh_namespacebrowser(self, interrupt=True):
callback=self.set_var_properties
).get_var_properties()
- def set_namespace_view_settings(self):
+ def set_namespace_view_settings(self, interrupt=True):
"""Set the namespace view settings"""
settings = self.get_view_settings()
self.shellwidget.call_kernel(
- interrupt=True
+ interrupt=interrupt
).set_namespace_view_settings(settings)
- def on_kernel_started(self):
- self.set_namespace_view_settings()
+ def setup_kernel(self):
+ self.set_namespace_view_settings(interrupt=False)
self.refresh_namespacebrowser(interrupt=False)
def process_remote_view(self, remote_view):