diff --git a/lib/matplotlib/backends/_backend_tk.py b/lib/matplotlib/backends/_backend_tk.py index 46913dfa8c2a..9132a859f90f 100644 --- a/lib/matplotlib/backends/_backend_tk.py +++ b/lib/matplotlib/backends/_backend_tk.py @@ -538,8 +538,12 @@ def release_zoom(self, event): def set_cursor(self, cursor): window = self.canvas.get_tk_widget().master - window.configure(cursor=cursord[cursor]) - window.update_idletasks() + try: + window.configure(cursor=cursord[cursor]) + except tkinter.TclError: + pass + else: + window.update_idletasks() def _Button(self, text, image_file, toggle, command): image = (tk.PhotoImage(master=self, file=image_file) diff --git a/lib/matplotlib/backends/backend_wx.py b/lib/matplotlib/backends/backend_wx.py index a0f1f75e7c84..97d4fd9b23c9 100644 --- a/lib/matplotlib/backends/backend_wx.py +++ b/lib/matplotlib/backends/backend_wx.py @@ -623,7 +623,9 @@ def gui_repaint(self, drawDC=None, origin='WX'): The 'WXAgg' backend sets origin accordingly. """ _log.debug("%s - gui_repaint()", type(self)) - if self.IsShownOnScreen(): + # The "if self" check avoids a "wrapped C/C++ object has been deleted" + # RuntimeError if doing things after window is closed. + if self and self.IsShownOnScreen(): if not drawDC: # not called from OnPaint use a ClientDC drawDC = wx.ClientDC(self) @@ -881,14 +883,11 @@ def _print_image(self, filename, filetype, *args, **kwargs): # Now that we have rendered into the bitmap, save it to the appropriate # file type and clean up. - if isinstance(filename, str): - if not image.SaveFile(filename, filetype): - raise RuntimeError(f'Could not save figure to {filename}') - elif cbook.is_writable_file_like(filename): - if not isinstance(image, wx.Image): - image = image.ConvertToImage() - if not image.SaveStream(filename, filetype): - raise RuntimeError(f'Could not save figure to {filename}') + if (cbook.is_writable_file_like(filename) and + not isinstance(image, wx.Image)): + image = image.ConvertToImage() + if not image.SaveFile(filename, filetype): + raise RuntimeError(f'Could not save figure to {filename}') # Restore everything to normal self.bitmap = origBitmap @@ -900,7 +899,10 @@ def _print_image(self, filename, filetype, *args, **kwargs): # otherwise. if self._isDrawn: self.draw() - self.Refresh() + # The "if self" check avoids a "wrapped C/C++ object has been deleted" + # RuntimeError if doing things after window is closed. + if self: + self.Refresh() class FigureFrameWx(wx.Frame): diff --git a/lib/matplotlib/tests/test_backends_interactive.py b/lib/matplotlib/tests/test_backends_interactive.py index 4fc6c966dd85..a14530c2cd75 100644 --- a/lib/matplotlib/tests/test_backends_interactive.py +++ b/lib/matplotlib/tests/test_backends_interactive.py @@ -61,6 +61,7 @@ def _get_testable_interactive_backends(): _test_script = """\ import importlib import importlib.util +import io import json import sys from unittest import TestCase @@ -120,7 +121,23 @@ def check_alt_backend(alt_backend): fig.canvas.mpl_connect("draw_event", lambda event: timer.start()) fig.canvas.mpl_connect("close_event", print) +result = io.BytesIO() +fig.savefig(result, format='png') + plt.show() + +# Ensure that the window is really closed. +plt.pause(0.5) + +# Test that saving works after interactive window is closed, but the figure is +# not deleted. +result_after = io.BytesIO() +fig.savefig(result_after, format='png') + +if not backend.startswith('qt5') and sys.platform == 'darwin': + # FIXME: This should be enabled everywhere once Qt5 is fixed on macOS to + # not resize incorrectly. + assert_equal(result.getvalue(), result_after.getvalue()) """ _test_timeout = 10 # Empirically, 1s is not enough on Travis. @@ -134,7 +151,8 @@ def test_interactive_backend(backend, toolbar): proc = subprocess.run( [sys.executable, "-c", _test_script, json.dumps({"toolbar": toolbar})], - env={**os.environ, "MPLBACKEND": backend}, timeout=_test_timeout, + env={**os.environ, "MPLBACKEND": backend, "SOURCE_DATE_EPOCH": "0"}, + timeout=_test_timeout, stdout=subprocess.PIPE, universal_newlines=True) if proc.returncode: pytest.fail("The subprocess returned with non-zero exit status " @@ -148,7 +166,8 @@ def test_interactive_backend(backend, toolbar): def test_webagg(): pytest.importorskip("tornado") proc = subprocess.Popen([sys.executable, "-c", _test_script], - env={**os.environ, "MPLBACKEND": "webagg"}) + env={**os.environ, "MPLBACKEND": "webagg", + "SOURCE_DATE_EPOCH": "0"}) url = "http://{}:{}".format( mpl.rcParams["webagg.address"], mpl.rcParams["webagg.port"]) timeout = time.perf_counter() + _test_timeout