Skip to content

Backport PR #27221 on branch v3.8.x (FIX: Enable interrupts on macosx event loops) #27227

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
75 changes: 48 additions & 27 deletions lib/matplotlib/backends/backend_macosx.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import contextlib
import os
import signal
import socket
Expand Down Expand Up @@ -106,6 +107,13 @@ def resize(self, width, height):
ResizeEvent("resize_event", self)._process()
self.draw_idle()

def start_event_loop(self, timeout=0):
# docstring inherited
with _maybe_allow_interrupt():
# Call the objc implementation of the event loop after
# setting up the interrupt handling
self._start_event_loop(timeout=timeout)


class NavigationToolbar2Mac(_macosx.NavigationToolbar2, NavigationToolbar2):

Expand Down Expand Up @@ -171,34 +179,8 @@ def start_main_loop(cls):
# Set up a SIGINT handler to allow terminating a plot via CTRL-C.
# The logic is largely copied from qt_compat._maybe_allow_interrupt; see its
# docstring for details. Parts are implemented by wake_on_fd_write in ObjC.

old_sigint_handler = signal.getsignal(signal.SIGINT)
if old_sigint_handler in (None, signal.SIG_IGN, signal.SIG_DFL):
_macosx.show()
return

handler_args = None
wsock, rsock = socket.socketpair()
wsock.setblocking(False)
rsock.setblocking(False)
old_wakeup_fd = signal.set_wakeup_fd(wsock.fileno())
_macosx.wake_on_fd_write(rsock.fileno())

def handle(*args):
nonlocal handler_args
handler_args = args
_macosx.stop()

signal.signal(signal.SIGINT, handle)
try:
with _maybe_allow_interrupt():
_macosx.show()
finally:
wsock.close()
rsock.close()
signal.set_wakeup_fd(old_wakeup_fd)
signal.signal(signal.SIGINT, old_sigint_handler)
if handler_args is not None:
old_sigint_handler(*handler_args)

def show(self):
if not self._shown:
Expand All @@ -208,6 +190,45 @@ def show(self):
self._raise()


@contextlib.contextmanager
def _maybe_allow_interrupt():
"""
This manager allows to terminate a plot by sending a SIGINT. It is
necessary because the running backend prevents Python interpreter to
run and process signals (i.e., to raise KeyboardInterrupt exception). To
solve this one needs to somehow wake up the interpreter and make it close
the plot window. The implementation is taken from qt_compat, see that
docstring for a more detailed description.
"""
old_sigint_handler = signal.getsignal(signal.SIGINT)
if old_sigint_handler in (None, signal.SIG_IGN, signal.SIG_DFL):
yield
return

handler_args = None
wsock, rsock = socket.socketpair()
wsock.setblocking(False)
rsock.setblocking(False)
old_wakeup_fd = signal.set_wakeup_fd(wsock.fileno())
_macosx.wake_on_fd_write(rsock.fileno())

def handle(*args):
nonlocal handler_args
handler_args = args
_macosx.stop()

signal.signal(signal.SIGINT, handle)
try:
yield
finally:
wsock.close()
rsock.close()
signal.set_wakeup_fd(old_wakeup_fd)
signal.signal(signal.SIGINT, old_sigint_handler)
if handler_args is not None:
old_sigint_handler(*handler_args)


@_Backend.export
class _BackendMac(_Backend):
FigureCanvas = FigureCanvasMac
Expand Down
6 changes: 3 additions & 3 deletions src/_macosx.m
Original file line number Diff line number Diff line change
Expand Up @@ -451,7 +451,7 @@ int mpl_check_modifier(
}

static PyObject*
FigureCanvas_start_event_loop(FigureCanvas* self, PyObject* args, PyObject* keywords)
FigureCanvas__start_event_loop(FigureCanvas* self, PyObject* args, PyObject* keywords)
{
float timeout = 0.0;

Expand Down Expand Up @@ -522,8 +522,8 @@ int mpl_check_modifier(
(PyCFunction)FigureCanvas_remove_rubberband,
METH_NOARGS,
"Remove the current rubberband rectangle."},
{"start_event_loop",
(PyCFunction)FigureCanvas_start_event_loop,
{"_start_event_loop",
(PyCFunction)FigureCanvas__start_event_loop,
METH_KEYWORDS | METH_VARARGS,
NULL}, // docstring inherited
{"stop_event_loop",
Expand Down