34
34
import io
35
35
import logging
36
36
import os
37
- import re
38
37
import sys
39
38
import time
40
- import traceback
41
39
from weakref import WeakKeyDictionary
42
40
43
41
import numpy as np
@@ -1534,14 +1532,14 @@ class Done(Exception):
1534
1532
1535
1533
def _draw (renderer ): raise Done (renderer )
1536
1534
1537
- with cbook ._setattr_cm (figure , draw = _draw ):
1535
+ with cbook ._setattr_cm (figure , draw = _draw ), ExitStack () as stack :
1538
1536
orig_canvas = figure .canvas
1539
1537
if print_method is None :
1540
1538
fmt = figure .canvas .get_default_filetype ()
1541
1539
# Even for a canvas' default output type, a canvas switch may be
1542
1540
# 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 ))
1545
1543
try :
1546
1544
print_method (io .BytesIO ())
1547
1545
except Done as exc :
@@ -1550,8 +1548,6 @@ def _draw(renderer): raise Done(renderer)
1550
1548
else :
1551
1549
raise RuntimeError (f"{ print_method } did not call Figure.draw, so "
1552
1550
f"no renderer is available" )
1553
- finally :
1554
- figure .canvas = orig_canvas
1555
1551
1556
1552
1557
1553
def _no_output_draw (figure ):
@@ -1574,84 +1570,6 @@ def _is_non_interactive_terminal_ipython(ip):
1574
1570
and getattr (ip .parent , 'interact' , None ) is False )
1575
1571
1576
1572
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
-
1655
1573
class FigureCanvasBase :
1656
1574
"""
1657
1575
The canvas the figure renders into.
@@ -2155,21 +2073,30 @@ def get_supported_filetypes_grouped(cls):
2155
2073
groupings [name ].sort ()
2156
2074
return groupings
2157
2075
2158
- def _get_output_canvas (self , backend , fmt ):
2076
+ @contextmanager
2077
+ def _switch_canvas_and_return_print_method (self , fmt , backend = None ):
2159
2078
"""
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``.
2161
2087
2162
2088
Parameters
2163
2089
----------
2164
- backend : str or None
2165
- If not None, switch the figure canvas to the ``FigureCanvas`` class
2166
- of the given backend.
2167
2090
fmt : str
2168
2091
If *backend* is None, then determine a suitable canvas class for
2169
2092
saving to format *fmt* -- either the current canvas class, if it
2170
2093
supports *fmt*, or whatever `get_registered_canvas_class` returns;
2171
2094
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.
2172
2098
"""
2099
+ canvas = None
2173
2100
if backend is not None :
2174
2101
# Return a specific canvas class, if requested.
2175
2102
canvas_class = (
@@ -2180,16 +2107,34 @@ def _get_output_canvas(self, backend, fmt):
2180
2107
f"The { backend !r} backend does not support { fmt } output" )
2181
2108
elif hasattr (self , f"print_{ fmt } " ):
2182
2109
# 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.
2184
2112
else :
2185
2113
# Return a default canvas for the requested format, if it exists.
2186
2114
canvas_class = get_registered_canvas_class (fmt )
2187
2115
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
2193
2138
2194
2139
def print_figure (
2195
2140
self , filename , dpi = None , facecolor = None , edgecolor = None ,
@@ -2257,20 +2202,18 @@ def print_figure(
2257
2202
filename = filename .rstrip ('.' ) + '.' + format
2258
2203
format = format .lower ()
2259
2204
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
-
2264
2205
if dpi is None :
2265
2206
dpi = rcParams ['savefig.dpi' ]
2266
2207
if dpi == 'figure' :
2267
2208
dpi = getattr (self .figure , '_original_dpi' , self .figure .dpi )
2268
2209
2269
2210
# Remove the figure manager, if any, to avoid resizing the GUI widget.
2270
2211
with cbook ._setattr_cm (self , manager = None ), \
2212
+ self ._switch_canvas_and_return_print_method (format , backend ) \
2213
+ as print_method , \
2271
2214
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 ), \
2274
2217
ExitStack () as stack :
2275
2218
2276
2219
for prop in ["facecolor" , "edgecolor" ]:
@@ -2305,8 +2248,8 @@ def print_figure(
2305
2248
bbox_inches = bbox_inches .padded (pad_inches )
2306
2249
2307
2250
# 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 )
2310
2253
2311
2254
_bbox_inches_restore = (bbox_inches , restore_bbox )
2312
2255
else :
@@ -2329,7 +2272,6 @@ def print_figure(
2329
2272
if bbox_inches and restore_bbox :
2330
2273
restore_bbox ()
2331
2274
2332
- self .figure .set_canvas (self )
2333
2275
return result
2334
2276
2335
2277
@classmethod
0 commit comments