Factor out common parts of qt and macos interrupt handling. · matplotlib/matplotlib@b23b010 · GitHub
Skip to content

Commit b23b010

Browse files
committed
Factor out common parts of qt and macos interrupt handling.
Note that we don't actually need to disable the QSocketNotifier at the end, just letting it go out of scope should be sufficient as its destructor also does that (see qsocketnotifier.cpp).
1 parent ea66786 commit b23b010

4 files changed

Lines changed: 115 additions & 137 deletions

File tree

lib/matplotlib/backends/backend_macosx.py

Lines changed: 10 additions & 49 deletions

lib/matplotlib/backends/backend_qt.py

Lines changed: 35 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -13,9 +13,7 @@
1313
import matplotlib.backends.qt_editor.figureoptions as figureoptions
1414
from . import qt_compat
1515
from .qt_compat import (
16-
QtCore, QtGui, QtWidgets, __version__, QT_API,
17-
_to_int, _isdeleted, _maybe_allow_interrupt
18-
)
16+
QtCore, QtGui, QtWidgets, __version__, QT_API, _to_int, _isdeleted)
1917

2018

2119
# SPECIAL_KEYS are Qt::Key that do *not* return their Unicode name
@@ -148,6 +146,38 @@ def _create_qApp():
148146
return app
149147

150148

149+
def _allow_interrupt_qt(qapp_or_eventloop):
150+
"""A context manager that allows terminating a plot by sending a SIGINT."""
151+
152+
# Use QSocketNotifier to read the socketpair while the Qt event loop runs.
153+
154+
def prepare_notifier(rsock):
155+
sn = QtCore.QSocketNotifier(rsock.fileno(), QtCore.QSocketNotifier.Type.Read)
156+
157+
@sn.activated.connect
158+
def _may_clear_sock():
159+
# Running a Python function on socket activation gives the interpreter a
160+
# chance to handle the signal in Python land. We also need to drain the
161+
# socket with recv() to re-arm it, because it will be written to as part of
162+
# the wakeup. (We need this in case set_wakeup_fd catches a signal other
163+
# than SIGINT and we shall continue waiting.)
164+
try:
165+
rsock.recv(1)
166+
except BlockingIOError:
167+
# This may occasionally fire too soon or more than once on Windows, so
168+
# be forgiving about reading an empty socket.
169+
pass
170+
171+
return sn # Actually keep the notifier alive.
172+
173+
def handle_sigint():
174+
if hasattr(qapp_or_eventloop, 'closeAllWindows'):
175+
qapp_or_eventloop.closeAllWindows()
176+
qapp_or_eventloop.quit()
177+
178+
return mpl.cbook._allow_interrupt(prepare_notifier, handle_sigint)
179+
180+
151181
class TimerQT(TimerBase):
152182
"""Subclass of `.TimerBase` using QTimer events."""
153183

@@ -417,7 +447,7 @@ def start_event_loop(self, timeout=0):
417447
if timeout > 0:
418448
_ = QtCore.QTimer.singleShot(int(timeout * 1000), event_loop.quit)
419449

420-
with _maybe_allow_interrupt(event_loop):
450+
with _allow_interrupt_qt(event_loop):
421451
qt_compat._exec(event_loop)
422452

423453
def stop_event_loop(self, event=None):
@@ -598,7 +628,7 @@ def resize(self, width, height):
598628
def start_main_loop(cls):
599629
qapp = QtWidgets.QApplication.instance()
600630
if qapp:
601-
with _maybe_allow_interrupt(qapp):
631+
with _allow_interrupt_qt(qapp):
602632
qt_compat._exec(qapp)
603633

604634
def show(self):

lib/matplotlib/backends/qt_compat.py

Lines changed: 0 additions & 73 deletions
Original file line numberDiff line numberDiff line change
@@ -13,9 +13,6 @@
1313
import os
1414
import platform
1515
import sys
16-
import signal
17-
import socket
18-
import contextlib
1916

2017
from packaging.version import parse as parse_version
2118

@@ -160,73 +157,3 @@ def _isdeleted(obj):
160157
def _exec(obj):
161158
# exec on PyQt6, exec_ elsewhere.
162159
obj.exec() if hasattr(obj, "exec") else obj.exec_()
163-
164-
165-
@contextlib.contextmanager
166-
def _maybe_allow_interrupt(qapp_or_eventloop):
167-
"""
168-
This manager allows to terminate a plot by sending a SIGINT. It is
169-
necessary because the running Qt backend prevents Python interpreter to
170-
run and process signals (i.e., to raise KeyboardInterrupt exception). To
171-
solve this one needs to somehow wake up the interpreter and make it close
172-
the plot window. We do this by using the signal.set_wakeup_fd() function
173-
which organizes a write of the signal number into a socketpair connected
174-
to the QSocketNotifier (since it is part of the Qt backend, it can react
175-
to that write event). Afterwards, the Qt handler empties the socketpair
176-
by a recv() command to re-arm it (we need this if a signal different from
177-
SIGINT was caught by set_wakeup_fd() and we shall continue waiting). If
178-
the SIGINT was caught indeed, after exiting the on_signal() function the
179-
interpreter reacts to the SIGINT according to the handle() function which
180-
had been set up by a signal.signal() call: it causes the qt_object to
181-
exit by calling its quit() method. Finally, we call the old SIGINT
182-
handler with the same arguments that were given to our custom handle()
183-
handler.
184-
185-
We do this only if the old handler for SIGINT was not None, which means
186-
that a non-python handler was installed, i.e. in Julia, and not SIG_IGN
187-
which means we should ignore the interrupts.
188-
"""
189-
190-
old_sigint_handler = signal.getsignal(signal.SIGINT)
191-
if old_sigint_handler in (None, signal.SIG_IGN, signal.SIG_DFL):
192-
yield
193-
return
194-
195-
handler_args = None
196-
wsock, rsock = socket.socketpair()
197-
wsock.setblocking(False)
198-
rsock.setblocking(False)
199-
old_wakeup_fd = signal.set_wakeup_fd(wsock.fileno())
200-
sn = QtCore.QSocketNotifier(rsock.fileno(), QtCore.QSocketNotifier.Type.Read)
201-
202-
# We do not actually care about this value other than running some Python code to
203-
# ensure that the interpreter has a chance to handle the signal in Python land. We
204-
# also need to drain the socket because it will be written to as part of the wakeup!
205-
# There are some cases where this may fire too soon / more than once on Windows so
206-
# we should be forgiving about reading an empty socket.
207-
# Clear the socket to re-arm the notifier.
208-
@sn.activated.connect
209-
def _may_clear_sock(*args):
210-
try:
211-
rsock.recv(1)
212-
except BlockingIOError:
213-
pass
214-
215-
def handle(*args):
216-
nonlocal handler_args
217-
handler_args = args
218-
if hasattr(qapp_or_eventloop, 'closeAllWindows'):
219-
qapp_or_eventloop.closeAllWindows()
220-
qapp_or_eventloop.quit()
221-
222-
signal.signal(signal.SIGINT, handle)
223-
try:
224-
yield
225-
finally:
226-
wsock.close()
227-
rsock.close()
228-
sn.setEnabled(False)
229-
signal.set_wakeup_fd(old_wakeup_fd)
230-
signal.signal(signal.SIGINT, old_sigint_handler)
231-
if handler_args is not None:
232-
old_sigint_handler(*handler_args)

lib/matplotlib/cbook.py

Lines changed: 70 additions & 10 deletions

0 commit comments

Comments
 (0)