-
-
Notifications
You must be signed in to change notification settings - Fork 7.5k
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
fix FigureManagerTk close behavior if embedded in Tk App #17802
fix FigureManagerTk close behavior if embedded in Tk App #17802
Conversation
Check if the Tk mainloop was dispatching (with an ugly thread hack) and don't quit it if that was the case.
managers[0].window.mainloop() | ||
|
||
|
||
def is_tk_mainloop_running(): |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
how does this compare with https://github.com/matplotlib/matplotlib/blob/master/lib/matplotlib/cbook/__init__.py#L65 (possibly combined with checking that the thread matches threading.main_thread()
)?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
In the contexts that I call is_tk_mainloop_running
, _get_running_interactive_framework
returns None
, not "tk". This is because _get_running_interactive_framework
is incorrectly implemented. There are several mainloop functions in various classes in tkinter and it only checks for one of them. Importantly one of them is a built-in method that has no __code__
attribute to check ("built-in method mainloop of _tkinter.tkapp object") so i'm not sure how to check for it. It would be a rare case to call that method directly, however, so maybe it would be ok to overlook?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I see your point. Adding a check for Misc.mainloop as you did certainly seems better. I don't have a good solution for the general case, though.
with revised _get_running_interactive_framework
Thanks for working on this @richardsheridan ! I am not sure that this will work, consider the sequence of:
I think this is going the right direction, but instead of detecting on a per-figure-manager basis if it should quit the tk event loop we should do the check in That is more global state (which I'm not wild about), but GUI frameworks seem to be an endless source of global state so one more bit won't do any harm.... |
For what it's worth, I'm not sure your sequence demonstrates an actual issue. Yes, the REPL hangs if you open a tkinter window and then
However, it is not necessary to call I'll try arming |
While true, we definitely have users that want to work in a terminal and do not use That aside, the same scenario in a script is a much better story (user wants to stop the script, open a plot (that spawns more figures) and if the windows get closed in the wrong order the script hangs forever). |
This idea came out very elegantly, as long as there are no unexpected wider behavior changes. I added a test that is more minimal than the MVE shown in #13470. For the record, this test also passes when performed on the REPL as well, except the root window hangs around due to This does not fix the The fix to |
@richardsheridan I took the liberty of fixing the style issues in the test, I hope you don't mind. |
While testing is good, there is a lot of code in the GUI / interactive / repl space that we do not have tests on because the trade off of the effort to write the test vs the benefit is not worth it. I think that this is in that category.
I'm not sure how to get the hang any more though, the following: import matplotlib.pyplot as plt
plt.ion()
fig, ax = plt.subplots()
fig.canvas.mpl_connect('button_press_event', lambda *args, **kwargs: plt.figure())
plt.show(block=True) sets it up so if you click anywhere on the first figure it spawns more. You can close them in any order and it seems to work (which is the case I was worried about). If you call |
Likely due to misusing bind("<Destroy>",...) rather than using protocol("WM_DELETE_WINDOW",...), there was a lot of unnecessary logic for dealing with recursive entry into this method.
…nto tk_backend_figuremanager_quit
I discovered two bugs in the PR, which I believe the additional commits address. Managers were not quitting loops that they had responsibility for. I cherry picked a commit from my other PR #17789 that corrects This appears to also fix the |
One thing we have found with CI is that you need to use rather extreme timeouts, things take longer than "reasonable" because they are running on very over-subscribed machines at low-priority (the service is free so we can not complain too much). How were you getting the repl to hang? |
It seems there is some kind of deeper race condition going on. Before the fix, there a spurious green zone around 0.1 seconds of waiting that will make the test pass and more or less than that everything hangs unless Tcl recieves some user events (i.e. mouseover the remaining window). I tried a dozen different things to iron this out and nothing was working. Now I finally understand why that test in test_backends_interactive.py was running in a subprocess... |
if self.canvas._idle_callback: | ||
self.canvas._tkcanvas.after_cancel(self.canvas._idle_callback) | ||
|
||
self.window.destroy() |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We used to drop the window, presumably to avoid calling destroy()
on it twice (?). Is that a concern we should have?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Calling destroy twice was an artifact of mistakenly binding instead of using protocol. It should be fine now.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
One small question, but still approve!
Ah, thanks! Sorry I was being dense. Thank you for working on this @richardsheridan ! I will try to take a look at your other PR tomorrow. |
skip running mainloop if we already own one do cheaper test first
Co-authored-by: Elliott Sales de Andrade <quantum.analyst@gmail.com>
Thanks @richardsheridan! Congratulations on your first PR to Matplotlib 🎉 We hope to hear from you again. |
PR Summary
Fixes #13470. I test to see if the Tk mainloop was running before FigureManagerTk was created and if that was the case, then I don't call the
window.quit
method.The solution relies on implementation details of the tkinter c module and a disgusting hack using threads, so I have made this PR a draft. Really, the functionality of my
is_tk_mainloop_running
function should be provided as a method on_tkinter.tkapp
AKATkappObject
to access the dispatching struct member from Python, but that's a job for cPython upstream.I suppose there could be an alternative method involving traversing the children of the
_default_root
, or parents of the figure window but that feels just as much like leaning on implementation details and more error prone.If there is any interest in maintaining the "fix" within matplotlib, I'll work on this PR further, otherwise this and #13470 should be closed pending upstream changes.
PR Checklist