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 ._switching_canvas_and_get_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,79 +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
- 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
-
1650
1573
class FigureCanvasBase :
1651
1574
"""
1652
1575
The canvas the figure renders into.
@@ -2145,21 +2068,30 @@ def get_supported_filetypes_grouped(cls):
2145
2068
groupings [name ].sort ()
2146
2069
return groupings
2147
2070
2148
- def _get_output_canvas (self , backend , fmt ):
2071
+ @contextmanager
2072
+ def _switching_canvas_and_get_print_method (self , fmt , backend = None ):
2149
2073
"""
2150
- Set the canvas in preparation for saving the figure.
2074
+ Context manager temporarily setting the canvas for saving the figure::
2075
+
2076
+ with canvas._switching_canvas_and_get_print_method(fmt, backend) \\
2077
+ as print_method:
2078
+ # ``print_method`` is a suitable ``print_{fmt}`` method, and
2079
+ # the figure's canvas is temporarily switched to the method's
2080
+ # canvas within the with... block. ``print_method`` is also
2081
+ # wrapped to suppress extra kwargs passed by ``print_figure``.
2151
2082
2152
2083
Parameters
2153
2084
----------
2154
- backend : str or None
2155
- If not None, switch the figure canvas to the ``FigureCanvas`` class
2156
- of the given backend.
2157
2085
fmt : str
2158
2086
If *backend* is None, then determine a suitable canvas class for
2159
2087
saving to format *fmt* -- either the current canvas class, if it
2160
2088
supports *fmt*, or whatever `get_registered_canvas_class` returns;
2161
2089
switch the figure canvas to that canvas class.
2090
+ backend : str or None, default: None
2091
+ If not None, switch the figure canvas to the ``FigureCanvas`` class
2092
+ of the given backend.
2162
2093
"""
2094
+ canvas = None
2163
2095
if backend is not None :
2164
2096
# Return a specific canvas class, if requested.
2165
2097
canvas_class = (
@@ -2170,16 +2102,34 @@ def _get_output_canvas(self, backend, fmt):
2170
2102
f"The { backend !r} backend does not support { fmt } output" )
2171
2103
elif hasattr (self , f"print_{ fmt } " ):
2172
2104
# Return the current canvas if it supports the requested format.
2173
- return self
2105
+ canvas = self
2106
+ canvas_class = None # Skip call to switch_backends.
2174
2107
else :
2175
2108
# Return a default canvas for the requested format, if it exists.
2176
2109
canvas_class = get_registered_canvas_class (fmt )
2177
2110
if canvas_class :
2178
- return self .switch_backends (canvas_class )
2179
- # Else report error for unsupported format.
2180
- raise ValueError (
2181
- "Format {!r} is not supported (supported formats: {})"
2182
- .format (fmt , ", " .join (sorted (self .get_supported_filetypes ()))))
2111
+ canvas = self .switch_backends (canvas_class )
2112
+ if canvas is None :
2113
+ raise ValueError (
2114
+ "Format {!r} is not supported (supported formats: {})" .format (
2115
+ fmt , ", " .join (sorted (self .get_supported_filetypes ()))))
2116
+ meth = getattr (canvas , f"print_{ fmt } " )
2117
+ mod = (getattr (meth .func , "__module__" , "" )
2118
+ if hasattr (meth , "func" ) # partialmethod, e.g. backend_wx.
2119
+ else getattr (meth , "__module__" , "" ))
2120
+ if mod .startswith (("matplotlib." , "mpl_toolkits." )):
2121
+ optional_kws = { # Passed by print_figure for other renderers.
2122
+ "dpi" , "facecolor" , "edgecolor" , "orientation" ,
2123
+ "bbox_inches_restore" }
2124
+ skip = optional_kws - {* inspect .signature (meth ).parameters }
2125
+ print_method = functools .wraps (meth )(lambda * args , ** kwargs : meth (
2126
+ * args , ** {k : v for k , v in kwargs .items () if k not in skip }))
2127
+ else : # Let third-parties do as they see fit.
2128
+ print_method = meth
2129
+ try :
2130
+ yield print_method
2131
+ finally :
2132
+ self .figure .canvas = self
2183
2133
2184
2134
def print_figure (
2185
2135
self , filename , dpi = None , facecolor = None , edgecolor = None ,
@@ -2247,20 +2197,18 @@ def print_figure(
2247
2197
filename = filename .rstrip ('.' ) + '.' + format
2248
2198
format = format .lower ()
2249
2199
2250
- # get canvas object and print method for format
2251
- canvas = self ._get_output_canvas (backend , format )
2252
- print_method = getattr (canvas , 'print_%s' % format )
2253
-
2254
2200
if dpi is None :
2255
2201
dpi = rcParams ['savefig.dpi' ]
2256
2202
if dpi == 'figure' :
2257
2203
dpi = getattr (self .figure , '_original_dpi' , self .figure .dpi )
2258
2204
2259
2205
# Remove the figure manager, if any, to avoid resizing the GUI widget.
2260
2206
with cbook ._setattr_cm (self , manager = None ), \
2207
+ self ._switching_canvas_and_get_print_method (format , backend ) \
2208
+ as print_method , \
2261
2209
cbook ._setattr_cm (self .figure , dpi = dpi ), \
2262
- cbook ._setattr_cm (canvas , _device_pixel_ratio = 1 ), \
2263
- cbook ._setattr_cm (canvas , _is_saving = True ), \
2210
+ cbook ._setattr_cm (self . figure . canvas , _device_pixel_ratio = 1 ), \
2211
+ cbook ._setattr_cm (self . figure . canvas , _is_saving = True ), \
2264
2212
ExitStack () as stack :
2265
2213
2266
2214
for prop in ["facecolor" , "edgecolor" ]:
@@ -2295,8 +2243,8 @@ def print_figure(
2295
2243
bbox_inches = bbox_inches .padded (pad_inches )
2296
2244
2297
2245
# call adjust_bbox to save only the given area
2298
- restore_bbox = tight_bbox .adjust_bbox (self . figure , bbox_inches ,
2299
- canvas .fixed_dpi )
2246
+ restore_bbox = tight_bbox .adjust_bbox (
2247
+ self . figure , bbox_inches , self . figure . canvas .fixed_dpi )
2300
2248
2301
2249
_bbox_inches_restore = (bbox_inches , restore_bbox )
2302
2250
else :
@@ -2319,7 +2267,6 @@ def print_figure(
2319
2267
if bbox_inches and restore_bbox :
2320
2268
restore_bbox ()
2321
2269
2322
- self .figure .set_canvas (self )
2323
2270
return result
2324
2271
2325
2272
@classmethod
0 commit comments