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

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

File tree

4 files changed

+108
-129
lines changed

4 files changed

+108
-129
lines changed

lib/matplotlib/backend_bases.py

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,8 @@
3535
import itertools
3636
import logging
3737
import os
38+
import signal
39+
import socket
3840
import sys
3941
import time
4042
import weakref
@@ -1651,6 +1653,64 @@ def _is_non_interactive_terminal_ipython(ip):
16511653
and getattr(ip.parent, 'interact', None) is False)
16521654

16531655

1656+
@contextmanager
1657+
def _allow_interrupt(prepare_notifier, handle_sigint):
1658+
"""
1659+
A context manager that allows terminating a plot by sending a SIGINT. It
1660+
is necessary because the running backend prevents the Python interpreter
1661+
from running and processing signals (i.e., to raise a KeyboardInterrupt).
1662+
To solve this, one needs to somehow wake up the interpreter and make it
1663+
close the plot window. We do this by using the signal.set_wakeup_fd()
1664+
function which organizes a write of the signal number into a socketpair.
1665+
A backend-specific function, *prepare_notifier*, arranges to listen to
1666+
the pair's read socket while the event loop is running. (If it returns a
1667+
notifier object, that object is kept alive while the context manager runs.)
1668+
1669+
If SIGINT was indeed caught, after exiting the on_signal() function the
1670+
interpreter reacts to the signal according to the handler function which
1671+
had been set up by a signal.signal() call; here, we arrange to call the
1672+
backend-specific *handle_sigint* function. Finally, we call the old SIGINT
1673+
handler with the same arguments that were given to our custom handler.
1674+
1675+
We do this only if the old handler for SIGINT was not None, which means
1676+
that a non-python handler was installed, i.e. in Julia, and not SIG_IGN
1677+
which means we should ignore the interrupts.
1678+
1679+
Parameters
1680+
----------
1681+
prepare_notifier : Callable[[socket.socket], object]
1682+
handle_sigint : Callable[[], object]
1683+
"""
1684+
1685+
old_sigint_handler = signal.getsignal(signal.SIGINT)
1686+
if old_sigint_handler in (None, signal.SIG_IGN, signal.SIG_DFL):
1687+
yield
1688+
return
1689+
1690+
handler_args = None
1691+
wsock, rsock = socket.socketpair()
1692+
wsock.setblocking(False)
1693+
rsock.setblocking(False)
1694+
old_wakeup_fd = signal.set_wakeup_fd(wsock.fileno())
1695+
notifier = prepare_notifier(rsock)
1696+
1697+
def save_args_and_handle_sigint(*args):
1698+
nonlocal handler_args
1699+
handler_args = args
1700+
handle_sigint()
1701+
1702+
signal.signal(signal.SIGINT, save_args_and_handle_sigint)
1703+
try:
1704+
yield
1705+
finally:
1706+
wsock.close()
1707+
rsock.close()
1708+
signal.set_wakeup_fd(old_wakeup_fd)
1709+
signal.signal(signal.SIGINT, old_sigint_handler)
1710+
if handler_args is not None:
1711+
old_sigint_handler(*handler_args)
1712+
1713+
16541714
class FigureCanvasBase:
16551715
"""
16561716
The canvas the figure renders into.

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
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)

0 commit comments

Comments
 (0)