From ac27a17a64d9f0c2c53d39796242b5e057e4c038 Mon Sep 17 00:00:00 2001 From: Gleb Fedorov Date: Mon, 28 Jan 2019 18:07:34 +0300 Subject: [PATCH 1/5] MNT: fix interrupt handling in Qt event loop signal.set_wakeup_fd() used instead, start_event_loop() is interruptible now, allow_interrupt context manager, do not override None, SIG_DFL, SIG_IGN --- lib/matplotlib/backends/backend_qt.py | 19 ++++--- lib/matplotlib/backends/qt_compat.py | 58 ++++++++++++++++++-- lib/matplotlib/tests/test_backend_qt.py | 70 ++++++++++++++++++++----- 3 files changed, 123 insertions(+), 24 deletions(-) diff --git a/lib/matplotlib/backends/backend_qt.py b/lib/matplotlib/backends/backend_qt.py index 97a27a31cb27..53841d24ac31 100644 --- a/lib/matplotlib/backends/backend_qt.py +++ b/lib/matplotlib/backends/backend_qt.py @@ -17,8 +17,10 @@ QtCore, QtGui, QtWidgets, __version__, QT_API, _enum, _to_int, _devicePixelRatioF, _isdeleted, _setDevicePixelRatio, + _allow_interrupt ) + backend_version = __version__ # SPECIAL_KEYS are Qt::Key that do *not* return their unicode name @@ -399,7 +401,13 @@ def start_event_loop(self, timeout=0): if timeout > 0: timer = QtCore.QTimer.singleShot(int(timeout * 1000), event_loop.quit) - qt_compat._exec(event_loop) + + old_sigint_handler = signal.getsignal(signal.SIGINT) + try: + with _allow_interrupt(qApp, old_sigint_handler): + qt_compat._exec(event_loop) + except ValueError: + qt_compat._exec(event_loop) def stop_event_loop(self, event=None): # docstring inherited @@ -1005,10 +1013,7 @@ def mainloop(): # allow SIGINT exceptions to close the plot window. is_python_signal_handler = old_signal is not None if is_python_signal_handler: - signal.signal(signal.SIGINT, signal.SIG_DFL) - try: + with _allow_interrupt(qApp, old_signal): + qt_compat._exec(qApp) + else: qt_compat._exec(qApp) - finally: - # reset the SIGINT exception handler - if is_python_signal_handler: - signal.signal(signal.SIGINT, old_signal) diff --git a/lib/matplotlib/backends/qt_compat.py b/lib/matplotlib/backends/qt_compat.py index d90d63fe3473..60f6c49d18ca 100644 --- a/lib/matplotlib/backends/qt_compat.py +++ b/lib/matplotlib/backends/qt_compat.py @@ -16,6 +16,8 @@ import os import platform import sys +import signal +import socket from packaging.version import parse as parse_version @@ -74,22 +76,22 @@ def _setup_pyqt5plus(): - global QtCore, QtGui, QtWidgets, __version__, is_pyqt5, \ + global QtCore, QtGui, QtWidgets, QtNetwork, __version__, is_pyqt5, \ _isdeleted, _getSaveFileName if QT_API == QT_API_PYQT6: - from PyQt6 import QtCore, QtGui, QtWidgets, sip + from PyQt6 import QtCore, QtGui, QtWidgets, QtNetwork, sip __version__ = QtCore.PYQT_VERSION_STR QtCore.Signal = QtCore.pyqtSignal QtCore.Slot = QtCore.pyqtSlot QtCore.Property = QtCore.pyqtProperty _isdeleted = sip.isdeleted elif QT_API == QT_API_PYSIDE6: - from PySide6 import QtCore, QtGui, QtWidgets, __version__ + from PySide6 import QtCore, QtGui, QtWidgets, QtNetwork, __version__ import shiboken6 def _isdeleted(obj): return not shiboken6.isValid(obj) elif QT_API == QT_API_PYQT5: - from PyQt5 import QtCore, QtGui, QtWidgets + from PyQt5 import QtCore, QtGui, QtWidgets, QtNetwork import sip __version__ = QtCore.PYQT_VERSION_STR QtCore.Signal = QtCore.pyqtSignal @@ -97,7 +99,7 @@ def _isdeleted(obj): return not shiboken6.isValid(obj) QtCore.Property = QtCore.pyqtProperty _isdeleted = sip.isdeleted elif QT_API == QT_API_PYSIDE2: - from PySide2 import QtCore, QtGui, QtWidgets, __version__ + from PySide2 import QtCore, QtGui, QtWidgets, QtNetwork, __version__ import shiboken2 def _isdeleted(obj): return not shiboken2.isValid(obj) else: @@ -191,3 +193,49 @@ def _setDevicePixelRatio(obj, val): if hasattr(obj, 'setDevicePixelRatio'): # Not available on Qt4 or some older Qt5. obj.setDevicePixelRatio(val) + + +class _allow_interrupt: + def __init__(self, qApp, old_sigint_handler): + self.interrupted_qobject = qApp + self.old_fd = None + if old_sigint_handler in (None, signal.SIG_IGN, signal.SIG_DFL): + raise ValueError(f"Old SIGINT handler {old_sigint_handler}" + f" will not be overridden") + self.old_sigint_handler = old_sigint_handler + self.caught_args = None + + QAS = QtNetwork.QAbstractSocket + self.qt_socket = QAS(QAS.TcpSocket, qApp) + # Create a socket pair + self.wsock, self.rsock = socket.socketpair() + # Let Qt listen on the one end + self.qt_socket.setSocketDescriptor(self.rsock.fileno()) + self.wsock.setblocking(False) + self.qt_socket.readyRead.connect(self._readSignal) + + def __enter__(self): + signal.signal(signal.SIGINT, self._handle) + # And let Python write on the other end + self.old_fd = signal.set_wakeup_fd(self.wsock.fileno()) + + def __exit__(self, type, val, traceback): + signal.set_wakeup_fd(self.old_fd) + signal.signal(signal.SIGINT, self.old_sigint_handler) + + self.wsock.close() + self.rsock.close() + self.qt_socket.abort() + if self.caught_args is not None: + self.old_sigint_handler(*self.caught_args) + + def _readSignal(self): + # Read the written byte. + # Note: readyRead is blocked from + # occurring again until readData() was called, so call it, + # even if you don't need the value. + self.qt_socket.readData(1) + + def _handle(self, *args): + self.caught_args = args + self.interrupted_qobject.quit() diff --git a/lib/matplotlib/tests/test_backend_qt.py b/lib/matplotlib/tests/test_backend_qt.py index 044e720c3e76..69da6e9638ce 100644 --- a/lib/matplotlib/tests/test_backend_qt.py +++ b/lib/matplotlib/tests/test_backend_qt.py @@ -33,8 +33,22 @@ def qt_core(request): return QtCore +@pytest.fixture +def platform_simulate_ctrl_c(request): + import signal + from functools import partial + + if hasattr(signal, "CTRL_C_EVENT"): + from win32api import GenerateConsoleCtrlEvent + return partial(GenerateConsoleCtrlEvent, 0, 0) + else: + # we're not on windows + return partial(os.kill, os.getpid(), signal.SIGINT) + + @pytest.mark.backend('QtAgg', skip_on_importerror=True) def test_fig_close(): + # save the state of Gcf.figs init_figs = copy.copy(Gcf.figs) @@ -51,18 +65,39 @@ def test_fig_close(): @pytest.mark.backend('QtAgg', skip_on_importerror=True) -def test_fig_signals(qt_core): +@pytest.mark.parametrize("target, kwargs", [ + (plt.show, {"block": True}), + (plt.pause, {"interval": 10}) +]) +def test_sigint(qt_core, platform_simulate_ctrl_c, target, + kwargs): + plt.figure() + def fire_signal(): + platform_simulate_ctrl_c() + + qt_core.QTimer.singleShot(100, fire_signal) + try: + target(**kwargs) + except KeyboardInterrupt as e: + assert True + else: + assert False # KeyboardInterrupt must be raised + + +@pytest.mark.backend('QtAgg', skip_on_importerror=True) +def test_fig_sigint_override(qt_core): + from matplotlib.backends.backend_qt5 import _BackendQT5 # Create a figure plt.figure() - # Access signals - event_loop_signal = None + # Variable to access the handler from the inside of the event loop + event_loop_handler = None # Callback to fire during event loop: save SIGINT handler, then exit def fire_signal_and_quit(): # Save event loop signal - nonlocal event_loop_signal - event_loop_signal = signal.getsignal(signal.SIGINT) + nonlocal event_loop_handler + event_loop_handler = signal.getsignal(signal.SIGINT) # Request event loop exit qt_core.QCoreApplication.exit() @@ -71,26 +106,37 @@ def fire_signal_and_quit(): qt_core.QTimer.singleShot(0, fire_signal_and_quit) # Save original SIGINT handler - original_signal = signal.getsignal(signal.SIGINT) + original_handler = signal.getsignal(signal.SIGINT) # Use our own SIGINT handler to be 100% sure this is working - def CustomHandler(signum, frame): + def custom_handler(signum, frame): pass - signal.signal(signal.SIGINT, CustomHandler) + signal.signal(signal.SIGINT, custom_handler) # mainloop() sets SIGINT, starts Qt event loop (which triggers timer and # exits) and then mainloop() resets SIGINT matplotlib.backends.backend_qt._BackendQT.mainloop() - # Assert: signal handler during loop execution is signal.SIG_DFL - assert event_loop_signal == signal.SIG_DFL + # Assert: signal handler during loop execution is changed + # (can't test equality with func) + assert event_loop_handler != custom_handler # Assert: current signal handler is the same as the one we set before - assert CustomHandler == signal.getsignal(signal.SIGINT) + assert signal.getsignal(signal.SIGINT) == custom_handler + + # Repeat again to test that SIG_DFL and SIG_IGN will not be overridden + for custom_handler in (signal.SIG_DFL, signal.SIG_IGN): + qt_core.QTimer.singleShot(0, fire_signal_and_quit) + signal.signal(signal.SIGINT, custom_handler) + + _BackendQT5.mainloop() + + assert event_loop_handler == custom_handler + assert signal.getsignal(signal.SIGINT) == custom_handler # Reset SIGINT handler to what it was before the test - signal.signal(signal.SIGINT, original_signal) + signal.signal(signal.SIGINT, original_handler) @pytest.mark.parametrize( From b384c82284fe0b1a9120fc948f1e6206ec229394 Mon Sep 17 00:00:00 2001 From: Gleb Fedorov Date: Tue, 11 May 2021 16:55:36 +0300 Subject: [PATCH 2/5] Using contextlib --- lib/matplotlib/backends/backend_qt.py | 16 +-- lib/matplotlib/backends/qt_compat.py | 163 +++++++++++++++++------- lib/matplotlib/tests/test_backend_qt.py | 31 +++++ 3 files changed, 150 insertions(+), 60 deletions(-) diff --git a/lib/matplotlib/backends/backend_qt.py b/lib/matplotlib/backends/backend_qt.py index 53841d24ac31..14481708dfb8 100644 --- a/lib/matplotlib/backends/backend_qt.py +++ b/lib/matplotlib/backends/backend_qt.py @@ -17,7 +17,7 @@ QtCore, QtGui, QtWidgets, __version__, QT_API, _enum, _to_int, _devicePixelRatioF, _isdeleted, _setDevicePixelRatio, - _allow_interrupt + _maybe_allow_interrupt ) @@ -402,11 +402,7 @@ def start_event_loop(self, timeout=0): timer = QtCore.QTimer.singleShot(int(timeout * 1000), event_loop.quit) - old_sigint_handler = signal.getsignal(signal.SIGINT) - try: - with _allow_interrupt(qApp, old_sigint_handler): - qt_compat._exec(event_loop) - except ValueError: + with _maybe_allow_interrupt(qApp): qt_compat._exec(event_loop) def stop_event_loop(self, event=None): @@ -1009,11 +1005,5 @@ class _BackendQT(_Backend): @staticmethod def mainloop(): - old_signal = signal.getsignal(signal.SIGINT) - # allow SIGINT exceptions to close the plot window. - is_python_signal_handler = old_signal is not None - if is_python_signal_handler: - with _allow_interrupt(qApp, old_signal): - qt_compat._exec(qApp) - else: + with _maybe_allow_interrupt(qApp): qt_compat._exec(qApp) diff --git a/lib/matplotlib/backends/qt_compat.py b/lib/matplotlib/backends/qt_compat.py index 60f6c49d18ca..5ed72f8d6801 100644 --- a/lib/matplotlib/backends/qt_compat.py +++ b/lib/matplotlib/backends/qt_compat.py @@ -30,7 +30,7 @@ QT_API_PYSIDE2 = "PySide2" QT_API_PYQTv2 = "PyQt4v2" QT_API_PYSIDE = "PySide" -QT_API_PYQT = "PyQt4" # Use the old sip v1 API (Py3 defaults to v2). +QT_API_PYQT = "PyQt4" # Use the old sip v1 API (Py3 defaults to v2). QT_API_ENV = os.environ.get("QT_API") if QT_API_ENV is not None: QT_API_ENV = QT_API_ENV.lower() @@ -101,7 +101,8 @@ def _isdeleted(obj): return not shiboken6.isValid(obj) elif QT_API == QT_API_PYSIDE2: from PySide2 import QtCore, QtGui, QtWidgets, QtNetwork, __version__ import shiboken2 - def _isdeleted(obj): return not shiboken2.isValid(obj) + def _isdeleted(obj): + return not shiboken2.isValid(obj) else: raise AssertionError(f"Unexpected QT_API: {QT_API}") _getSaveFileName = QtWidgets.QFileDialog.getSaveFileName @@ -136,7 +137,6 @@ def _isdeleted(obj): return not shiboken2.isValid(obj) "QT_MAC_WANTS_LAYER" not in os.environ): os.environ["QT_MAC_WANTS_LAYER"] = "1" - # These globals are only defined for backcompatibility purposes. ETS = dict(pyqt5=(QT_API_PYQT5, 5), pyside2=(QT_API_PYSIDE2, 5)) @@ -195,47 +195,116 @@ def _setDevicePixelRatio(obj, val): obj.setDevicePixelRatio(val) -class _allow_interrupt: - def __init__(self, qApp, old_sigint_handler): - self.interrupted_qobject = qApp - self.old_fd = None - if old_sigint_handler in (None, signal.SIG_IGN, signal.SIG_DFL): - raise ValueError(f"Old SIGINT handler {old_sigint_handler}" - f" will not be overridden") - self.old_sigint_handler = old_sigint_handler - self.caught_args = None - - QAS = QtNetwork.QAbstractSocket - self.qt_socket = QAS(QAS.TcpSocket, qApp) - # Create a socket pair - self.wsock, self.rsock = socket.socketpair() - # Let Qt listen on the one end - self.qt_socket.setSocketDescriptor(self.rsock.fileno()) - self.wsock.setblocking(False) - self.qt_socket.readyRead.connect(self._readSignal) - - def __enter__(self): - signal.signal(signal.SIGINT, self._handle) - # And let Python write on the other end - self.old_fd = signal.set_wakeup_fd(self.wsock.fileno()) - - def __exit__(self, type, val, traceback): - signal.set_wakeup_fd(self.old_fd) - signal.signal(signal.SIGINT, self.old_sigint_handler) - - self.wsock.close() - self.rsock.close() - self.qt_socket.abort() - if self.caught_args is not None: - self.old_sigint_handler(*self.caught_args) - - def _readSignal(self): - # Read the written byte. - # Note: readyRead is blocked from - # occurring again until readData() was called, so call it, - # even if you don't need the value. - self.qt_socket.readData(1) - - def _handle(self, *args): - self.caught_args = args - self.interrupted_qobject.quit() +@contextlib.contextmanager +def _maybe_allow_interrupt(qapp): + ''' + This manager allows to terminate a plot by sending a SIGINT. It is + necessary because the running Qt backend prevents Python interpreter to + run and process signals (i.e., to raise KeyboardInterrupt exception). To + solve this one needs to somehow wake up the interpreter and make it close + the plot window. We do this by using the signal.set_wakeup_fd() function + which organizes a write of the signal number into a socketpair connected + to the QSocketNotifier (since it is part of the Qt backend, it can react + to that write event). Afterwards, the Qt handler empties the socketpair + by a recv() command to re-arm it (we need this if a signal different from + SIGINT was caught by set_wakeup_fd() and we shall continue waiting). If + the SIGINT was caught indeed, after exiting the on_signal() function the + interpreter reacts to the SIGINT according to the handle() function which + had been set up by a signal.signal() call: it causes the qt_object to + exit by calling its quit() method. Finally, we call the old SIGINT + handler with the same arguments that were given to our custom handle() + handler. + + We do this only if the old handler for SIGINT was not None, which means + that a non-python handler was installed, i.e. in Julia, and not SIG_IGN + which means we should ignore the interrupts. + ''' + old_sigint_handler = signal.getsignal(signal.SIGINT) + handler_args = None + skip = False + if old_sigint_handler in (None, signal.SIG_IGN, signal.SIG_DFL): + skip = True + else: + wsock, rsock = socket.socketpair() + wsock.setblocking(False) + old_wakeup_fd = signal.set_wakeup_fd(wsock.fileno()) + sn = QtCore.QSocketNotifier(rsock.fileno(), + QtCore.QSocketNotifier.Read) + + @sn.activated.connect + def on_signal(*args): + rsock.recv( + sys.getsizeof(int)) # clear the socket to re-arm the notifier + + def handle(*args): + nonlocal handler_args + handler_args = args + qapp.quit() + + signal.signal(signal.SIGINT, handle) + try: + yield + finally: + if not skip: + wsock.close() + rsock.close() + sn.setEnabled(False) + signal.set_wakeup_fd(old_wakeup_fd) + signal.signal(signal.SIGINT, old_sigint_handler) + if handler_args is not None: + old_sigint_handler(handler_args) + +# class _maybe_allow_interrupt: +# +# def __init__(self, qt_object): +# self.interrupted_qobject = qt_object +# self.old_fd = None +# self.caught_args = None +# +# self.skip = False +# self.old_sigint_handler = signal.getsignal(signal.SIGINT) +# if self.old_sigint_handler in (None, signal.SIG_IGN, signal.SIG_DFL): +# self.skip = True +# return +# +# QAS = QtNetwork.QAbstractSocket +# self.qt_socket = QAS(QAS.TcpSocket, qt_object) +# # Create a socket pair +# self.wsock, self.rsock = socket.socketpair() +# # Let Qt listen on the one end +# self.qt_socket.setSocketDescriptor(self.rsock.fileno()) +# self.wsock.setblocking(False) +# self.qt_socket.readyRead.connect(self._readSignal) +# +# def __enter__(self): +# if self.skip: +# return +# +# signal.signal(signal.SIGINT, self._handle) +# # And let Python write on the other end +# self.old_fd = signal.set_wakeup_fd(self.wsock.fileno()) +# +# def __exit__(self, type, val, traceback): +# if self.skip: +# return +# +# signal.set_wakeup_fd(self.old_fd) +# signal.signal(signal.SIGINT, self.old_sigint_handler) +# +# self.wsock.close() +# self.rsock.close() +# self.qt_socket.abort() +# if self.caught_args is not None: +# self.old_sigint_handler(*self.caught_args) +# +# def _readSignal(self): +# # Read the written byte to re-arm the socket if a signal different +# # from SIGINT was caught. +# # Since a custom handler was installed for SIGINT, KeyboardInterrupt +# # is not raised here. +# self.qt_socket.readData(1) +# +# def _handle(self, *args): +# self.caught_args = args # save the caught args to call the old +# # SIGINT handler afterwards +# self.interrupted_qobject.quit() diff --git a/lib/matplotlib/tests/test_backend_qt.py b/lib/matplotlib/tests/test_backend_qt.py index 69da6e9638ce..4342a2723dfa 100644 --- a/lib/matplotlib/tests/test_backend_qt.py +++ b/lib/matplotlib/tests/test_backend_qt.py @@ -85,6 +85,37 @@ def fire_signal(): @pytest.mark.backend('QtAgg', skip_on_importerror=True) +@pytest.mark.parametrize("target, kwargs", [ + (plt.show, {"block": True}), + (plt.pause, {"interval": 10}) +]) +def test_other_signal_before_sigint(qt_core, platform_simulate_ctrl_c, + target, kwargs): + plt.figure() + + sigpipe_caught = False + def custom_sigpipe_handler(signum, frame): + nonlocal sigpipe_caught + sigpipe_caught = True + signal.signal(signal.SIGPIPE, custom_sigpipe_handler) + + def fire_other_signal(): + os.kill(os.getpid(), signal.SIGPIPE) + + def fire_sigint(): + platform_simulate_ctrl_c() + + qt_core.QTimer.singleShot(50, fire_other_signal) + qt_core.QTimer.singleShot(100, fire_sigint) + try: + target(**kwargs) + except KeyboardInterrupt as e: + assert sigpipe_caught + else: + assert False # KeyboardInterrupt must be raised + + +@pytest.mark.backend('Qt5Agg') def test_fig_sigint_override(qt_core): from matplotlib.backends.backend_qt5 import _BackendQT5 # Create a figure From 1a1f67f99d746446b64b15e583d0ad565d9d5177 Mon Sep 17 00:00:00 2001 From: Gleb Fedorov Date: Wed, 12 May 2021 12:18:34 +0300 Subject: [PATCH 3/5] Change SIGPIPE to SIGCHLD --- lib/matplotlib/backends/qt_compat.py | 4 ++-- lib/matplotlib/tests/test_backend_qt.py | 12 ++++++------ 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/lib/matplotlib/backends/qt_compat.py b/lib/matplotlib/backends/qt_compat.py index 5ed72f8d6801..28cb5724a26c 100644 --- a/lib/matplotlib/backends/qt_compat.py +++ b/lib/matplotlib/backends/qt_compat.py @@ -197,7 +197,7 @@ def _setDevicePixelRatio(obj, val): @contextlib.contextmanager def _maybe_allow_interrupt(qapp): - ''' + """ This manager allows to terminate a plot by sending a SIGINT. It is necessary because the running Qt backend prevents Python interpreter to run and process signals (i.e., to raise KeyboardInterrupt exception). To @@ -218,7 +218,7 @@ def _maybe_allow_interrupt(qapp): We do this only if the old handler for SIGINT was not None, which means that a non-python handler was installed, i.e. in Julia, and not SIG_IGN which means we should ignore the interrupts. - ''' + """ old_sigint_handler = signal.getsignal(signal.SIGINT) handler_args = None skip = False diff --git a/lib/matplotlib/tests/test_backend_qt.py b/lib/matplotlib/tests/test_backend_qt.py index 4342a2723dfa..5ce676dee002 100644 --- a/lib/matplotlib/tests/test_backend_qt.py +++ b/lib/matplotlib/tests/test_backend_qt.py @@ -93,14 +93,14 @@ def test_other_signal_before_sigint(qt_core, platform_simulate_ctrl_c, target, kwargs): plt.figure() - sigpipe_caught = False + sigcld_caught = False def custom_sigpipe_handler(signum, frame): - nonlocal sigpipe_caught - sigpipe_caught = True - signal.signal(signal.SIGPIPE, custom_sigpipe_handler) + nonlocal sigcld_caught + sigcld_caught = True + signal.signal(signal.SIGCLD, custom_sigpipe_handler) def fire_other_signal(): - os.kill(os.getpid(), signal.SIGPIPE) + os.kill(os.getpid(), signal.SIGCLD) def fire_sigint(): platform_simulate_ctrl_c() @@ -110,7 +110,7 @@ def fire_sigint(): try: target(**kwargs) except KeyboardInterrupt as e: - assert sigpipe_caught + assert sigcld_caught else: assert False # KeyboardInterrupt must be raised From 9070ade587f42bd8d64eb17ca631d0a7cfbc1546 Mon Sep 17 00:00:00 2001 From: Thomas A Caswell Date: Fri, 6 Aug 2021 20:10:33 -0400 Subject: [PATCH 4/5] MNT: fix rebase --- lib/matplotlib/backends/backend_qt.py | 2 +- lib/matplotlib/backends/qt_compat.py | 63 ++----------------------- lib/matplotlib/tests/test_backend_qt.py | 15 ++---- 3 files changed, 11 insertions(+), 69 deletions(-) diff --git a/lib/matplotlib/backends/backend_qt.py b/lib/matplotlib/backends/backend_qt.py index 14481708dfb8..bfbdf62fd27b 100644 --- a/lib/matplotlib/backends/backend_qt.py +++ b/lib/matplotlib/backends/backend_qt.py @@ -402,7 +402,7 @@ def start_event_loop(self, timeout=0): timer = QtCore.QTimer.singleShot(int(timeout * 1000), event_loop.quit) - with _maybe_allow_interrupt(qApp): + with _maybe_allow_interrupt(event_loop): qt_compat._exec(event_loop) def stop_event_loop(self, event=None): diff --git a/lib/matplotlib/backends/qt_compat.py b/lib/matplotlib/backends/qt_compat.py index 28cb5724a26c..87a330f03b69 100644 --- a/lib/matplotlib/backends/qt_compat.py +++ b/lib/matplotlib/backends/qt_compat.py @@ -18,6 +18,7 @@ import sys import signal import socket +import contextlib from packaging.version import parse as parse_version @@ -228,8 +229,9 @@ def _maybe_allow_interrupt(qapp): wsock, rsock = socket.socketpair() wsock.setblocking(False) old_wakeup_fd = signal.set_wakeup_fd(wsock.fileno()) - sn = QtCore.QSocketNotifier(rsock.fileno(), - QtCore.QSocketNotifier.Read) + sn = QtCore.QSocketNotifier( + rsock.fileno(), _enum('QtCore.QSocketNotifier.Type').Read + ) @sn.activated.connect def on_signal(*args): @@ -252,59 +254,4 @@ def handle(*args): signal.set_wakeup_fd(old_wakeup_fd) signal.signal(signal.SIGINT, old_sigint_handler) if handler_args is not None: - old_sigint_handler(handler_args) - -# class _maybe_allow_interrupt: -# -# def __init__(self, qt_object): -# self.interrupted_qobject = qt_object -# self.old_fd = None -# self.caught_args = None -# -# self.skip = False -# self.old_sigint_handler = signal.getsignal(signal.SIGINT) -# if self.old_sigint_handler in (None, signal.SIG_IGN, signal.SIG_DFL): -# self.skip = True -# return -# -# QAS = QtNetwork.QAbstractSocket -# self.qt_socket = QAS(QAS.TcpSocket, qt_object) -# # Create a socket pair -# self.wsock, self.rsock = socket.socketpair() -# # Let Qt listen on the one end -# self.qt_socket.setSocketDescriptor(self.rsock.fileno()) -# self.wsock.setblocking(False) -# self.qt_socket.readyRead.connect(self._readSignal) -# -# def __enter__(self): -# if self.skip: -# return -# -# signal.signal(signal.SIGINT, self._handle) -# # And let Python write on the other end -# self.old_fd = signal.set_wakeup_fd(self.wsock.fileno()) -# -# def __exit__(self, type, val, traceback): -# if self.skip: -# return -# -# signal.set_wakeup_fd(self.old_fd) -# signal.signal(signal.SIGINT, self.old_sigint_handler) -# -# self.wsock.close() -# self.rsock.close() -# self.qt_socket.abort() -# if self.caught_args is not None: -# self.old_sigint_handler(*self.caught_args) -# -# def _readSignal(self): -# # Read the written byte to re-arm the socket if a signal different -# # from SIGINT was caught. -# # Since a custom handler was installed for SIGINT, KeyboardInterrupt -# # is not raised here. -# self.qt_socket.readData(1) -# -# def _handle(self, *args): -# self.caught_args = args # save the caught args to call the old -# # SIGINT handler afterwards -# self.interrupted_qobject.quit() + old_sigint_handler(*handler_args) diff --git a/lib/matplotlib/tests/test_backend_qt.py b/lib/matplotlib/tests/test_backend_qt.py index 5ce676dee002..06e1b0c35d08 100644 --- a/lib/matplotlib/tests/test_backend_qt.py +++ b/lib/matplotlib/tests/test_backend_qt.py @@ -76,12 +76,8 @@ def fire_signal(): platform_simulate_ctrl_c() qt_core.QTimer.singleShot(100, fire_signal) - try: + with pytest.raises(KeyboardInterrupt): target(**kwargs) - except KeyboardInterrupt as e: - assert True - else: - assert False # KeyboardInterrupt must be raised @pytest.mark.backend('QtAgg', skip_on_importerror=True) @@ -107,12 +103,11 @@ def fire_sigint(): qt_core.QTimer.singleShot(50, fire_other_signal) qt_core.QTimer.singleShot(100, fire_sigint) - try: + + with pytest.raises(KeyboardInterrupt): target(**kwargs) - except KeyboardInterrupt as e: - assert sigcld_caught - else: - assert False # KeyboardInterrupt must be raised + + assert sigcld_caught @pytest.mark.backend('Qt5Agg') From c3772ae037c5a2c7614013a9c36954675c5d9de0 Mon Sep 17 00:00:00 2001 From: Thomas A Caswell Date: Sat, 21 Aug 2021 13:11:09 -0400 Subject: [PATCH 5/5] Simplify rearming wakup socket Co-authored-by: Elliott Sales de Andrade --- lib/matplotlib/backends/qt_compat.py | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/lib/matplotlib/backends/qt_compat.py b/lib/matplotlib/backends/qt_compat.py index 87a330f03b69..b7db788d2bc4 100644 --- a/lib/matplotlib/backends/qt_compat.py +++ b/lib/matplotlib/backends/qt_compat.py @@ -233,10 +233,8 @@ def _maybe_allow_interrupt(qapp): rsock.fileno(), _enum('QtCore.QSocketNotifier.Type').Read ) - @sn.activated.connect - def on_signal(*args): - rsock.recv( - sys.getsizeof(int)) # clear the socket to re-arm the notifier + # Clear the socket to re-arm the notifier. + sn.activated.connect(lambda *args: rsock.recv(1)) def handle(*args): nonlocal handler_args