3434import io
3535import logging
3636import os
37- import re
3837import sys
3938import time
40- import traceback
4139from weakref import WeakKeyDictionary
4240
4341import numpy as np
@@ -1534,14 +1532,14 @@ class Done(Exception):
15341532
15351533 def _draw (renderer ): raise Done (renderer )
15361534
1537- with cbook ._setattr_cm (figure , draw = _draw ):
1535+ with cbook ._setattr_cm (figure , draw = _draw ), ExitStack () as stack :
15381536 orig_canvas = figure .canvas
15391537 if print_method is None :
15401538 fmt = figure .canvas .get_default_filetype ()
15411539 # Even for a canvas' default output type, a canvas switch may be
15421540 # needed, e.g. for FigureCanvasBase.
1543- print_method = getattr (
1544- figure .canvas ._get_output_canvas ( None , fmt ), f"print_ { fmt } " )
1541+ print_method = stack . enter_context (
1542+ figure .canvas ._switching_canvas_and_get_print_method ( fmt ))
15451543 try :
15461544 print_method (io .BytesIO ())
15471545 except Done as exc :
@@ -1550,8 +1548,6 @@ def _draw(renderer): raise Done(renderer)
15501548 else :
15511549 raise RuntimeError (f"{ print_method } did not call Figure.draw, so "
15521550 f"no renderer is available" )
1553- finally :
1554- figure .canvas = orig_canvas
15551551
15561552
15571553def _no_output_draw (figure ):
@@ -1574,79 +1570,6 @@ def _is_non_interactive_terminal_ipython(ip):
15741570 and getattr (ip .parent , 'interact' , None ) is False )
15751571
15761572
1577- def _check_savefig_extra_args (func = None , extra_kwargs = ()):
1578- """
1579- Decorator for the final print_* methods that accept keyword arguments.
1580-
1581- If any unused keyword arguments are left, this decorator will warn about
1582- them, and as part of the warning, will attempt to specify the function that
1583- the user actually called, instead of the backend-specific method. If unable
1584- to determine which function the user called, it will specify `.savefig`.
1585-
1586- For compatibility across backends, this does not warn about keyword
1587- arguments added by `FigureCanvasBase.print_figure` for use in a subset of
1588- backends, because the user would not have added them directly.
1589- """
1590-
1591- if func is None :
1592- return functools .partial (_check_savefig_extra_args ,
1593- extra_kwargs = extra_kwargs )
1594-
1595- old_sig = inspect .signature (func )
1596-
1597- @functools .wraps (func )
1598- def wrapper (* args , ** kwargs ):
1599- name = 'savefig' # Reasonable default guess.
1600- public_api = re .compile (
1601- r'^savefig|print_[A-Za-z0-9]+|_no_output_draw$'
1602- )
1603- seen_print_figure = False
1604- for frame , line in traceback .walk_stack (None ):
1605- if frame is None :
1606- # when called in embedded context may hit frame is None.
1607- break
1608- # Work around sphinx-gallery not setting __name__.
1609- frame_name = frame .f_globals .get ('__name__' , '' )
1610- if re .match (r'\A(matplotlib|mpl_toolkits)(\Z|\.(?!tests\.))' ,
1611- frame_name ):
1612- name = frame .f_code .co_name
1613- if public_api .match (name ):
1614- if name in ('print_figure' , '_no_output_draw' ):
1615- seen_print_figure = True
1616-
1617- elif frame_name == '_functools' :
1618- # PyPy adds an extra frame without module prefix for this
1619- # functools wrapper, which we ignore to assume we're still in
1620- # Matplotlib code.
1621- continue
1622- else :
1623- break
1624-
1625- accepted_kwargs = {* old_sig .parameters , * extra_kwargs }
1626- if seen_print_figure :
1627- for kw in ['dpi' , 'facecolor' , 'edgecolor' , 'orientation' ,
1628- 'bbox_inches_restore' ]:
1629- # Ignore keyword arguments that are passed in by print_figure
1630- # for the use of other renderers.
1631- if kw not in accepted_kwargs :
1632- kwargs .pop (kw , None )
1633-
1634- for arg in list (kwargs ):
1635- if arg in accepted_kwargs :
1636- continue
1637- _api .warn_deprecated (
1638- '3.3' , name = name ,
1639- message = '%(name)s() got unexpected keyword argument "'
1640- + arg + '" which is no longer supported as of '
1641- '%(since)s and will become an error '
1642- '%(removal)s' )
1643- kwargs .pop (arg )
1644-
1645- return func (* args , ** kwargs )
1646-
1647- return wrapper
1648-
1649-
16501573class FigureCanvasBase :
16511574 """
16521575 The canvas the figure renders into.
@@ -2150,21 +2073,30 @@ def get_supported_filetypes_grouped(cls):
21502073 groupings [name ].sort ()
21512074 return groupings
21522075
2153- def _get_output_canvas (self , backend , fmt ):
2076+ @contextmanager
2077+ def _switching_canvas_and_get_print_method (self , fmt , backend = None ):
21542078 """
2155- Set the canvas in preparation for saving the figure.
2079+ Context manager temporarily setting the canvas for saving the figure::
2080+
2081+ with canvas._switching_canvas_and_get_print_method(fmt, backend) \\
2082+ as print_method:
2083+ # ``print_method`` is a suitable ``print_{fmt}`` method, and
2084+ # the figure's canvas is temporarily switched to the method's
2085+ # canvas within the with... block. ``print_method`` is also
2086+ # wrapped to suppress extra kwargs passed by ``print_figure``.
21562087
21572088 Parameters
21582089 ----------
2159- backend : str or None
2160- If not None, switch the figure canvas to the ``FigureCanvas`` class
2161- of the given backend.
21622090 fmt : str
21632091 If *backend* is None, then determine a suitable canvas class for
21642092 saving to format *fmt* -- either the current canvas class, if it
21652093 supports *fmt*, or whatever `get_registered_canvas_class` returns;
21662094 switch the figure canvas to that canvas class.
2095+ backend : str or None, default: None
2096+ If not None, switch the figure canvas to the ``FigureCanvas`` class
2097+ of the given backend.
21672098 """
2099+ canvas = None
21682100 if backend is not None :
21692101 # Return a specific canvas class, if requested.
21702102 canvas_class = (
@@ -2175,16 +2107,34 @@ def _get_output_canvas(self, backend, fmt):
21752107 f"The { backend !r} backend does not support { fmt } output" )
21762108 elif hasattr (self , f"print_{ fmt } " ):
21772109 # Return the current canvas if it supports the requested format.
2178- return self
2110+ canvas = self
2111+ canvas_class = None # Skip call to switch_backends.
21792112 else :
21802113 # Return a default canvas for the requested format, if it exists.
21812114 canvas_class = get_registered_canvas_class (fmt )
21822115 if canvas_class :
2183- return self .switch_backends (canvas_class )
2184- # Else report error for unsupported format.
2185- raise ValueError (
2186- "Format {!r} is not supported (supported formats: {})"
2187- .format (fmt , ", " .join (sorted (self .get_supported_filetypes ()))))
2116+ canvas = self .switch_backends (canvas_class )
2117+ if canvas is None :
2118+ raise ValueError (
2119+ "Format {!r} is not supported (supported formats: {})" .format (
2120+ fmt , ", " .join (sorted (self .get_supported_filetypes ()))))
2121+ meth = getattr (canvas , f"print_{ fmt } " )
2122+ mod = (meth .func .__module__
2123+ if hasattr (meth , "func" ) # partialmethod, e.g. backend_wx.
2124+ else meth .__module__ )
2125+ if mod .startswith (("matplotlib." , "mpl_toolkits." )):
2126+ optional_kws = { # Passed by print_figure for other renderers.
2127+ "dpi" , "facecolor" , "edgecolor" , "orientation" ,
2128+ "bbox_inches_restore" }
2129+ skip = optional_kws - {* inspect .signature (meth ).parameters }
2130+ print_method = functools .wraps (meth )(lambda * args , ** kwargs : meth (
2131+ * args , ** {k : v for k , v in kwargs .items () if k not in skip }))
2132+ else : # Let third-parties do as they see fit.
2133+ print_method = meth
2134+ try :
2135+ yield print_method
2136+ finally :
2137+ self .figure .canvas = self
21882138
21892139 def print_figure (
21902140 self , filename , dpi = None , facecolor = None , edgecolor = None ,
@@ -2252,20 +2202,18 @@ def print_figure(
22522202 filename = filename .rstrip ('.' ) + '.' + format
22532203 format = format .lower ()
22542204
2255- # get canvas object and print method for format
2256- canvas = self ._get_output_canvas (backend , format )
2257- print_method = getattr (canvas , 'print_%s' % format )
2258-
22592205 if dpi is None :
22602206 dpi = rcParams ['savefig.dpi' ]
22612207 if dpi == 'figure' :
22622208 dpi = getattr (self .figure , '_original_dpi' , self .figure .dpi )
22632209
22642210 # Remove the figure manager, if any, to avoid resizing the GUI widget.
22652211 with cbook ._setattr_cm (self , manager = None ), \
2212+ self ._switching_canvas_and_get_print_method (format , backend ) \
2213+ as print_method , \
22662214 cbook ._setattr_cm (self .figure , dpi = dpi ), \
2267- cbook ._setattr_cm (canvas , _device_pixel_ratio = 1 ), \
2268- cbook ._setattr_cm (canvas , _is_saving = True ), \
2215+ cbook ._setattr_cm (self . figure . canvas , _device_pixel_ratio = 1 ), \
2216+ cbook ._setattr_cm (self . figure . canvas , _is_saving = True ), \
22692217 ExitStack () as stack :
22702218
22712219 for prop in ["facecolor" , "edgecolor" ]:
@@ -2300,8 +2248,8 @@ def print_figure(
23002248 bbox_inches = bbox_inches .padded (pad_inches )
23012249
23022250 # call adjust_bbox to save only the given area
2303- restore_bbox = tight_bbox .adjust_bbox (self . figure , bbox_inches ,
2304- canvas .fixed_dpi )
2251+ restore_bbox = tight_bbox .adjust_bbox (
2252+ self . figure , bbox_inches , self . figure . canvas .fixed_dpi )
23052253
23062254 _bbox_inches_restore = (bbox_inches , restore_bbox )
23072255 else :
@@ -2324,7 +2272,6 @@ def print_figure(
23242272 if bbox_inches and restore_bbox :
23252273 restore_bbox ()
23262274
2327- self .figure .set_canvas (self )
23282275 return result
23292276
23302277 @classmethod
0 commit comments