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 ._switch_canvas_and_return_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,84 +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- if sys .version_info < (3 , 11 ):
1605- current_frame = None
1606- else :
1607- import inspect
1608- current_frame = inspect .currentframe ()
1609- for frame , line in traceback .walk_stack (current_frame ):
1610- if frame is None :
1611- # when called in embedded context may hit frame is None.
1612- break
1613- # Work around sphinx-gallery not setting __name__.
1614- frame_name = frame .f_globals .get ('__name__' , '' )
1615- if re .match (r'\A(matplotlib|mpl_toolkits)(\Z|\.(?!tests\.))' ,
1616- frame_name ):
1617- name = frame .f_code .co_name
1618- if public_api .match (name ):
1619- if name in ('print_figure' , '_no_output_draw' ):
1620- seen_print_figure = True
1621-
1622- elif frame_name == '_functools' :
1623- # PyPy adds an extra frame without module prefix for this
1624- # functools wrapper, which we ignore to assume we're still in
1625- # Matplotlib code.
1626- continue
1627- else :
1628- break
1629-
1630- accepted_kwargs = {* old_sig .parameters , * extra_kwargs }
1631- if seen_print_figure :
1632- for kw in ['dpi' , 'facecolor' , 'edgecolor' , 'orientation' ,
1633- 'bbox_inches_restore' ]:
1634- # Ignore keyword arguments that are passed in by print_figure
1635- # for the use of other renderers.
1636- if kw not in accepted_kwargs :
1637- kwargs .pop (kw , None )
1638-
1639- for arg in list (kwargs ):
1640- if arg in accepted_kwargs :
1641- continue
1642- _api .warn_deprecated (
1643- '3.3' , name = name , removal = '3.6' ,
1644- message = '%(name)s() got unexpected keyword argument "'
1645- + arg + '" which is no longer supported as of '
1646- '%(since)s and will become an error '
1647- '%(removal)s' )
1648- kwargs .pop (arg )
1649-
1650- return func (* args , ** kwargs )
1651-
1652- return wrapper
1653-
1654-
16551573class FigureCanvasBase :
16561574 """
16571575 The canvas the figure renders into.
@@ -2155,21 +2073,30 @@ def get_supported_filetypes_grouped(cls):
21552073 groupings [name ].sort ()
21562074 return groupings
21572075
2158- def _get_output_canvas (self , backend , fmt ):
2076+ @contextmanager
2077+ def _switch_canvas_and_return_print_method (self , fmt , backend = None ):
21592078 """
2160- Set the canvas in preparation for saving the figure.
2079+ Context manager temporarily setting the canvas for saving the figure::
2080+
2081+ with canvas._switch_canvas_and_return_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``.
21612087
21622088 Parameters
21632089 ----------
2164- backend : str or None
2165- If not None, switch the figure canvas to the ``FigureCanvas`` class
2166- of the given backend.
21672090 fmt : str
21682091 If *backend* is None, then determine a suitable canvas class for
21692092 saving to format *fmt* -- either the current canvas class, if it
21702093 supports *fmt*, or whatever `get_registered_canvas_class` returns;
21712094 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.
21722098 """
2099+ canvas = None
21732100 if backend is not None :
21742101 # Return a specific canvas class, if requested.
21752102 canvas_class = (
@@ -2180,16 +2107,34 @@ def _get_output_canvas(self, backend, fmt):
21802107 f"The { backend !r} backend does not support { fmt } output" )
21812108 elif hasattr (self , f"print_{ fmt } " ):
21822109 # Return the current canvas if it supports the requested format.
2183- return self
2110+ canvas = self
2111+ canvas_class = None # Skip call to switch_backends.
21842112 else :
21852113 # Return a default canvas for the requested format, if it exists.
21862114 canvas_class = get_registered_canvas_class (fmt )
21872115 if canvas_class :
2188- return self .switch_backends (canvas_class )
2189- # Else report error for unsupported format.
2190- raise ValueError (
2191- "Format {!r} is not supported (supported formats: {})"
2192- .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
21932138
21942139 def print_figure (
21952140 self , filename , dpi = None , facecolor = None , edgecolor = None ,
@@ -2257,20 +2202,18 @@ def print_figure(
22572202 filename = filename .rstrip ('.' ) + '.' + format
22582203 format = format .lower ()
22592204
2260- # get canvas object and print method for format
2261- canvas = self ._get_output_canvas (backend , format )
2262- print_method = getattr (canvas , 'print_%s' % format )
2263-
22642205 if dpi is None :
22652206 dpi = rcParams ['savefig.dpi' ]
22662207 if dpi == 'figure' :
22672208 dpi = getattr (self .figure , '_original_dpi' , self .figure .dpi )
22682209
22692210 # Remove the figure manager, if any, to avoid resizing the GUI widget.
22702211 with cbook ._setattr_cm (self , manager = None ), \
2212+ self ._switch_canvas_and_return_print_method (format , backend ) \
2213+ as print_method , \
22712214 cbook ._setattr_cm (self .figure , dpi = dpi ), \
2272- cbook ._setattr_cm (canvas , _device_pixel_ratio = 1 ), \
2273- 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 ), \
22742217 ExitStack () as stack :
22752218
22762219 for prop in ["facecolor" , "edgecolor" ]:
@@ -2305,8 +2248,8 @@ def print_figure(
23052248 bbox_inches = bbox_inches .padded (pad_inches )
23062249
23072250 # call adjust_bbox to save only the given area
2308- restore_bbox = tight_bbox .adjust_bbox (self . figure , bbox_inches ,
2309- canvas .fixed_dpi )
2251+ restore_bbox = tight_bbox .adjust_bbox (
2252+ self . figure , bbox_inches , self . figure . canvas .fixed_dpi )
23102253
23112254 _bbox_inches_restore = (bbox_inches , restore_bbox )
23122255 else :
@@ -2329,7 +2272,6 @@ def print_figure(
23292272 if bbox_inches and restore_bbox :
23302273 restore_bbox ()
23312274
2332- self .figure .set_canvas (self )
23332275 return result
23342276
23352277 @classmethod
0 commit comments