From 0bb70bfcad28df0594c069f769d9a4b4c6bd115d Mon Sep 17 00:00:00 2001 From: Thomas A Caswell Date: Tue, 19 May 2020 23:09:17 -0400 Subject: [PATCH] Backport PR #17391: tk/wx: Fix saving after the window is closed --- lib/matplotlib/backends/_backend_tk.py | 8 ++++-- lib/matplotlib/backends/backend_wx.py | 22 ++++++++------- .../tests/test_backends_interactive.py | 27 ++++++++++++++++--- 3 files changed, 41 insertions(+), 16 deletions(-) diff --git a/lib/matplotlib/backends/_backend_tk.py b/lib/matplotlib/backends/_backend_tk.py index c14ee7262933..5c1716ec2477 100644 --- a/lib/matplotlib/backends/_backend_tk.py +++ b/lib/matplotlib/backends/_backend_tk.py @@ -538,8 +538,12 @@ def release(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, file, command, extension='.gif'): img_file = str(cbook._get_data_path('images', file + extension)) diff --git a/lib/matplotlib/backends/backend_wx.py b/lib/matplotlib/backends/backend_wx.py index 46374ebdcb13..a579b73bf94e 100644 --- a/lib/matplotlib/backends/backend_wx.py +++ b/lib/matplotlib/backends/backend_wx.py @@ -702,7 +702,9 @@ def gui_repaint(self, drawDC=None, origin='WX'): The 'WXAgg' backend sets origin accordingly. """ DEBUG_MSG("gui_repaint()", 1, 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) @@ -978,14 +980,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 @@ -997,7 +996,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() ######################################################################## diff --git a/lib/matplotlib/tests/test_backends_interactive.py b/lib/matplotlib/tests/test_backends_interactive.py index bc3796a41747..7465bb9197e2 100644 --- a/lib/matplotlib/tests/test_backends_interactive.py +++ b/lib/matplotlib/tests/test_backends_interactive.py @@ -52,6 +52,7 @@ def _get_testable_interactive_backends(): _test_script = """\ import importlib import importlib.util +import io import sys from unittest import TestCase @@ -107,7 +108,23 @@ def check_alt_backend(alt_backend): # Trigger quitting upon draw. fig.canvas.mpl_connect("draw_event", lambda event: timer.start()) +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. @@ -115,9 +132,10 @@ def check_alt_backend(alt_backend): @pytest.mark.parametrize("backend", _get_testable_interactive_backends()) @pytest.mark.flaky(reruns=3) def test_interactive_backend(backend): - proc = subprocess.run([sys.executable, "-c", _test_script], - env={**os.environ, "MPLBACKEND": backend}, - timeout=_test_timeout) + proc = subprocess.run( + [sys.executable, "-c", _test_script], + env={**os.environ, "MPLBACKEND": backend, "SOURCE_DATE_EPOCH": "0"}, + timeout=_test_timeout) if proc.returncode: pytest.fail("The subprocess returned with non-zero exit status " f"{proc.returncode}.") @@ -129,7 +147,8 @@ def test_interactive_backend(backend): 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