Thanks to visit codestin.com
Credit goes to github.com

Skip to content

Commit 29de7a1

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 29de7a1

File tree

4 files changed

+105
-127
lines changed

4 files changed

+105
-127
lines changed

lib/matplotlib/backends/backend_macosx.py

Lines changed: 10 additions & 49 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,4 @@
1-
import contextlib
21
import os
3-
import signal
4-
import socket
52

63
import matplotlib as mpl
74
from matplotlib import _api, cbook
@@ -18,6 +15,12 @@ class TimerMac(_macosx.Timer, TimerBase):
1815
# completely implemented at the C-level (in _macosx.Timer)
1916

2017

18+
def _allow_interrupt_macos():
19+
"""A context manager that allows terminating a plot by sending a SIGINT."""
20+
return cbook._allow_interrupt(
21+
lambda rsock: _macosx.wake_on_fd_write(rsock.fileno()), _macosx.stop)
22+
23+
2124
class FigureCanvasMac(FigureCanvasAgg, _macosx.FigureCanvas, FigureCanvasBase):
2225
# docstring inherited
2326

@@ -109,10 +112,9 @@ def resize(self, width, height):
109112

110113
def start_event_loop(self, timeout=0):
111114
# docstring inherited
112-
with _maybe_allow_interrupt():
113-
# Call the objc implementation of the event loop after
114-
# setting up the interrupt handling
115-
self._start_event_loop(timeout=timeout)
115+
# Set up a SIGINT handler to allow terminating a plot via CTRL-C.
116+
with _allow_interrupt_macos():
117+
self._start_event_loop(timeout=timeout) # Forward to ObjC implementation.
116118

117119

118120
class NavigationToolbar2Mac(_macosx.NavigationToolbar2, NavigationToolbar2):
@@ -177,9 +179,7 @@ def destroy(self):
177179
@classmethod
178180
def start_main_loop(cls):
179181
# Set up a SIGINT handler to allow terminating a plot via CTRL-C.
180-
# The logic is largely copied from qt_compat._maybe_allow_interrupt; see its
181-
# docstring for details. Parts are implemented by wake_on_fd_write in ObjC.
182-
with _maybe_allow_interrupt():
182+
with _allow_interrupt_macos():
183183
_macosx.show()
184184

185185
def show(self):
@@ -190,45 +190,6 @@ def show(self):
190190
self._raise()
191191

192192

193-
@contextlib.contextmanager
194-
def _maybe_allow_interrupt():
195-
"""
196-
This manager allows to terminate a plot by sending a SIGINT. It is
197-
necessary because the running backend prevents Python interpreter to
198-
run and process signals (i.e., to raise KeyboardInterrupt exception). To
199-
solve this one needs to somehow wake up the interpreter and make it close
200-
the plot window. The implementation is taken from qt_compat, see that
201-
docstring for a more detailed description.
202-
"""
203-
old_sigint_handler = signal.getsignal(signal.SIGINT)
204-
if old_sigint_handler in (None, signal.SIG_IGN, signal.SIG_DFL):
205-
yield
206-
return
207-
208-
handler_args = None
209-
wsock, rsock = socket.socketpair()
210-
wsock.setblocking(False)
211-
rsock.setblocking(False)
212-
old_wakeup_fd = signal.set_wakeup_fd(wsock.fileno())
213-
_macosx.wake_on_fd_write(rsock.fileno())
214-
215-
def handle(*args):
216-
nonlocal handler_args
217-
handler_args = args
218-
_macosx.stop()
219-
220-
signal.signal(signal.SIGINT, handle)
221-
try:
222-
yield
223-
finally:
224-
wsock.close()
225-
rsock.close()
226-
signal.set_wakeup_fd(old_wakeup_fd)
227-
signal.signal(signal.SIGINT, old_sigint_handler)
228-
if handler_args is not None:
229-
old_sigint_handler(*handler_args)
230-
231-
232193
@_Backend.export
233194
class _BackendMac(_Backend):
234195
FigureCanvas = FigureCanvasMac

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: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,8 @@
1414
import os
1515
from pathlib import Path
1616
import shlex
17+
import signal
18+
import socket
1719
import subprocess
1820
import sys
1921
import time
@@ -82,6 +84,64 @@ def _get_running_interactive_framework():
8284
return None
8385

8486

87+
@contextlib.contextmanager
88+
def _allow_interrupt(prepare_notifier, handle_sigint):
89+
"""
90+
A context manager that allows terminating a plot by sending a SIGINT. It
91+
is necessary because the running backend prevents Python interpreter to
92+
run and process signals (i.e., to raise KeyboardInterrupt exception). To
93+
solve this one needs to somehow wake up the interpreter and make it close
94+
the plot window. We do this by using the signal.set_wakeup_fd() function
95+
which organizes a write of the signal number into a socketpair. A
96+
backend-specific function, *prepare_notifier*, arranges to listen to the
97+
pair's read socket while the event loop is running. (If it returns a
98+
notifier object, that object is kept alive while the context manager runs.)
99+
100+
If SIGINT was indeed caught, after exiting the on_signal() function the
101+
interpreter reacts to the signal according to the handler function which
102+
had been set up by a signal.signal() call; here, we arrange to call the
103+
backend-specific *handle_sigint* function. Finally, we call the old SIGINT
104+
handler with the same arguments that were given to our custom handler.
105+
106+
We do this only if the old handler for SIGINT was not None, which means
107+
that a non-python handler was installed, i.e. in Julia, and not SIG_IGN
108+
which means we should ignore the interrupts.
109+
110+
Parameters
111+
----------
112+
prepare_notifier : Callable[[socket.socket], object]
113+
handle_sigint : Callable[[], object]
114+
"""
115+
116+
old_sigint_handler = signal.getsignal(signal.SIGINT)
117+
if old_sigint_handler in (None, signal.SIG_IGN, signal.SIG_DFL):
118+
yield
119+
return
120+
121+
handler_args = None
122+
wsock, rsock = socket.socketpair()
123+
wsock.setblocking(False)
124+
rsock.setblocking(False)
125+
old_wakeup_fd = signal.set_wakeup_fd(wsock.fileno())
126+
notifier = prepare_notifier(rsock)
127+
128+
def save_args_and_handle_sigint(*args):
129+
nonlocal handler_args
130+
handler_args = args
131+
handle_sigint()
132+
133+
signal.signal(signal.SIGINT, save_args_and_handle_sigint)
134+
try:
135+
yield
136+
finally:
137+
wsock.close()
138+
rsock.close()
139+
signal.set_wakeup_fd(old_wakeup_fd)
140+
signal.signal(signal.SIGINT, old_sigint_handler)
141+
if handler_args is not None:
142+
old_sigint_handler(*handler_args)
143+
144+
85145
def _exception_printer(exc):
86146
if _get_running_interactive_framework() in ["headless", None]:
87147
raise exc

0 commit comments

Comments
 (0)