Description
Bug report
Bug summary (suggested fix is in the very bottom)
This internal method is used in plt.pause()
and BlockingInput class (I don't know what the latter does).
The problem is approximately since Matplotlib 2.0.0 (but I don't know what exactly has changed). In the current implementation, the method execution can't be interrupted easily since it is based on event_loop.exec_()
(internally C++) function that locks out the Python interpreter and prevents it from processing SIGINT as KeyboardInterrupt.
The problem is clearly visible when you run some kind of animation (see example below), since the SIGINT is getting understood as KeyboardInterrupt by the target function of the Timer (which is in Python and thus the interpreter gets to run from time to time in the event loop). However, this exception in the timer target can't stop the even loop, and it continues.
In overall, this problem actually renders plt.pause()
practically unusable (this, and the window focusing issues). So either we should deprecate plt.pause()
or fix it, as I suggest below.
Code for reproduction
%pylab qt5
import numpy as np
import matplotlib.pyplot as plt
from matplotlib.animation import FuncAnimation
from matplotlib._pylab_helpers import Gcf
stopped = False
fig, ax = plt.subplots()
xdata, ydata = [], []
ln, = plt.plot([], [], 'ro')
def init():
ax.set_xlim(0, 2*np.pi)
ax.set_ylim(-1, 1)
return ln,
def update(frame):
xdata.append(frame)
ydata.append(np.sin(frame))
ln.set_data(xdata, ydata)
if frame == 2*np.pi:
stopped = True
return ln,
ani = FuncAnimation(fig, update, frames=np.linspace(0, 2*np.pi, 128),
init_func=init, blit=True, interval = 50, repeat=False)
# wait for the plotting/underlying process to end
try:
while not stopped:
manager = Gcf.get_fig_manager(fig.number)
manager.canvas.start_event_loop(1)
except KeyboardInterrupt:
plt.close(fig)
print("KeyboardInterrupt caught")
except AttributeError:
print("Figure closed from GUI")
Actual outcome
On pressing stop button in Jupyter:
And then the waiting cycle continues with no error. The exceptions on the Qt event loop get caught somewhere inside?
Expected outcome
Clean stop and closed figure on the except KeyboardInterrupt
clause.
Matplotlib version
Operating system: 4.18.16-100.fc27.x86_64
Matplotlib version: 3.0.2
Matplotlib backend: Qt5Agg
Python version: 3.6
IPython: 7.2.0
Jupyter notebook: 5.7.4
Everything installed with pip in a virtualenv.
Suggested fix
This problem has annoyed me for quite some time, and I am very glad that it seems that there is the solution. It is very closely related to #13302, and is solved similarly (using signal.set_wakeup_fd()
and socket.createsocketpair()
).
The problematic method should be rewritten as follows:
def start_event_loop(self, timeout=0):
if hasattr(self, "_event_loop") and self._event_loop.isRunning():
raise RuntimeError("Event loop already running")
self._event_loop = event_loop = QtCore.QEventLoop()
if timeout:
timer = QtCore.QTimer.singleShot(timeout * 1000, event_loop.quit)
SIGINTHandler(qApp)
interrupted = False
def sigint_handler():
nonlocal interrupted
event_loop.quit()
interrupted = True
old_sigint_handler = signal.getsignal(signal.SIGINT)
signal.signal(signal.SIGINT, lambda sig, _: sigint_handler())
try:
event_loop.exec_()
finally:
signal.signal(signal.SIGINT, old_sigint_handler)
if interrupted:
raise KeyboardInterrupt
SIGINTHandler class may be found in #13306