From faddff9a21095a9f2777016dcd244904812f8441 Mon Sep 17 00:00:00 2001 From: Greg Lucas Date: Sat, 28 Oct 2023 09:28:14 -0600 Subject: [PATCH] FIX: Enable interrupts on macosx event loops Follow the same logic with a context manager for the handling of the event interrupts so that we can re-use the logic from the main loop implementation in the start_event_loop function as well. --- lib/matplotlib/backends/backend_macosx.py | 75 +++++++++++++++-------- src/_macosx.m | 6 +- 2 files changed, 51 insertions(+), 30 deletions(-) diff --git a/lib/matplotlib/backends/backend_macosx.py b/lib/matplotlib/backends/backend_macosx.py index ecf21b07aef4..a39f5b5b1497 100644 --- a/lib/matplotlib/backends/backend_macosx.py +++ b/lib/matplotlib/backends/backend_macosx.py @@ -1,3 +1,4 @@ +import contextlib import os import signal import socket @@ -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): @@ -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: @@ -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 diff --git a/src/_macosx.m b/src/_macosx.m index e9677a40e6b5..6df00d0eca8e 100755 --- a/src/_macosx.m +++ b/src/_macosx.m @@ -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; @@ -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",