diff --git a/.github/scripts/install.sh b/.github/scripts/install.sh index 66a0e7e3cac..934462d19b1 100755 --- a/.github/scripts/install.sh +++ b/.github/scripts/install.sh @@ -23,8 +23,8 @@ if [ "$USE_CONDA" = "true" ]; then # To check our manifest and coverage micromamba install check-manifest -c conda-forge codecov -q -y - # Install PyZMQ 24 to avoid hangs - micromamba install -c conda-forge pyzmq=24 + # To test with Jupyter-client 8 + micromamba install jupyter_client=8.1 else # Update pip and setuptools python -m pip install -U pip setuptools wheel build @@ -47,8 +47,8 @@ else pip install pyqt5==5.12.* pyqtwebengine==5.12.* fi - # Install PyZMQ 24 to avoid hangs - pip install pyzmq==24.0.1 + # To test with Jupyter-client 8 + pip install jupyter-client==8.1 fi # Install subrepos from source diff --git a/external-deps/qtconsole/.gitrepo b/external-deps/qtconsole/.gitrepo index 70cb69ffb03..7d8fcdad34a 100644 --- a/external-deps/qtconsole/.gitrepo +++ b/external-deps/qtconsole/.gitrepo @@ -6,7 +6,7 @@ [subrepo] remote = https://github.com/jupyter/qtconsole.git branch = master - commit = 96604f44cdbca051712b5c51072309cba9518ae7 - parent = be96be0cfc313a34097505631502e711af94071d + commit = 729a87a3e6e8d63da22b82bcc3b7d60b6cabee29 + parent = 7e67e2e1f9930058dd7242465c434a4fdcbcc9f2 method = merge cmdver = 0.4.3 diff --git a/external-deps/qtconsole/docs/source/changelog.rst b/external-deps/qtconsole/docs/source/changelog.rst index f5e5fc4af7d..cc659645799 100644 --- a/external-deps/qtconsole/docs/source/changelog.rst +++ b/external-deps/qtconsole/docs/source/changelog.rst @@ -8,6 +8,14 @@ Changes in Jupyter Qt console 5.4 ~~~ +5.4.1 +----- + +`5.4.1 on GitHub `__ + +* Fix crash at startup with PySide6. +* Cast images width and height to int when trying to insert them. + 5.4.0 ----- diff --git a/external-deps/qtconsole/qtconsole/client.py b/external-deps/qtconsole/qtconsole/client.py index 58b0cb3fb2b..9306e859ea0 100644 --- a/external-deps/qtconsole/qtconsole/client.py +++ b/external-deps/qtconsole/qtconsole/client.py @@ -1,28 +1,17 @@ """ Defines a KernelClient that provides signals and slots. """ -import atexit -import errno -from threading import Thread -import time -import zmq -# import ZMQError in top-level namespace, to avoid ugly attribute-error messages -# during garbage collection of threads at exit: -from zmq import ZMQError -from zmq.eventloop import ioloop, zmqstream - -from qtpy import QtCore - -# Local imports -from traitlets import Type, Instance +# Third-party imports from jupyter_client.channels import HBChannel -from jupyter_client import KernelClient -from jupyter_client.channels import InvalidPortNumber from jupyter_client.threaded import ThreadedKernelClient, ThreadedZMQSocketChannel +from qtpy import QtCore +from traitlets import Type +# Local imports from .kernel_mixins import QtKernelClientMixin from .util import SuperQObject + class QtHBChannel(SuperQObject, HBChannel): # A longer timeout than the base class time_to_dead = 3.0 @@ -36,11 +25,8 @@ def call_handlers(self, since_last_heartbeat): # Emit the generic signal. self.kernel_died.emit(since_last_heartbeat) -from jupyter_client import protocol_version_info - -major_protocol_version = protocol_version_info[0] -class QtZMQSocketChannel(ThreadedZMQSocketChannel,SuperQObject): +class QtZMQSocketChannel(ThreadedZMQSocketChannel, SuperQObject): """A ZMQ socket emitting a Qt signal when a message is received.""" message_received = QtCore.Signal(object) @@ -49,7 +35,6 @@ def process_events(self): """ QtCore.QCoreApplication.instance().processEvents() - def call_handlers(self, msg): """This method is called in the ioloop thread when a message arrives. @@ -64,7 +49,6 @@ def call_handlers(self, msg): class QtKernelClient(QtKernelClientMixin, ThreadedKernelClient): """ A KernelClient that provides signals and slots. """ - iopub_channel_class = Type(QtZMQSocketChannel) shell_channel_class = Type(QtZMQSocketChannel) stdin_channel_class = Type(QtZMQSocketChannel) diff --git a/external-deps/qtconsole/qtconsole/frontend_widget.py b/external-deps/qtconsole/qtconsole/frontend_widget.py index 02860bb6fd6..0978f2d383b 100644 --- a/external-deps/qtconsole/qtconsole/frontend_widget.py +++ b/external-deps/qtconsole/qtconsole/frontend_widget.py @@ -538,6 +538,12 @@ def _handle_kernel_restarted(self, died=True): """ self.log.warning("kernel restarted") self._kernel_restarted_message(died=died) + + # This resets the autorestart counter so that the kernel can be + # auto-restarted before the next time it's polled to see if it's alive. + if self.kernel_manager: + self.kernel_manager.reset_autorestart_count() + self.reset() def _handle_inspect_reply(self, rep): @@ -831,8 +837,6 @@ def _document_contents_change(self, position, removed, added): """ # Calculate where the cursor should be *after* the change: position += added - - document = self._control.document() if position == self._get_cursor().position(): self._auto_call_tip() diff --git a/external-deps/qtconsole/qtconsole/jupyter_widget.py b/external-deps/qtconsole/qtconsole/jupyter_widget.py index 1e3f33dc7ee..cc9de8770b7 100644 --- a/external-deps/qtconsole/qtconsole/jupyter_widget.py +++ b/external-deps/qtconsole/qtconsole/jupyter_widget.py @@ -7,12 +7,9 @@ # Distributed under the terms of the Modified BSD License. from collections import namedtuple -import os.path -import re from subprocess import Popen import sys import time -from textwrap import dedent from warnings import warn from qtpy import QtCore, QtGui @@ -287,7 +284,6 @@ def _handle_display_data(self, msg): if self.include_output(msg): self.flush_clearoutput() data = msg['content']['data'] - metadata = msg['content']['metadata'] # In the regular JupyterWidget, we simply print the plain text # representation. if 'text/plain' in data: diff --git a/external-deps/qtconsole/qtconsole/manager.py b/external-deps/qtconsole/qtconsole/manager.py index cb6d2c61781..1e61254794c 100644 --- a/external-deps/qtconsole/qtconsole/manager.py +++ b/external-deps/qtconsole/qtconsole/manager.py @@ -26,6 +26,9 @@ def stop(self): def poll(self): super().poll() + def reset_count(self): + self._restart_count = 0 + class QtKernelManager(KernelManager, QtKernelManagerMixin): """A KernelManager with Qt signals for restart""" @@ -62,6 +65,21 @@ def post_start_kernel(self, **kw): self.kernel_restarted.emit() self._is_restarting = False + def reset_autorestart_count(self): + """Reset autorestart count.""" + if self._restarter: + self._restarter.reset_count() + + async def _async_post_start_kernel(self, **kw): + """ + This is necessary for Jupyter-client 8+ because `start_kernel` doesn't + call `post_start_kernel` directly. + """ + await super()._async_post_start_kernel(**kw) + if self._is_restarting: + self.kernel_restarted.emit() + self._is_restarting = False + def _handle_kernel_restarting(self): """Kernel has died, and will be restarted.""" self._is_restarting = True diff --git a/external-deps/qtconsole/qtconsole/qtconsoleapp.py b/external-deps/qtconsole/qtconsole/qtconsoleapp.py index 326f0adafab..cee348d6deb 100644 --- a/external-deps/qtconsole/qtconsole/qtconsoleapp.py +++ b/external-deps/qtconsole/qtconsole/qtconsoleapp.py @@ -12,6 +12,8 @@ import sys from warnings import warn +from packaging.version import parse + # If run on Windows: # # 1. Install an exception hook which pops up a message box. @@ -416,8 +418,8 @@ def initialize(self, argv=None): # Fixes launching issues with Big Sur # https://bugreports.qt.io/browse/QTBUG-87014, fixed in qt 5.15.2 if sys.platform == 'darwin': - v_5_15_2 = QtCore.QVersionNumber.fromString('5.15.2')[0] - v_current = QtCore.QVersionNumber.fromString(QT_VERSION)[0] + v_5_15_2 = parse('5.15.2') + v_current = parse(QT_VERSION) if v_current < v_5_15_2: os.environ['QT_MAC_WANTS_LAYER'] = '1' self._init_asyncio_patch() diff --git a/external-deps/qtconsole/qtconsole/tests/test_00_console_widget.py b/external-deps/qtconsole/qtconsole/tests/test_00_console_widget.py index 40f9a0d7da9..2745be6ee9b 100644 --- a/external-deps/qtconsole/qtconsole/tests/test_00_console_widget.py +++ b/external-deps/qtconsole/qtconsole/tests/test_00_console_widget.py @@ -1,3 +1,4 @@ +import os import unittest import sys @@ -259,6 +260,49 @@ def wait_for_input(): assert output in control.toPlainText() +@flaky(max_runs=5) +@pytest.mark.skipif(os.name == 'nt', reason="no SIGTERM on Windows") +def test_restart_after_kill(qtconsole, qtbot): + """ + Test that the kernel correctly restarts after a kill. + """ + window = qtconsole.window + shell = window.active_frontend + control = shell._control + + def wait_for_restart(): + qtbot.waitUntil( + lambda: 'Kernel died, restarting' in control.toPlainText() + ) + + # Wait until the console is fully up + qtbot.waitUntil(lambda: shell._prompt_html is not None, + timeout=SHELL_TIMEOUT) + + # This checks that we are able to restart the kernel even after the number + # of consecutive auto-restarts is reached (which by default is five). + for _ in range(10): + # Clear console + with qtbot.waitSignal(shell.executed): + shell.execute('%clear') + qtbot.wait(500) + + # Run some code that kills the kernel + code = "import os, signal; os.kill(os.getpid(), signal.SIGTERM)" + shell.execute(code) + + # Check that the restart message is printed + qtbot.waitUntil( + lambda: 'Kernel died, restarting' in control.toPlainText() + ) + + # Check that a new prompt is available after the restart + qtbot.waitUntil( + lambda: control.toPlainText().splitlines()[-1] == 'In [1]: ' + ) + qtbot.wait(500) + + @pytest.mark.skipif(no_display, reason="Doesn't work without a display") class TestConsoleWidget(unittest.TestCase): diff --git a/external-deps/qtconsole/setup.py b/external-deps/qtconsole/setup.py index b858ce2d1fe..5342906a3c0 100644 --- a/external-deps/qtconsole/setup.py +++ b/external-deps/qtconsole/setup.py @@ -73,7 +73,8 @@ 'pygments', 'ipykernel>=4.1', # not a real dependency, but require the reference kernel 'qtpy>=2.0.1', - 'pyzmq>=17.1' + 'pyzmq>=17.1', + 'packaging' ], extras_require = { 'test': ['flaky', 'pytest', 'pytest-qt'],