Skip to content

Commit bfe34be

Browse files
committed
Add a backend kwarg to savefig.
This makes it easier e.g. to test the pgf backend without globally switching the backend.
1 parent 1c6ee08 commit bfe34be

File tree

7 files changed

+70
-15
lines changed

7 files changed

+70
-15
lines changed
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
``savefig()`` gained a ``backend`` keyword argument
2+
---------------------------------------------------
3+
4+
The ``backend`` keyword argument to ``savefig`` can now be used to pick the
5+
rendering backend without having to globally set the backend; e.g. one can save
6+
pdfs using the pgf backend with ``savefig("file.pdf", backend="pgf")``.

lib/matplotlib/backend_bases.py

Lines changed: 35 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1951,18 +1951,35 @@ def get_supported_filetypes_grouped(cls):
19511951
groupings[name].sort()
19521952
return groupings
19531953

1954-
def _get_output_canvas(self, fmt):
1954+
def _get_output_canvas(self, backend, fmt):
19551955
"""
1956-
Return a canvas suitable for saving figures to a specified file format.
1956+
Set the canvas in preparation for saving the figure.
19571957
1958-
If necessary, this function will switch to a registered backend that
1959-
supports the format.
1960-
"""
1961-
# Return the current canvas if it supports the requested format.
1962-
if hasattr(self, 'print_{}'.format(fmt)):
1958+
Parameters
1959+
----------
1960+
backend : str or None
1961+
If not None, switch the figure canvas to the ``FigureCanvas`` class
1962+
of the given backend.
1963+
fmt : str
1964+
If *backend* is None, then determine a suitable canvas class for
1965+
saving to format *fmt* -- either the current canvas class, if it
1966+
supports *fmt*, or whatever `get_registered_canvas_class` returns;
1967+
switch the figure canvas to that canvas class.
1968+
"""
1969+
if backend is not None:
1970+
# Return a specific canvas class, if requested.
1971+
canvas_class = (
1972+
importlib.import_module(cbook._backend_module_name(backend))
1973+
.FigureCanvas)
1974+
if not hasattr(canvas_class, f"print_{fmt}"):
1975+
raise ValueError(
1976+
f"The {backend!r} backend does not support {fmt} output")
1977+
elif hasattr(self, f"print_{fmt}"):
1978+
# Return the current canvas if it supports the requested format.
19631979
return self
1964-
# Return a default canvas for the requested format, if it exists.
1965-
canvas_class = get_registered_canvas_class(fmt)
1980+
else:
1981+
# Return a default canvas for the requested format, if it exists.
1982+
canvas_class = get_registered_canvas_class(fmt)
19661983
if canvas_class:
19671984
return self.switch_backends(canvas_class)
19681985
# Else report error for unsupported format.
@@ -1972,7 +1989,7 @@ def _get_output_canvas(self, fmt):
19721989

19731990
def print_figure(self, filename, dpi=None, facecolor=None, edgecolor=None,
19741991
orientation='portrait', format=None,
1975-
*, bbox_inches=None, **kwargs):
1992+
*, bbox_inches=None, backend=None, **kwargs):
19761993
"""
19771994
Render the figure to hardcopy. Set the figure patch face and edge
19781995
colors. This is useful because some of the GUIs have a gray figure
@@ -2012,6 +2029,13 @@ def print_figure(self, filename, dpi=None, facecolor=None, edgecolor=None,
20122029
A list of extra artists that will be considered when the
20132030
tight bbox is calculated.
20142031
2032+
backend : str, optional
2033+
Use a non-default backend to render the file, e.g. to render a
2034+
png file with the "cairo" backend rather than the default "agg",
2035+
or a pdf file with the "pgf" backend rather than the default
2036+
"pdf". Note that the default backend is normally sufficient. See
2037+
the-builtin-backends_ for a list of valid backends for each file
2038+
format. Custom backends can be referenced as "module://...".
20152039
"""
20162040
if format is None:
20172041
# get format from filename, or from backend's default filetype
@@ -2026,7 +2050,7 @@ def print_figure(self, filename, dpi=None, facecolor=None, edgecolor=None,
20262050
format = format.lower()
20272051

20282052
# get canvas object and print method for format
2029-
canvas = self._get_output_canvas(format)
2053+
canvas = self._get_output_canvas(backend, format)
20302054
print_method = getattr(canvas, 'print_%s' % format)
20312055

20322056
if dpi is None:

lib/matplotlib/cbook/__init__.py

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2212,3 +2212,12 @@ def __init__(self, fget):
22122212

22132213
def __get__(self, instance, owner):
22142214
return self._fget(owner)
2215+
2216+
2217+
def _backend_module_name(name):
2218+
"""
2219+
Convert a backend name (either a standard backend -- "Agg", "TkAgg", ... --
2220+
or a custom backend -- "module://...") to the corresponding module name).
2221+
"""
2222+
return (name[9:] if name.startswith("module://")
2223+
else "matplotlib.backends.backend_{}".format(name.lower()))

lib/matplotlib/figure.py

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2131,6 +2131,14 @@ def savefig(self, fname, *, transparent=None, **kwargs):
21312131
A list of extra artists that will be considered when the
21322132
tight bbox is calculated.
21332133
2134+
backend : str, optional
2135+
Use a non-default backend to render the file, e.g. to render a
2136+
png file with the "cairo" backend rather than the default "agg",
2137+
or a pdf file with the "pgf" backend rather than the default
2138+
"pdf". Note that the default backend is normally sufficient. See
2139+
the-builtin-backends_ for a list of valid backends for each file
2140+
format. Custom backends can be referenced as "module://...".
2141+
21342142
metadata : dict, optional
21352143
Key/value pairs to store in the image metadata. The supported keys
21362144
and defaults depend on the image format and backend:

lib/matplotlib/pyplot.py

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -212,10 +212,7 @@ def switch_backend(newbackend):
212212
rcParamsOrig["backend"] = "agg"
213213
return
214214

215-
backend_name = (
216-
newbackend[9:] if newbackend.startswith("module://")
217-
else "matplotlib.backends.backend_{}".format(newbackend.lower()))
218-
215+
backend_name = cbook._backend_module_name(newbackend)
219216
backend_mod = importlib.import_module(backend_name)
220217
Backend = type(
221218
"Backend", (matplotlib.backend_bases._Backend,), vars(backend_mod))

lib/matplotlib/tests/test_figure.py

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -391,6 +391,16 @@ def test_savefig():
391391
fig.savefig("fname1.png", "fname2.png")
392392

393393

394+
def test_savefig_backend():
395+
fig = plt.figure()
396+
# Intentionally use an invalid module name.
397+
with pytest.raises(ModuleNotFoundError, match="No module named '@absent'"):
398+
fig.savefig("test", backend="module://@absent")
399+
with pytest.raises(ValueError,
400+
match="The 'pdf' backend does not support png output"):
401+
fig.savefig("test.png", backend="pdf")
402+
403+
394404
def test_figure_repr():
395405
fig = plt.figure(figsize=(10, 20), dpi=10)
396406
assert repr(fig) == "<Figure size 100x200 with 0 Axes>"

tutorials/introductory/usage.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -350,6 +350,7 @@ def my_plotter(ax, data1, data2, param_dict):
350350
# use a different backend. Therefore, you should avoid explicitly calling
351351
# `~matplotlib.use` unless absolutely necessary.
352352
#
353+
# .. _the-builtin-backends:
353354
#
354355
# The builtin backends
355356
# --------------------

0 commit comments

Comments
 (0)