Skip to content

tk/wx: Fix saving after the window is closed #17391

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

Merged
merged 5 commits into from
May 20, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 6 additions & 2 deletions lib/matplotlib/backends/_backend_tk.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
22 changes: 12 additions & 10 deletions lib/matplotlib/backends/backend_wx.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -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
Expand All @@ -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):
Expand Down
23 changes: 21 additions & 2 deletions lib/matplotlib/tests/test_backends_interactive.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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)
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

BTW, if anyone can think of a better way to do this, I'd change it.

I tried fig.canvas.flush_events(), but that crashes because the main window is gone. If you ignore any crashes, then the test doesn't fail like it should on master.


# 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.

Expand All @@ -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 "
Expand All @@ -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
Expand Down