Skip to content

Commit a38f9bb

Browse files
committed
Fix: properly decouple from pyplot and specific backends when destroying
When destroying a manager, replace the figure's canvas by a figure canvas base.
1 parent c718eae commit a38f9bb

File tree

10 files changed

+26
-5
lines changed

10 files changed

+26
-5
lines changed

lib/matplotlib/backend_bases.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2723,7 +2723,9 @@ def show(self):
27232723
f"shown")
27242724

27252725
def destroy(self):
2726-
pass
2726+
# managers may have swapped the canvas to a GUI-framework specific one.
2727+
# restore the base canvas when the manager is destroyed.
2728+
self.canvas.figure._set_base_canvas()
27272729

27282730
def full_screen_toggle(self):
27292731
pass

lib/matplotlib/backends/_backend_gtk.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -195,6 +195,7 @@ def destroy(self, *args):
195195
self._destroying = True
196196
self.window.destroy()
197197
self.canvas.destroy()
198+
super().destroy()
198199

199200
@classmethod
200201
def start_main_loop(cls):

lib/matplotlib/backends/_backend_tk.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -606,6 +606,7 @@ def delayed_destroy():
606606
else:
607607
self.window.update()
608608
delayed_destroy()
609+
super().destroy()
609610

610611
def get_window_title(self):
611612
return self.window.wm_title()

lib/matplotlib/backends/backend_gtk3.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -89,6 +89,7 @@ def __init__(self, figure=None):
8989

9090
def destroy(self):
9191
CloseEvent("close_event", self)._process()
92+
super().destroy()
9293

9394
def set_cursor(self, cursor):
9495
# docstring inherited

lib/matplotlib/backends/backend_gtk4.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -90,6 +90,7 @@ def __init__(self, figure=None):
9090

9191
def destroy(self):
9292
CloseEvent("close_event", self)._process()
93+
super().destroy()
9394

9495
def set_cursor(self, cursor):
9596
# docstring inherited

lib/matplotlib/backends/backend_nbagg.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -137,6 +137,7 @@ def _create_comm(self):
137137
return comm
138138

139139
def destroy(self):
140+
super().destroy()
140141
self._send_event('close')
141142
# need to copy comms as callbacks will modify this list
142143
for comm in list(self.web_sockets):

lib/matplotlib/backends/backend_qt.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -664,6 +664,7 @@ def show(self):
664664
self.window.raise_()
665665

666666
def destroy(self, *args):
667+
super().destroy()
667668
# check for qApp first, as PySide deletes it in its atexit handler
668669
if QtWidgets.QApplication.instance() is None:
669670
return

lib/matplotlib/backends/backend_wx.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1007,6 +1007,7 @@ def show(self):
10071007
def destroy(self, *args):
10081008
# docstring inherited
10091009
_log.debug("%s - destroy()", type(self))
1010+
super().destroy()
10101011
frame = self.frame
10111012
if frame: # Else, may have been already deleted, e.g. when closing.
10121013
# As this can be called from non-GUI thread from plt.close use

lib/matplotlib/figure.py

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2642,7 +2642,7 @@ def __init__(self,
26422642
self._set_artist_props(self.patch)
26432643
self.patch.set_antialiased(False)
26442644

2645-
FigureCanvasBase(self) # Set self.canvas.
2645+
self._set_base_canvas()
26462646

26472647
if subplotpars is None:
26482648
subplotpars = SubplotParams()
@@ -2996,6 +2996,15 @@ def get_constrained_layout_pads(self, relative=False):
29962996

29972997
return w_pad, h_pad, wspace, hspace
29982998

2999+
def _set_base_canvas(self):
3000+
"""
3001+
Initialize self.canvas with a FigureCanvasBase instance.
3002+
3003+
This is used upon initialization of the Figure, but also
3004+
to reset the canvas when decoupling from pyplot.
3005+
"""
3006+
FigureCanvasBase(self) # Set self.canvas as a side-effect
3007+
29993008
def set_canvas(self, canvas):
30003009
"""
30013010
Set the canvas that contains the figure

lib/matplotlib/tests/test_backends_interactive.py

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -227,12 +227,15 @@ def check_alt_backend(alt_backend):
227227
# Ensure that the window is really closed.
228228
plt.pause(0.5)
229229

230-
# Test that saving works after interactive window is closed, but the figure
231-
# is not deleted.
230+
# When the figure is closed, it's manager is removed and the canvas is reset to
231+
# FigureCanvasBase. Saving should still be possible.
232232
result_after = io.BytesIO()
233233
fig.savefig(result_after, format='png')
234234

235-
assert result.getvalue() == result_after.getvalue()
235+
if backend.endswith("agg"):
236+
# agg-based interactive backends should save the same image as a non-interactive
237+
# figure
238+
assert result.getvalue() == result_after.getvalue()
236239

237240

238241
@pytest.mark.parametrize("env", _get_testable_interactive_backends())

0 commit comments

Comments
 (0)