Skip to content

Commit ff262a1

Browse files
Zybulontacaswell
andauthored
Return filename from save_figure (#27766)
--------- Co-authored-by: Thomas A Caswell <tcaswell@gmail.com>
1 parent 36daea4 commit ff262a1

14 files changed

+119
-8
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
``NavigationToolbar2.save_figure`` now returns filepath of saved figure
2+
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
3+
4+
``NavigationToolbar2.save_figure`` function may return the filename of the saved figure.
5+
6+
If a backend implements this functionality it should return `None`
7+
in the case where no figure is actually saved (because the user closed the dialog without saving).
8+
9+
If the backend does not or can not implement this functionality (currently the Gtk4 backends
10+
and webagg backends do not) this method will return ``NavigationToolbar2.UNKNOWN_SAVED_STATUS``.

lib/matplotlib/backend_bases.py

+22-1
Original file line numberDiff line numberDiff line change
@@ -2780,6 +2780,8 @@ class NavigationToolbar2:
27802780
('Save', 'Save the figure', 'filesave', 'save_figure'),
27812781
)
27822782

2783+
UNKNOWN_SAVED_STATUS = object()
2784+
27832785
def __init__(self, canvas):
27842786
self.canvas = canvas
27852787
canvas.toolbar = self
@@ -3194,7 +3196,26 @@ def on_tool_fig_close(e):
31943196
return self.subplot_tool
31953197

31963198
def save_figure(self, *args):
3197-
"""Save the current figure."""
3199+
"""
3200+
Save the current figure.
3201+
3202+
Backend implementations may choose to return
3203+
the absolute path of the saved file, if any, as
3204+
a string.
3205+
3206+
If no file is created then `None` is returned.
3207+
3208+
If the backend does not implement this functionality
3209+
then `NavigationToolbar2.UNKNOWN_SAVED_STATUS` is returned.
3210+
3211+
Returns
3212+
-------
3213+
str or `NavigationToolbar2.UNKNOWN_SAVED_STATUS` or `None`
3214+
The filepath of the saved figure.
3215+
Returns `None` if figure is not saved.
3216+
Returns `NavigationToolbar2.UNKNOWN_SAVED_STATUS` when
3217+
the backend does not provide the information.
3218+
"""
31983219
raise NotImplementedError
31993220

32003221
def update(self):

lib/matplotlib/backend_bases.pyi

+2-1
Original file line numberDiff line numberDiff line change
@@ -402,6 +402,7 @@ class _Mode(str, Enum):
402402

403403
class NavigationToolbar2:
404404
toolitems: tuple[tuple[str, ...] | tuple[None, ...], ...]
405+
UNKNOWN_SAVED_STATUS: object
405406
canvas: FigureCanvasBase
406407
mode: _Mode
407408
def __init__(self, canvas: FigureCanvasBase) -> None: ...
@@ -437,7 +438,7 @@ class NavigationToolbar2:
437438
def push_current(self) -> None: ...
438439
subplot_tool: widgets.SubplotTool
439440
def configure_subplots(self, *args): ...
440-
def save_figure(self, *args) -> None: ...
441+
def save_figure(self, *args) -> str | None | object: ...
441442
def update(self) -> None: ...
442443
def set_history_buttons(self) -> None: ...
443444

lib/matplotlib/backends/_backend_tk.py

+2-1
Original file line numberDiff line numberDiff line change
@@ -870,7 +870,7 @@ def save_figure(self, *args):
870870
)
871871

872872
if fname in ["", ()]:
873-
return
873+
return None
874874
# Save dir for next time, unless empty str (i.e., use cwd).
875875
if initialdir != "":
876876
mpl.rcParams['savefig.directory'] = (
@@ -885,6 +885,7 @@ def save_figure(self, *args):
885885

886886
try:
887887
self.canvas.figure.savefig(fname, format=extension)
888+
return fname
888889
except Exception as e:
889890
tkinter.messagebox.showerror("Error saving file", str(e))
890891

lib/matplotlib/backends/backend_gtk3.py

+5-4
Original file line numberDiff line numberDiff line change
@@ -339,7 +339,7 @@ def __init__(self, canvas):
339339
def save_figure(self, *args):
340340
dialog = Gtk.FileChooserDialog(
341341
title="Save the figure",
342-
parent=self.canvas.get_toplevel(),
342+
transient_for=self.canvas.get_toplevel(),
343343
action=Gtk.FileChooserAction.SAVE,
344344
buttons=(Gtk.STOCK_CANCEL, Gtk.ResponseType.CANCEL,
345345
Gtk.STOCK_SAVE, Gtk.ResponseType.OK),
@@ -371,16 +371,17 @@ def on_notify_filter(*args):
371371
fmt = self.canvas.get_supported_filetypes_grouped()[ff.get_name()][0]
372372
dialog.destroy()
373373
if response != Gtk.ResponseType.OK:
374-
return
374+
return None
375375
# Save dir for next time, unless empty str (which means use cwd).
376376
if mpl.rcParams['savefig.directory']:
377377
mpl.rcParams['savefig.directory'] = os.path.dirname(fname)
378378
try:
379379
self.canvas.figure.savefig(fname, format=fmt)
380+
return fname
380381
except Exception as e:
381382
dialog = Gtk.MessageDialog(
382-
parent=self.canvas.get_toplevel(), message_format=str(e),
383-
type=Gtk.MessageType.ERROR, buttons=Gtk.ButtonsType.OK)
383+
transient_for=self.canvas.get_toplevel(), text=str(e),
384+
message_type=Gtk.MessageType.ERROR, buttons=Gtk.ButtonsType.OK)
384385
dialog.run()
385386
dialog.destroy()
386387

lib/matplotlib/backends/backend_gtk4.py

+1
Original file line numberDiff line numberDiff line change
@@ -393,6 +393,7 @@ def on_response(dialog, response):
393393
msg.show()
394394

395395
dialog.show()
396+
return self.UNKNOWN_SAVED_STATUS
396397

397398

398399
class ToolbarGTK4(ToolContainerBase, Gtk.Box):

lib/matplotlib/backends/backend_macosx.py

+1
Original file line numberDiff line numberDiff line change
@@ -142,6 +142,7 @@ def save_figure(self, *args):
142142
if mpl.rcParams['savefig.directory']:
143143
mpl.rcParams['savefig.directory'] = os.path.dirname(filename)
144144
self.canvas.figure.savefig(filename)
145+
return filename
145146

146147

147148
class FigureManagerMac(_macosx.FigureManager, FigureManagerBase):

lib/matplotlib/backends/backend_qt.py

+1
Original file line numberDiff line numberDiff line change
@@ -835,6 +835,7 @@ def save_figure(self, *args):
835835
self, "Error saving file", str(e),
836836
QtWidgets.QMessageBox.StandardButton.Ok,
837837
QtWidgets.QMessageBox.StandardButton.NoButton)
838+
return fname
838839

839840
def set_history_buttons(self):
840841
can_backward = self._nav_stack._pos > 0

lib/matplotlib/backends/backend_webagg_core.py

+2-1
Original file line numberDiff line numberDiff line change
@@ -403,8 +403,9 @@ def remove_rubberband(self):
403403
self.canvas.send_event("rubberband", x0=-1, y0=-1, x1=-1, y1=-1)
404404

405405
def save_figure(self, *args):
406-
"""Save the current figure"""
406+
"""Save the current figure."""
407407
self.canvas.send_event('save')
408+
return self.UNKNOWN_SAVED_STATUS
408409

409410
def pan(self):
410411
super().pan()

lib/matplotlib/backends/backend_wx.py

+1
Original file line numberDiff line numberDiff line change
@@ -1143,6 +1143,7 @@ def save_figure(self, *args):
11431143
mpl.rcParams["savefig.directory"] = str(path.parent)
11441144
try:
11451145
self.canvas.figure.savefig(path, format=fmt)
1146+
return path
11461147
except Exception as e:
11471148
dialog = wx.MessageDialog(
11481149
parent=self.canvas.GetParent(), message=str(e),

lib/matplotlib/tests/test_backend_gtk3.py

+26
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
1+
import os
12
from matplotlib import pyplot as plt
23

34
import pytest
5+
from unittest import mock
46

57

68
@pytest.mark.backend("gtk3agg", skip_on_importerror=True)
@@ -46,3 +48,27 @@ def receive(event):
4648
fig.canvas.mpl_connect("draw_event", send)
4749
fig.canvas.mpl_connect("key_press_event", receive)
4850
plt.show()
51+
52+
53+
@pytest.mark.backend("gtk3agg", skip_on_importerror=True)
54+
def test_save_figure_return():
55+
from gi.repository import Gtk
56+
fig, ax = plt.subplots()
57+
ax.imshow([[1]])
58+
with mock.patch("gi.repository.Gtk.FileFilter") as fileFilter:
59+
filt = fileFilter.return_value
60+
filt.get_name.return_value = "Portable Network Graphics"
61+
with mock.patch("gi.repository.Gtk.FileChooserDialog") as dialogChooser:
62+
dialog = dialogChooser.return_value
63+
dialog.get_filter.return_value = filt
64+
dialog.get_filename.return_value = "foobar.png"
65+
dialog.run.return_value = Gtk.ResponseType.OK
66+
fname = fig.canvas.manager.toolbar.save_figure()
67+
os.remove("foobar.png")
68+
assert fname == "foobar.png"
69+
70+
with mock.patch("gi.repository.Gtk.MessageDialog"):
71+
dialog.get_filename.return_value = None
72+
dialog.run.return_value = Gtk.ResponseType.OK
73+
fname = fig.canvas.manager.toolbar.save_figure()
74+
assert fname is None

lib/matplotlib/tests/test_backend_macosx.py

+15
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import os
22

33
import pytest
4+
from unittest import mock
45

56
import matplotlib as mpl
67
import matplotlib.pyplot as plt
@@ -50,3 +51,17 @@ def new_choose_save_file(title, directory, filename):
5051
def test_ipython():
5152
from matplotlib.testing import ipython_in_subprocess
5253
ipython_in_subprocess("osx", {(8, 24): "macosx", (7, 0): "MacOSX"})
54+
55+
56+
@pytest.mark.backend('macosx')
57+
def test_save_figure_return():
58+
fig, ax = plt.subplots()
59+
ax.imshow([[1]])
60+
prop = "matplotlib.backends._macosx.choose_save_file"
61+
with mock.patch(prop, return_value="foobar.png"):
62+
fname = fig.canvas.manager.toolbar.save_figure()
63+
os.remove("foobar.png")
64+
assert fname == "foobar.png"
65+
with mock.patch(prop, return_value=None):
66+
fname = fig.canvas.manager.toolbar.save_figure()
67+
assert fname is None

lib/matplotlib/tests/test_backend_qt.py

+14
Original file line numberDiff line numberDiff line change
@@ -226,6 +226,20 @@ def test_figureoptions():
226226
fig.canvas.manager.toolbar.edit_parameters()
227227

228228

229+
@pytest.mark.backend('QtAgg', skip_on_importerror=True)
230+
def test_save_figure_return():
231+
fig, ax = plt.subplots()
232+
ax.imshow([[1]])
233+
prop = "matplotlib.backends.qt_compat.QtWidgets.QFileDialog.getSaveFileName"
234+
with mock.patch(prop, return_value=("foobar.png", None)):
235+
fname = fig.canvas.manager.toolbar.save_figure()
236+
os.remove("foobar.png")
237+
assert fname == "foobar.png"
238+
with mock.patch(prop, return_value=(None, None)):
239+
fname = fig.canvas.manager.toolbar.save_figure()
240+
assert fname is None
241+
242+
229243
@pytest.mark.backend('QtAgg', skip_on_importerror=True)
230244
def test_figureoptions_with_datetime_axes():
231245
fig, ax = plt.subplots()

lib/matplotlib/tests/test_backend_tk.py

+17
Original file line numberDiff line numberDiff line change
@@ -196,6 +196,23 @@ class Toolbar(NavigationToolbar2Tk):
196196
print("success")
197197

198198

199+
@_isolated_tk_test(success_count=2)
200+
def test_save_figure_return():
201+
import matplotlib.pyplot as plt
202+
from unittest import mock
203+
fig = plt.figure()
204+
prop = "tkinter.filedialog.asksaveasfilename"
205+
with mock.patch(prop, return_value="foobar.png"):
206+
fname = fig.canvas.manager.toolbar.save_figure()
207+
os.remove("foobar.png")
208+
assert fname == "foobar.png"
209+
print("success")
210+
with mock.patch(prop, return_value=""):
211+
fname = fig.canvas.manager.toolbar.save_figure()
212+
assert fname is None
213+
print("success")
214+
215+
199216
@_isolated_tk_test(success_count=1)
200217
def test_canvas_focus():
201218
import tkinter as tk

0 commit comments

Comments
 (0)