-
-
Notifications
You must be signed in to change notification settings - Fork 7.9k
Can't interrupt start_event_loop() at backend_qt5.py::FigureCanvasQt #13315
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
Comments
I can reproduce some of these issues from a terminal import numpy as np
from matplotlib.animation import FuncAnimation
from matplotlib._pylab_helpers import Gcf
fig, ax = plt.subplots()
xdata, ydata = [], []
ln, = plt.plot([], [], 'ro')
stopped = False
def init():
ax.set_xlim(0, 2*np.pi)
ax.set_ylim(-1, 1)
return ln,
def update(frame):
global stopped
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:
plt.show(block=False)
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") although, oddly when I put the I it is possible that this is due to a change in default signal registration by pyqt rather than an intentional change in Matplotlib. One change that may be relevant here is 872675c which switched to using a stand-alone event loop in
qApp.exec_() sets up the requisite signal handling but the QEventLoop does not.
While this is very annoying for your use case, it still is plenty useful. I don't understand why we need the complexity of |
Yes, I agree that it is somewhere in the transition from PyQt4 to PyQt5. You probably should try the example from Jupyter to understand more precisely what I mean and my motivation. I am not sure what is the purpose of I think the problem with the approach of sending SIGTERM and killing Qt (taking down the Python process as well) is that a matplotlib window is treated like a standalone Qt application -- and it isn't. As @joelfrederico said, the end user doesn't usually even know what an event loop is! In my opinion, SIGINT should always return control to Python and raise there the pythonic KeyboardInterrupt that may be caught and processed. To do this it seems that, unfortunately, we need that complex class (may be it's possible to simplify it). You have mentioned I caught it: |
I do not think we should officially support running GUI backends from a jupyter notebook. It only works if and only if you are running the kernel on the same machine that you are running the web browser on. This means you lose the biggest benefit of notebooks (being able to run them remotely). We should focus on getting the behavior in the IPython terminal correct (it will be easier to debug without one more server involved). That will hopefully carry over to the notebook case.
This is a misunderstanding of what quitting
The
fair, the edge we need to walk here is to both make it so if users do not need to know about event loops they do not have to, but also to not re-invent a lot of semantics / functionality so that a user that does know about event loops will be surprised / frustrated.
I agree with the first part, but am 75/25 on the second part. Has it historically worked that way? A reasonable way to get the right behavior is to have the custom signal handler first interrupt Qt event loop and then call the original handler (basically
It does if you pause and then close all of the windows it does not exit the event loop we are running, it should to be consistent with
The purpose of
Non-visible windows may still existing in memory. If you still have a hard-reference to the |
@vdrhtc Sorry if I am reading as hostile, there is a lot of detail / complexity in this space and I am trying to be direct and efficient in this conversation. I am very excited someone wants to talk about this stuff 😄 . |
I need to double-check how signals are handled in Qt, but I believe the behavior on SIGINT is currently to return from Should it call the SIGINT handler on a ctrl-c during I moved away from running plots while other Python code is running. The real issue is that Python is not multithreaded. So the way interactive matplotlib works is to flicker between terminal and Qt execution. That's a hit in performance no matter how you slice it, so I opted to take a different approach and all this became moot to me. I agree though, if it should do anything, it should call the original SIGINT handler and not just raise a KeyboardInterrupt. We stored the original handler in order to reset it later, so that's easy enough to do. If it's the default handler, calling it should raise a KeyboardInterrupt. |
@tacaswell, it's all right, we are just getting to know each other =)
Well, the notebooks do not survive a simple page update (regarding standard output, the [*] mark and %pylab notebook plots) so dropping an SSH tunnel ruins the experience. Conversely, I would want to have on my local PC an environment like Matlab, or Mathematica, or RStudio, if you wish. JupyterLab is being developed for this, and I am sure it still has %pylab qt5 magic in it. What is the alternative? I know Spyder, but it looks the same to me with the same problems. I agree that we should test in the terminal first, but I can't agree with dropping %matplotlib qt5 support.
No-no, I meant matplotlib/lib/matplotlib/backends/backend_qt5.py Line 1129 in e7fb714
Yes, I understand otherwise I wouldn't be talking about lastWindowClosed (btw, did you watch my video??). I am only saying that same line prioritizes Qt over my script which has called it, and my program dies inside the qApp.exec_() if I want to interrupt it/or I can't interrupt it at all like in case.
Oh, yes, of course, that's right, I'll do that.
I am a bit lost in this sentence. I have tried to
Sorry, but than probably it should be called plt.run()! You are telling me what it's doing and not why would it do it!
I have a PR ready to deal with that as I said, should I post it?.. I feel that's not the intended behaviour. |
May be it is important when we are developing a standalone application using matplotlib, but then it probably won't use plt.show() like @tacaswell's bluesky -- it just calls the Qt internals. But if I call plt.show() it probably means I haven't ever heard about Qt and do not know how it works, I only want to see some plot (I am probably in the interactive mode, or at least I am used to it). This leads us to second thought:
I think when it comes to SIGINT during an interactive session, the default behaviour should be to stop at all costs and return to the shell so the user can issue other commands. If it is not interactive, it still should behave familiar. The users should feel as they were interrupting, for example, a script with a simple for-loop: may be an exception, if they do not explicitly catch it, but no delays. PyQt5 can't handle (fatal, what does this mean?) exceptions, as it seems from here: matplotlib/lib/matplotlib/backends/backend_qt5.py Lines 512 to 518 in e7fb714
And what's that, if that's not a secret?
Yes, I will do that at least |
Unfortunately, my different approach is proprietary. ;) If you're going to call That's exactly what I mean, what does "can't handle" and "fatal" mean? I don't know. Presumably this isn't undefined behavior, I just haven't done the research to know what the well-defined behavior is. Probably involves terminating the app somehow? |
Okay... But are you still on Python and mpl?
I tested with this: from PyQt5 import QtCore, QtWidgets
qApp = QtWidgets.QApplication(["lol"])
window = QtWidgets.QWidget()
def raise_error():
raise ValueError("lol")
window.show()
timer = QtCore.QTimer.singleShot(1000, raise_error)
qApp.exec() In Jupyter with no magic it spits out exception but does not stop, window is still closeable and qApp.exec() returns 0 after closing. Far from fatal! When run as a script in Pycharm, it prints the traceback and gets killed: |
Ah, "proprietary" unfortunately means we've reached the end of the discussion. What happens in a vanilla Python shell...? Ooo badness... it's an unhandled abort. My hunch is that Python calls |
This may be a change in PyQt under us. The recently have recently gone through a process of making it much less forgiving of exceptions.
I think it is closely related.
It pauses your code and gives the user a chance to poke around.
If you use the pan/zoom functionality you are using that functionality. You can build pretty cool stuff that is cross-platform and cross-framework without too much extra hassle.
I think this is critical for use in IPython (although, the most important thing it does (registering the inputhook) should happen auto-magically... Sorry, don't have time for more in-depth comments tonight. |
Okay (x2) 😅
I like this error, that's oldschool |
This issue has been marked "inactive" because it has been 365 days since the last comment. If this issue is still present in recent Matplotlib releases, or the feature request is still wanted, please leave a comment and this label will be removed. If there are no updates in another 30 days, this issue will be automatically closed, but you are free to re-open or create a new issue if needed. We value issue reports, and this procedure is meant to help us resurface and prioritize issues that have not been addressed yet, not make them disappear. Thanks for your help! |
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 deprecateplt.pause()
or fix it, as I suggest below.Code for reproduction
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
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()
andsocket.createsocketpair()
).The problematic method should be rewritten as follows:
SIGINTHandler class may be found in #13306
The text was updated successfully, but these errors were encountered: