Thanks to visit codestin.com
Credit goes to github.com

Skip to content

Commit 7b76234

Browse files
authored
Merge pull request #17223 from QuLogic/savefig-extra-kwargs
Warn on invalid savefig keyword arguments
2 parents c8e7865 + 853d8c9 commit 7b76234

9 files changed

+125
-34
lines changed

lib/matplotlib/backend_bases.py

Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,11 +29,14 @@
2929
from enum import Enum, IntEnum
3030
import functools
3131
import importlib
32+
import inspect
3233
import io
3334
import logging
3435
import os
36+
import re
3537
import sys
3638
import time
39+
import traceback
3740
from weakref import WeakKeyDictionary
3841

3942
import numpy as np
@@ -1567,6 +1570,70 @@ def _is_non_interactive_terminal_ipython(ip):
15671570
and getattr(ip.parent, 'interact', None) is False)
15681571

15691572

1573+
def _check_savefig_extra_args(func=None, extra_kwargs=()):
1574+
"""
1575+
Decorator for the final print_* methods that accept keyword arguments.
1576+
1577+
If any unused keyword arguments are left, this decorator will warn about
1578+
them, and as part of the warning, will attempt to specify the function that
1579+
the user actually called, instead of the backend-specific method. If unable
1580+
to determine which function the user called, it will specify `.savefig`.
1581+
1582+
For compatibility across backends, this does not warn about keyword
1583+
arguments added by `FigureCanvasBase.print_figure` for use in a subset of
1584+
backends, because the user would not have added them directly.
1585+
"""
1586+
1587+
if func is None:
1588+
return functools.partial(_check_savefig_extra_args,
1589+
extra_kwargs=extra_kwargs)
1590+
1591+
old_sig = inspect.signature(func)
1592+
1593+
@functools.wraps(func)
1594+
def wrapper(*args, **kwargs):
1595+
name = 'savefig' # Reasonable default guess.
1596+
public_api = re.compile(r'^savefig|print_[A-Za-z0-9]+$')
1597+
seen_print_figure = False
1598+
for frame, line in traceback.walk_stack(None):
1599+
if frame is None:
1600+
# when called in embedded context may hit frame is None.
1601+
break
1602+
if re.match(r'\A(matplotlib|mpl_toolkits)(\Z|\.(?!tests\.))',
1603+
# Work around sphinx-gallery not setting __name__.
1604+
frame.f_globals.get('__name__', '')):
1605+
if public_api.match(frame.f_code.co_name):
1606+
name = frame.f_code.co_name
1607+
if name == 'print_figure':
1608+
seen_print_figure = True
1609+
else:
1610+
break
1611+
1612+
accepted_kwargs = {*old_sig.parameters, *extra_kwargs}
1613+
if seen_print_figure:
1614+
for kw in ['dpi', 'facecolor', 'edgecolor', 'orientation',
1615+
'bbox_inches_restore']:
1616+
# Ignore keyword arguments that are passed in by print_figure
1617+
# for the use of other renderers.
1618+
if kw not in accepted_kwargs:
1619+
kwargs.pop(kw, None)
1620+
1621+
for arg in list(kwargs):
1622+
if arg in accepted_kwargs:
1623+
continue
1624+
cbook.warn_deprecated(
1625+
'3.3', name=name,
1626+
message='%(name)s() got unexpected keyword argument "'
1627+
+ arg + '" which is no longer supported as of '
1628+
'%(since)s and will become an error '
1629+
'%(removal)s')
1630+
kwargs.pop(arg)
1631+
1632+
return func(*args, **kwargs)
1633+
1634+
return wrapper
1635+
1636+
15701637
class FigureCanvasBase:
15711638
"""
15721639
The canvas the figure renders into.

lib/matplotlib/backends/backend_agg.py

Lines changed: 11 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,8 @@
3838
from matplotlib import cbook
3939
from matplotlib import colors as mcolors
4040
from matplotlib.backend_bases import (
41-
_Backend, FigureCanvasBase, FigureManagerBase, RendererBase)
41+
_Backend, _check_savefig_extra_args, FigureCanvasBase, FigureManagerBase,
42+
RendererBase)
4243
from matplotlib.font_manager import findfont, get_font
4344
from matplotlib.ft2font import (LOAD_FORCE_AUTOHINT, LOAD_NO_HINTING,
4445
LOAD_DEFAULT, LOAD_NO_AUTOHINT)
@@ -447,17 +448,18 @@ def buffer_rgba(self):
447448
"""
448449
return self.renderer.buffer_rgba()
449450

450-
def print_raw(self, filename_or_obj, *args, **kwargs):
451+
@_check_savefig_extra_args
452+
def print_raw(self, filename_or_obj, *args):
451453
FigureCanvasAgg.draw(self)
452454
renderer = self.get_renderer()
453455
with cbook.open_file_cm(filename_or_obj, "wb") as fh:
454456
fh.write(renderer.buffer_rgba())
455457

456458
print_rgba = print_raw
457459

460+
@_check_savefig_extra_args
458461
def print_png(self, filename_or_obj, *args,
459-
metadata=None, pil_kwargs=None,
460-
**kwargs):
462+
metadata=None, pil_kwargs=None):
461463
"""
462464
Write the figure to a PNG file.
463465
@@ -519,6 +521,8 @@ def print_to_buffer(self):
519521
# print_figure(), and the latter ensures that `self.figure.dpi` already
520522
# matches the dpi kwarg (if any).
521523

524+
@_check_savefig_extra_args(
525+
extra_kwargs=["quality", "optimize", "progressive"])
522526
@cbook._delete_parameter("3.2", "dryrun")
523527
@cbook._delete_parameter("3.3", "quality",
524528
alternative="pil_kwargs={'quality': ...}")
@@ -572,7 +576,7 @@ def print_jpg(self, filename_or_obj, *args, dryrun=False, pil_kwargs=None,
572576
pil_kwargs = {}
573577
for k in ["quality", "optimize", "progressive"]:
574578
if k in kwargs:
575-
pil_kwargs.setdefault(k, kwargs[k])
579+
pil_kwargs.setdefault(k, kwargs.pop(k))
576580
if "quality" not in pil_kwargs:
577581
quality = pil_kwargs["quality"] = \
578582
dict.__getitem__(mpl.rcParams, "savefig.jpeg_quality")
@@ -590,9 +594,9 @@ def print_jpg(self, filename_or_obj, *args, dryrun=False, pil_kwargs=None,
590594

591595
print_jpeg = print_jpg
592596

597+
@_check_savefig_extra_args
593598
@cbook._delete_parameter("3.2", "dryrun")
594-
def print_tif(self, filename_or_obj, *args, dryrun=False, pil_kwargs=None,
595-
**kwargs):
599+
def print_tif(self, filename_or_obj, *, dryrun=False, pil_kwargs=None):
596600
FigureCanvasAgg.draw(self)
597601
if dryrun:
598602
return

lib/matplotlib/backends/backend_cairo.py

Lines changed: 8 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -26,8 +26,8 @@
2626

2727
from .. import cbook, font_manager
2828
from matplotlib.backend_bases import (
29-
_Backend, FigureCanvasBase, FigureManagerBase, GraphicsContextBase,
30-
RendererBase)
29+
_Backend, _check_savefig_extra_args, FigureCanvasBase, FigureManagerBase,
30+
GraphicsContextBase, RendererBase)
3131
from matplotlib.font_manager import ttfFontProperty
3232
from matplotlib.mathtext import MathTextParser
3333
from matplotlib.path import Path
@@ -444,10 +444,12 @@ def restore_region(self, region):
444444
surface.mark_dirty_rectangle(
445445
slx.start, sly.start, slx.stop - slx.start, sly.stop - sly.start)
446446

447-
def print_png(self, fobj, *args, **kwargs):
447+
@_check_savefig_extra_args
448+
def print_png(self, fobj):
448449
self._get_printed_image_surface().write_to_png(fobj)
449450

450-
def print_rgba(self, fobj, *args, **kwargs):
451+
@_check_savefig_extra_args
452+
def print_rgba(self, fobj):
451453
width, height = self.get_width_height()
452454
buf = self._get_printed_image_surface().get_data()
453455
fobj.write(cbook._premultiplied_argb32_to_unmultiplied_rgba8888(
@@ -476,7 +478,8 @@ def print_svg(self, fobj, *args, **kwargs):
476478
def print_svgz(self, fobj, *args, **kwargs):
477479
return self._save(fobj, 'svgz', *args, **kwargs)
478480

479-
def _save(self, fo, fmt, *, orientation='portrait', **kwargs):
481+
@_check_savefig_extra_args
482+
def _save(self, fo, fmt, *, orientation='portrait'):
480483
# save PDF/PS/SVG
481484

482485
dpi = 72

lib/matplotlib/backends/backend_pdf.py

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -26,8 +26,8 @@
2626
from matplotlib import _text_layout, cbook
2727
from matplotlib._pylab_helpers import Gcf
2828
from matplotlib.backend_bases import (
29-
_Backend, FigureCanvasBase, FigureManagerBase, GraphicsContextBase,
30-
RendererBase)
29+
_Backend, _check_savefig_extra_args, FigureCanvasBase, FigureManagerBase,
30+
GraphicsContextBase, RendererBase)
3131
from matplotlib.backends.backend_mixed import MixedModeRenderer
3232
from matplotlib.figure import Figure
3333
from matplotlib.font_manager import findfont, is_opentype_cff_font, get_font
@@ -2573,10 +2573,11 @@ class FigureCanvasPdf(FigureCanvasBase):
25732573
def get_default_filetype(self):
25742574
return 'pdf'
25752575

2576+
@_check_savefig_extra_args
25762577
def print_pdf(self, filename, *,
25772578
dpi=72, # dpi to use for images
2578-
bbox_inches_restore=None, metadata=None,
2579-
**kwargs):
2579+
bbox_inches_restore=None, metadata=None):
2580+
25802581
self.figure.set_dpi(72) # there are 72 pdf points to an inch
25812582
width, height = self.figure.get_size_inches()
25822583
if isinstance(filename, PdfPages):

lib/matplotlib/backends/backend_pgf.py

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -18,8 +18,8 @@
1818
import matplotlib as mpl
1919
from matplotlib import cbook, font_manager as fm
2020
from matplotlib.backend_bases import (
21-
_Backend, FigureCanvasBase, FigureManagerBase, GraphicsContextBase,
22-
RendererBase)
21+
_Backend, _check_savefig_extra_args, FigureCanvasBase, FigureManagerBase,
22+
GraphicsContextBase, RendererBase)
2323
from matplotlib.backends.backend_mixed import MixedModeRenderer
2424
from matplotlib.backends.backend_pdf import (
2525
_create_pdf_info_dict, _datetime_to_pdf)
@@ -802,9 +802,11 @@ class FigureCanvasPgf(FigureCanvasBase):
802802
def get_default_filetype(self):
803803
return 'pdf'
804804

805+
@_check_savefig_extra_args
805806
@cbook._delete_parameter("3.2", "dryrun")
806-
def _print_pgf_to_fh(self, fh, *args,
807-
dryrun=False, bbox_inches_restore=None, **kwargs):
807+
def _print_pgf_to_fh(self, fh, *,
808+
dryrun=False, bbox_inches_restore=None):
809+
808810
if dryrun:
809811
renderer = RendererPgf(self.figure, None, dummy=True)
810812
self.figure.draw(renderer)

lib/matplotlib/backends/backend_ps.py

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -21,8 +21,8 @@
2121
from matplotlib import cbook, _path
2222
from matplotlib import _text_layout
2323
from matplotlib.backend_bases import (
24-
_Backend, FigureCanvasBase, FigureManagerBase, GraphicsContextBase,
25-
RendererBase)
24+
_Backend, _check_savefig_extra_args, FigureCanvasBase, FigureManagerBase,
25+
GraphicsContextBase, RendererBase)
2626
from matplotlib.cbook import is_writable_file_like, file_requires_unicode
2727
from matplotlib.font_manager import is_opentype_cff_font, get_font
2828
from matplotlib.ft2font import LOAD_NO_HINTING
@@ -808,11 +808,12 @@ def _print_ps(
808808
printer(outfile, format, dpi=dpi, dsc_comments=dsc_comments,
809809
orientation=orientation, papertype=papertype, **kwargs)
810810

811+
@_check_savefig_extra_args
811812
@cbook._delete_parameter("3.2", "dryrun")
812813
def _print_figure(
813814
self, outfile, format, *,
814815
dpi, dsc_comments, orientation, papertype,
815-
dryrun=False, bbox_inches_restore=None, **kwargs):
816+
dryrun=False, bbox_inches_restore=None):
816817
"""
817818
Render the figure to a filesystem path or a file-like object.
818819
@@ -985,11 +986,12 @@ def print_figure_impl(fh):
985986
with open(outfile, 'w', encoding='latin-1') as fh:
986987
print_figure_impl(fh)
987988

989+
@_check_savefig_extra_args
988990
@cbook._delete_parameter("3.2", "dryrun")
989991
def _print_figure_tex(
990992
self, outfile, format, *,
991993
dpi, dsc_comments, orientation, papertype,
992-
dryrun=False, bbox_inches_restore=None, **kwargs):
994+
dryrun=False, bbox_inches_restore=None):
993995
"""
994996
If :rc:`text.usetex` is True, a temporary pair of tex/eps files
995997
are created to allow tex to manage the text layout via the PSFrags

lib/matplotlib/backends/backend_svg.py

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,8 @@
1616
import matplotlib as mpl
1717
from matplotlib import cbook
1818
from matplotlib.backend_bases import (
19-
_Backend, FigureCanvasBase, FigureManagerBase, RendererBase)
19+
_Backend, _check_savefig_extra_args, FigureCanvasBase, FigureManagerBase,
20+
RendererBase)
2021
from matplotlib.backends.backend_mixed import MixedModeRenderer
2122
from matplotlib.colors import rgb2hex
2223
from matplotlib.dates import UTC
@@ -1328,8 +1329,9 @@ def print_svgz(self, filename, *args, **kwargs):
13281329
gzip.GzipFile(mode='w', fileobj=fh) as gzipwriter:
13291330
return self.print_svg(gzipwriter)
13301331

1332+
@_check_savefig_extra_args
13311333
def _print_svg(self, filename, fh, *, dpi=72, bbox_inches_restore=None,
1332-
metadata=None, **kwargs):
1334+
metadata=None):
13331335
self.figure.set_dpi(72.0)
13341336
width, height = self.figure.get_size_inches()
13351337
w, h = width * 72, height * 72

lib/matplotlib/backends/backend_wx.py

Lines changed: 9 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -18,9 +18,9 @@
1818

1919
import matplotlib as mpl
2020
from matplotlib.backend_bases import (
21-
_Backend, FigureCanvasBase, FigureManagerBase, GraphicsContextBase,
22-
MouseButton, NavigationToolbar2, RendererBase, StatusbarBase, TimerBase,
23-
ToolContainerBase, cursors)
21+
_Backend, _check_savefig_extra_args, FigureCanvasBase, FigureManagerBase,
22+
GraphicsContextBase, MouseButton, NavigationToolbar2, RendererBase,
23+
StatusbarBase, TimerBase, ToolContainerBase, cursors)
2424

2525
from matplotlib import cbook, backend_tools
2626
from matplotlib._pylab_helpers import Gcf
@@ -861,7 +861,8 @@ def print_tiff(self, filename, *args, **kwargs):
861861
def print_xpm(self, filename, *args, **kwargs):
862862
return self._print_image(filename, wx.BITMAP_TYPE_XPM, *args, **kwargs)
863863

864-
def _print_image(self, filename, filetype, *args, **kwargs):
864+
@_check_savefig_extra_args
865+
def _print_image(self, filename, filetype, *, quality=None):
865866
origBitmap = self.bitmap
866867

867868
self.bitmap = wx.Bitmap(math.ceil(self.figure.bbox.width),
@@ -878,11 +879,11 @@ def _print_image(self, filename, filetype, *args, **kwargs):
878879
# are saving a JPEG, convert the wx.Bitmap to a wx.Image,
879880
# and set the quality.
880881
if filetype == wx.BITMAP_TYPE_JPEG:
881-
jpeg_quality = kwargs.get(
882-
'quality',
883-
dict.__getitem__(mpl.rcParams, 'savefig.jpeg_quality'))
882+
if quality is None:
883+
quality = dict.__getitem__(mpl.rcParams,
884+
'savefig.jpeg_quality')
884885
image = self.bitmap.ConvertToImage()
885-
image.SetOption(wx.IMAGE_OPTION_QUALITY, str(jpeg_quality))
886+
image.SetOption(wx.IMAGE_OPTION_QUALITY, str(quality))
886887

887888
# Now that we have rendered into the bitmap, save it to the appropriate
888889
# file type and clean up.

lib/matplotlib/tests/test_figure.py

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
from datetime import datetime
2+
import io
23
from pathlib import Path
34
import platform
45
from types import SimpleNamespace
@@ -423,6 +424,14 @@ def test_savefig():
423424
fig.savefig("fname1.png", "fname2.png")
424425

425426

427+
def test_savefig_warns():
428+
fig = plt.figure()
429+
msg = r'savefig\(\) got unexpected keyword argument "non_existent_kwarg"'
430+
for format in ['png', 'pdf', 'svg', 'tif', 'jpg']:
431+
with pytest.warns(cbook.MatplotlibDeprecationWarning, match=msg):
432+
fig.savefig(io.BytesIO(), format=format, non_existent_kwarg=True)
433+
434+
426435
def test_savefig_backend():
427436
fig = plt.figure()
428437
# Intentionally use an invalid module name.

0 commit comments

Comments
 (0)