Skip to content

Existing FigureCanvasQT objects destroyed by call to plt.figure #14426

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

Closed
rayosborn opened this issue Jun 2, 2019 · 23 comments
Closed

Existing FigureCanvasQT objects destroyed by call to plt.figure #14426

rayosborn opened this issue Jun 2, 2019 · 23 comments
Milestone

Comments

@rayosborn
Copy link

rayosborn commented Jun 2, 2019

Bug report

Bug summary

For a number of years, I have been maintaining an interactive application that embeds subclassed FigureCanvasQT objects within a PyQt application. Up until Matplotlib v3.0.3., it was possible to create standard Matplotlib PyQt figures, i.e., using plt.figure within an embedded IPython shell, that coexist with the subclassed canvas objects. Now in Matplotlib v3.1.0, a call to plt.figure() destroys all the other canvas objects.

Unfortunately, I cannot debug this within Visual Studio since I am currently unable to install Matplotlib from the source. By going through the new_figure_manager code line by line, I can confirm that the windows are destroyed when calling FigureCanvasQT.__init__(figure), but not when calling FigureCanvasBase.__init__(figure), but I can't work out what triggers the destruction of the other windows. I presume the subclassed canvasses are not being registered somewhere, and pyplot assumes they are no longer active, but I am hoping someone will show me where to look. This may not be a Matplotlib bug, but it's certainly an unwelcome side effect in my application.

Code for reproduction
If you have nexpy installed (pip install nexpy) and launch it, you can reproduce this from within the embedded IPython shell with the following, which creates a new subclassed window and then attempts to open a regular pyplot window.:

In [1]: new_window=NXPlotView()
In [2]: plt.get_fignums()
Out[2]: [1, 2]
In [3]: plt.figure()

There are two figure numbers, because NeXpy automatically creates a window with a figure number of 1 when it is launched.

Actual outcome

A new window with an updated figure number is created but all other windows not created by pyplot are destroyed.

In [4]: plt.get_fignums()
Out[4]: [3]

Expected outcome

In Matplotlib v3.0.3, a new pyplot window is created by the PyQt5 backend without destroying anything else.

In [4]: plt.get_fignums()
Out[4]: [1, 2, 3]

Matplotlib version

  • Operating system: Mac OS v10.14.5
  • Matplotlib version: 3.1.0
  • Matplotlib backend: Qt5Agg
  • Python version: 3.7.2
  • Jupyter version (if applicable): 1.0.0
  • Other libraries:
@anntzer
Copy link
Contributor

anntzer commented Jun 3, 2019

This bisects to #12637, and is essentially due to the fact that we now initialize ipython/matplotlib support when the first canvas is created (here, by plt.figure()), that during initialization, ipython calls switch_backend, that switch_backend starts by calling close("all"), and that NXPlotView() is registered with pyplot and thus gets closed at that point.

I think we can actually remove the close("all") (well, modulo backcompat, yada yada)? If there are conflicting event loops, closing figures won't help, if there aren't (like in this case), we don't need to close them.

In the meantime you can probably get away with creating a figure and immediately closing it -- we only initialize ipython/matplotlib support once.

@anntzer anntzer added this to the v3.1.1 milestone Jun 3, 2019
@anntzer anntzer added the Release critical For bugs that make the library unusable (segfaults, incorrect plots, etc) and major regressions. label Jun 3, 2019
@rayosborn
Copy link
Author

Thanks for the information. I am subclassing FigureCanvasQT, so I would prefer to preempt the close("all") call, perhaps by callingswitch_backend myself for the first instance, but I can't see how it gets called during the first plt.figure() call. I'll look again tomorrow, but if you have any tips, I would appreciate them. I wondered if it was hidden in the switch_backend_decorator, but I don't see that being used.

@anntzer
Copy link
Contributor

anntzer commented Jun 4, 2019

switch_backend is called via ip.enable_matplotlib(), which is called in FigureCanvasBase._fix_ipython_backend2gui, which is called in the FigureCanvasBase constructor.

@rayosborn
Copy link
Author

I think removing that close("all") might be the only solution for me. I have tried endless permutations of preemptively setting mpl.rcParamsOrig('backend'), calling pt.enable_matplotlib or pt.activate_matplotlib, adding a dummy _fix_ipython_backend2gui to my FigureCanvasQT subclass, even defining my own subclass of _BackendQT5 using the _Backend.export decorator (which produces unhelpful side effects) , but none of them stop IPython from calling switch_backend, whose first line is close("all"). I need to make a new release of NeXpy in the next couple of days, but I am going to have to make Matplotlib v3.0.3 the highest allowed version, unless anyone has other tips for me to try.

@anntzer
Copy link
Contributor

anntzer commented Jun 5, 2019

It looks like the following workaround does suffice: add a call to FigureCanvas(Figure()) at the toplevel of plotview.py (note that because the figure is not registered with pyplot, it will be immediately garbage collected anyways, but that'll arrange to call _fix_ipython_backend2gui properly).

(I still do think that we should not call close("all"), but that may take longer to change...)

@rayosborn
Copy link
Author

Thanks for the suggestion, which I think would work. In the past few minutes, I have just found an alternative solution. I just have to monkey-patch the IPython InteractiveShell class to do nothing when there is a call to enable_matplotlib. Everything else is already initialized so the call is redundant. It also seems to work.

I haven't thought about this issue nearly as long as you, but I get the impression that the ultimate solution would have to come from IPython allowing alternative backends to be registered in some well-documented way.

@anntzer
Copy link
Contributor

anntzer commented Jun 5, 2019

The way we are registering backends with IPython definitely borders API abuse, sorry for breaking things on your side. On the other hand, I would rather have as much of that code as possible living on Matplotlib's side, as cross-project coordination is a much harder problem...

@anntzer anntzer modified the milestones: v3.1.1, v3.2.0 Jun 6, 2019
@anntzer anntzer removed the Release critical For bugs that make the library unusable (segfaults, incorrect plots, etc) and major regressions. label Jun 6, 2019
@anntzer
Copy link
Contributor

anntzer commented Jun 6, 2019

I remilestone to 3.2 as we have a workaround now, although I do think the API needs to be revisited on our side.

@rayosborn
Copy link
Author

I think the simplest short-term fix would be to add an optional keyword argument to the FigureCanvasBase to allow instances to skip the call to _fix_ipython_backend2gui if it's unnecessary. If you are loath to change call signatures, the same thing could be achieved by adding a private class variable (_fix_ipython=True) that a subclass could override.

@anntzer
Copy link
Contributor

anntzer commented Jun 6, 2019

That doesn't seem much better than just documenting "call FigureCanvas(Figure()) early"?

@rayosborn
Copy link
Author

I am a little rushed at the moment, so I may be wrong about this, but I think the problem with calling FigureCanvas(Figure()) in NeXpy is that the IPython shell doesn't exist when I initialize the plotting windows, so the call to IPython.get_ipython() would fail. I could probably reconfigure the initialization process I would prefer not to refactor my code if possible, and I think Matplotlib should allow for situations where the IPython fix is completely unnecessary.

@anntzer
Copy link
Contributor

anntzer commented Jun 6, 2019

As it is written right now, FigureCanvas(Figure()) will call _fix_ipython_backend2gui regardless of whether IPython was already initialized, and _fix_ipython_backend2gui will not be called a second time (due to the functools.lru_cache() decorator) even if IPython is later initialized (I didn't think about the embedding case at all when I wrote this). So the fix works (... at least for me) even if you call FigureCanvas(Figure()) at the toplevel of the module.

@rayosborn
Copy link
Author

I must admit that I had been puzzled about how subsequent calls were suppressed. I didn't know what the functools.lru_cache() decorator did. I am happy to leave this to real Matplotlib experts such as you now that I have my own solution and you are aware of the issue. If you reference this issue when any relevant changes are implemented, I should be able to keep my own code compatible. This isn't the first bit of monkey-patching I've had to do - it's one of the hazards of embedding other packages.

tacaswell added a commit to tacaswell/matplotlib that referenced this issue Jun 6, 2019
@tacaswell
Copy link
Member

I propose #14471 as a fix which avoids closing the figures if the backend switch is a no-op.

@anntzer Documenting that import / instantiation order seems a bit brittle.

@rayosborn I would much rather not have you monkey patching (some what selfishly as I suspect I'll be on both sides of that interaction sooner or later ;) ).

@anntzer
Copy link
Contributor

anntzer commented Jun 6, 2019

I agree it's brittle. The problem of #14471 is that I could possibly imaging some people are actually (implicitly?) relying on switch_backend() to call close("all"), but I'll trust your judgment that the backcompat break is fine.

@tacaswell
Copy link
Member

Hmm, I may have miss-understood the source of the behavior change between 3.0 and 3.1. I thought it was a change to switch_backend but reading this thread more carefully (and checking blame), the change is actually the call to ip.enable_matplotlib().

Could we put a guard in _fix_ipython_backend2gui to not make that call if the user has already started using pyplot? That also has problems of more implicit magic....

@rayosborn do you actually want your figures to be known to pyplot?

@anntzer
Copy link
Contributor

anntzer commented Jun 6, 2019

(an important point is that ip.enable_matplotlib calls switch_backend)

@rayosborn
Copy link
Author

@tacaswell the NeXpy figures use a subclass of FigureCanvasQT, which ensures that pyplot at least knows they exist, e.g., they are included in plt.get_fignums, etc. This still works fine. The problem only arises if a user uses pyplot to create a figure from the IPython shell (plt.figure()). This creates a regular plotting window using the Matplotlib QT backend with an incremented figure number. That allows the Matplotlib power user to create their own plots without the restrictions that NeXpy has (e.g., with lots of subplots), so I want to retain that option and allow the two types of plotting windows to co-exist.

@rayosborn
Copy link
Author

My monkey patch is just to disable the redundant IPython call to enable_matplotlib by assigning the IPython shell function to a dummy function that does nothing. I think it's fairly safe because it's a function that should never be called within NeXpy, but I could probably remove it again if Matplotlib manages to avoid calling switch_backend.

@tacaswell
Copy link
Member

which ensures that pyplot at least knows they exist, e.g., they are included in plt.get_fignums, etc

But do you want this behavior?

@tacaswell
Copy link
Member

https://github.com/nexpy/nexpy/blob/8e83badf6d38092cfc6abaf5d53e1a162db8830c/src/nexpy/gui/plotview.py#L289
https://github.com/nexpy/nexpy/blob/8e83badf6d38092cfc6abaf5d53e1a162db8830c/src/nexpy/gui/plotview.py#L564

are the lines that let pyplot know your figures exist. If you dropped those lines the nexpy plots and the plt.xxx plots would live in essentially independent namespaces and would not conflict (ie nexpy windows won't be in plt.get_fignums and will be immune to plt.close('all'))

@rayosborn
Copy link
Author

That's an interesting point. I'll think about that. It might be worth cutting the cord.

@rayosborn
Copy link
Author

@tacaswell, thanks for the suggestion. As you have seen, I already merged a NeXpy PR where I separate the NXPlotView and pyplot figures, and I think it's a big improvement, even without this issue, so it's had a very useful outcome.

tacaswell added a commit to tacaswell/matplotlib that referenced this issue Aug 19, 2019
tacaswell added a commit to tacaswell/matplotlib that referenced this issue Aug 19, 2019
tacaswell added a commit to tacaswell/matplotlib that referenced this issue Aug 24, 2019
@tacaswell tacaswell modified the milestones: v3.2.0, v3.3.0 Sep 5, 2019
tacaswell added a commit to tacaswell/matplotlib that referenced this issue Jan 20, 2020
@QuLogic QuLogic modified the milestones: v3.3.0, v3.4.0 May 22, 2020
tacaswell added a commit to tacaswell/matplotlib that referenced this issue Aug 24, 2020
@QuLogic QuLogic modified the milestones: v3.4.0, v3.5.0 Jan 27, 2021
@QuLogic QuLogic modified the milestones: v3.5.0, v3.6.0 Sep 25, 2021
@QuLogic QuLogic modified the milestones: v3.6.0, v3.7.0 Jul 8, 2022
melissawm pushed a commit to melissawm/matplotlib that referenced this issue Dec 19, 2022
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

4 participants