Merge pull request #27285 from anntzer/ai · matplotlib/matplotlib@aeefb3b · GitHub
Skip to content

Commit aeefb3b

Browse files
authored
Merge pull request #27285 from anntzer/ai
Factor out common parts of qt and macos interrupt handling.
2 parents 2a840e4 + 3bd9abe commit aeefb3b

4 files changed

Lines changed: 108 additions & 129 deletions

File tree

lib/matplotlib/backend_bases.py

Lines changed: 60 additions & 0 deletions

lib/matplotlib/backends/backend_macosx.py

Lines changed: 11 additions & 50 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
@@ -10,14 +7,20 @@
107
from .backend_agg import FigureCanvasAgg
118
from matplotlib.backend_bases import (
129
_Backend, FigureCanvasBase, FigureManagerBase, NavigationToolbar2,
13-
ResizeEvent, TimerBase)
10+
ResizeEvent, TimerBase, _allow_interrupt)
1411

1512

1613
class TimerMac(_macosx.Timer, TimerBase):
1714
"""Subclass of `.TimerBase` using CFRunLoop timer events."""
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 _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: 37 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -9,13 +9,12 @@
99
from matplotlib.backend_bases import (
1010
_Backend, FigureCanvasBase, FigureManagerBase, NavigationToolbar2,
1111
TimerBase, cursors, ToolContainerBase, MouseButton,
12-
CloseEvent, KeyEvent, LocationEvent, MouseEvent, ResizeEvent)
12+
CloseEvent, KeyEvent, LocationEvent, MouseEvent, ResizeEvent,
13+
_allow_interrupt)
1314
import matplotlib.backends.qt_editor.figureoptions as figureoptions
1415
from . import qt_compat
1516
from .qt_compat import (
16-
QtCore, QtGui, QtWidgets, __version__, QT_API,
17-
_to_int, _isdeleted, _maybe_allow_interrupt
18-
)
17+
QtCore, QtGui, QtWidgets, __version__, QT_API, _to_int, _isdeleted)
1918

2019

2120
# SPECIAL_KEYS are Qt::Key that do *not* return their Unicode name
@@ -148,6 +147,38 @@ def _create_qApp():
148147
return app
149148

150149

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

@@ -417,7 +448,7 @@ def start_event_loop(self, timeout=0):
417448
if timeout > 0:
418449
_ = QtCore.QTimer.singleShot(int(timeout * 1000), event_loop.quit)
419450

420-
with _maybe_allow_interrupt(event_loop):
451+
with _allow_interrupt_qt(event_loop):
421452
qt_compat._exec(event_loop)
422453

423454
def stop_event_loop(self, event=None):
@@ -598,7 +629,7 @@ def resize(self, width, height):
598629
def start_main_loop(cls):
599630
qapp = QtWidgets.QApplication.instance()
600631
if qapp:
601-
with _maybe_allow_interrupt(qapp):
632+
with _allow_interrupt_qt(qapp):
602633
qt_compat._exec(qapp)
603634

604635
def show(self):

lib/matplotlib/backends/qt_compat.py

Lines changed: 0 additions & 73 deletions

0 commit comments

Comments
 (0)