8000 Fix support for Ctrl-C on the macosx backend. · matplotlib/matplotlib@bfa5d4a · GitHub
[go: up one dir, main page]

Skip to content

Commit bfa5d4a

Browse files
committed
Fix support for Ctrl-C on the macosx backend.
Support is largely copy-pasted from, and tests are shared with, the qt implementation (qt_compat._maybe_allow_interrupt), the main difference being that what we need from QSocketNotifier, as well as the equivalent for QApplication.quit(), are reimplemented in ObjC. qt_compat._maybe_allow_interrupt is also slightly cleaned up by moving out the "do-nothing" case (`old_sigint_handler in (None, SIG_IGN, SIG_DFL)`) and dedenting the rest, instead of keeping track of whether signals were actually manipulated via a `skip` variable. Factoring out the common parts of _maybe_allow_interrupt is left as a follow-up. (Test e.g. with `MPLBACKEND=macosx python -c "from pylab import *; plot(); show()"` followed by Ctrl-C.)
1 parent f017315 commit bfa5d4a

File tree

6 files changed

+314
-236
lines changed

6 files changed

+314
-236
lines changed

lib/matplotlib/backends/backend_macosx.py

Lines changed: 33 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,6 @@
11
import os
2+
import signal
3+
import socket
24

35
import matplotlib as mpl
46
from matplotlib import _api, cbook
@@ -164,7 +166,37 @@ def destroy(self):
164166

165167
@classmethod
166168
def start_main_loop(cls):
167-
_macosx.show()
169+
# Set up a SIGINT handler to allow terminating a plot via CTRL-C.
170+
# The logic is largely copied from qt_compat._maybe_allow_interrupt; see its
171+
# docstring for details. Parts are implemented by wake_on_fd_write in ObjC.
172+
173+
old_sigint_handler = signal.getsignal(signal.SIGINT)
174+
if old_sigint_handler in (None, signal.SIG_IGN, signal.SIG_DFL):
175+
_macosx.show()
176+
return
177+
178+
handler_args = None
179+
wsock, rsock = socket.socketpair()
180+
wsock.setblocking(False)
181+
rsock.setblocking(False)
182+
old_wakeup_fd = signal.set_wakeup_fd(wsock.fileno())
183+
_macosx.wake_on_fd_write(rsock.fileno())
184+
185+
def handle(*args):
186+
nonlocal handler_args
187+
handler_args = args
188+
_macosx.stop()
189+
190+
signal.signal(signal.SIGINT, handle)
191+
try:
192+
_macosx.show()
193+
finally:
194+
wsock.close()
195+
rsock.close()
196+
signal.set_wakeup_fd(old_wakeup_fd)
197+
signal.signal(signal.SIGINT, old_sigint_handler)
198+
if handler_args is not None:
199+
old_sigint_handler(*handler_args)
168200

169201
def show(self):
170202
if not self._shown:

lib/matplotlib/backends/qt_compat.py

Lines changed: 37 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -198,48 +198,46 @@ def _maybe_allow_interrupt(qapp):
198198
that a non-python handler was installed, i.e. in Julia, and not SIG_IGN
199199
which means we should ignore the interrupts.
200200
"""
201+
201202
old_sigint_handler = signal.getsignal(signal.SIGINT)
202-
handler_args = None
203-
skip = False
204203
if old_sigint_handler in (None, signal.SIG_IGN, signal.SIG_DFL):
205-
skip = True
206-
else:
207-
wsock, rsock = socket.socketpair()
208-
wsock.setblocking(False)
209-
old_wakeup_fd = signal.set_wakeup_fd(wsock.fileno())
210-
sn = QtCore.QSocketNotifier(
211-
rsock.fileno(), _enum('QtCore.QSocketNotifier.Type').Read
212-
)
204+
yield
205+
return
206+
207+
handler_args = None
208+
wsock, rsock = socket.socketpair()
209+
wsock.setblocking(False)
210+
rsock.setblocking(False)
211+
old_wakeup_fd = signal.set_wakeup_fd(wsock.fileno())
212+
sn = QtCore.QSocketNotifier(
213+
rsock.fileno(), _enum('QtCore.QSocketNotifier.Type').Read)
214+
215+
# We do not actually care about this value other than running some Python code to
216+
# ensure that the interpreter has a chance to handle the signal in Python land. We
217+
# also need to drain the socket because it will be written to as part of the wakeup!
218+
# There are some cases where this may fire too soon / more than once on Windows so
219+
# we should be forgiving about reading an empty socket.
220+
# Clear the socket to re-arm the notifier.
221+
@sn.activated.connect
222+
def _may_clear_sock(*args):
223+
try:
224+
rsock.recv(1)
225+
except BlockingIOError:
226+
pass
227+
228+
def handle(*args):
229+
nonlocal handler_args
230+
handler_args = args
231+
qapp.quit()
213232

214-
# We do not actually care about this value other than running some
215-
# Python code to ensure that the interpreter has a chance to handle the
216-
# signal in Python land. We also need to drain the socket because it
217-
# will be written to as part of the wakeup! There are some cases where
218-
# this may fire too soon / more than once on Windows so we should be
219-
# forgiving about reading an empty socket.
220-
rsock.setblocking(False)
221-
# Clear the socket to re-arm the notifier.
222-
@sn.activated.connect
223-
def _may_clear_sock(*args):
224-
try:
225-
rsock.recv(1)
226-
except BlockingIOError:
227-
pass
228-
229-
def handle(*args):
230-
nonlocal handler_args
231-
handler_args = args
232-
qapp.quit()
233-
234-
signal.signal(signal.SIGINT, handle)
233+
signal.signal(signal.SIGINT, handle)
235234
try:
236235
yield
237236
finally:
238-
if not skip:
239-
wsock.close()
240-
rsock.close()
241-
sn.setEnabled(False)
242-
signal.set_wakeup_fd(old_wakeup_fd)
243-
signal.signal(signal.SIGINT, old_sigint_handler)
244-
if handler_args is not None:
245-
old_sigint_handler(*handler_args)
237+
wsock.close()
238+
rsock.close()
239+
sn.setEnabled(False)
240+
signal.set_wakeup_fd(old_wakeup_fd)
241+
signal.signal(signal.SIGINT, old_sigint_handler)
242+
if handler_args is not None:
243+
old_sigint_handler(*handler_args)

lib/matplotlib/testing/__init__.py

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -173,6 +173,33 @@ def _check_for_pgf(texsystem):
173173
return True
174174

175175

176+
class _WaitForStringPopen(subprocess.Popen):
177+
"""
178+
A Popen that passes flags that allow triggering KeyboardInterrupt.
179+
"""
180+
181+
def __init__(self, *args, **kwargs):
182+
if sys.platform == 'win32':
183+
kwargs['creationflags'] = subprocess.CREATE_NEW_CONSOLE
184+
super().__init__(
185+
*args, **kwargs,
186+
# Force Agg so that each test can switch to its desired Qt backend.
187+
env={**os.environ, "MPLBACKEND": "Agg", "SOURCE_DATE_EPOCH": "0"},
188+
stdout=subprocess.PIPE, universal_newlines=True)
189+
190+
def wait_for(self, terminator):
191+
"""Read until the terminator is reached."""
192+
buf = ''
193+
while True:
194+
c = self.stdout.read(1)
195+
if not c:
196+
raise RuntimeError(
197+
f'Subprocess died before emitting expected {terminator!r}')
198+
buf += c
199+
if buf.endswith(terminator):
200+
return
201+
202+
176203
def _has_tex_package(package):
177204
try:
178205
mpl.dviread.find_tex_file(f"{package}.sty")

0 commit comments

Comments
 (0)
0