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.
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.
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.
switch_backend is called via ip.enable_matplotlib(), which is called in FigureCanvasBase._fix_ipython_backend2gui, which is called in the FigureCanvasBase constructor.
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.
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...)
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.
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...
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.
Activity
anntzer commented on 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.
rayosborn commented on Jun 4, 2019
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 commented on 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 commented on Jun 5, 2019
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 commented on 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 commented on Jun 5, 2019
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 commented on 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 commented on 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 commented on Jun 6, 2019
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 commented on Jun 6, 2019
That doesn't seem much better than just documenting "call FigureCanvas(Figure()) early"?
19 remaining items
FIX: don't close figures is switch_backend unless really switching
FIX: don't close figures is switch_backend unless really switching
FIX: don't close figures is switch_backend unless really switching
FIX: don't close figures is switch_backend unless really switching
FIX: don't close figures is switch_backend unless really switching