8000 Merge pull request #17560 from tacaswell/fix_noop_tight_bbox · matplotlib/matplotlib@f820c27 · GitHub
[go: up one dir, main page]

Skip to content

Commit f820c27

Browse files
authored
Merge pull request #17560 from tacaswell/fix_noop_tight_bbox
FIX: do not let no-op monkey patches to renderer leak out
2 parents 5157797 + f777177 commit f820c27

File tree

4 files changed

+63
-21
lines changed

4 files changed

+63
-21
lines changed

lib/matplotlib/backend_bases.py

Lines changed: 31 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@
2525
The base class for the Toolbar class of each interactive backend.
2626
"""
2727

28-
from contextlib import contextmanager
28+
from contextlib import contextmanager, suppress
2929
from enum import Enum, IntEnum
3030
import functools
3131
import importlib
@@ -46,6 +46,7 @@
4646
from matplotlib.backend_managers import ToolManager
4747
from matplotlib.transforms import Affine2D
4848
from matplotlib.path import Path
49+
from matplotlib.cbook import _setattr_cm
4950

5051

5152
_log = logging.getLogger(__name__)
@@ -708,6 +709,23 @@ def stop_filter(self, filter_func):
708709
Currently only supported by the agg renderer.
709710
"""
710711

712+
def _draw_disabled(self):
713+
"""
714+
Context manager to temporary disable drawing.
715+
716+
This is used for getting the drawn size of Artists. This lets us
717+
run the draw process to update any Python state but does not pay the
718+
cost of the draw_XYZ calls on the canvas.
< 10000 /td>
719+
"""
720+
no_ops = {
721+
meth_name: lambda *args, **kwargs: None
722+
for meth_name in dir(RendererBase)
723+
if (meth_name.startswith("draw_")
724+
or meth_name in ["open_group", "close_group"])
725+
}
726+
727+
return _setattr_cm(self, **no_ops)
728+
711729

712730
class GraphicsContextBase:
713731
"""An abstract base class that provides color, line styles, etc."""
@@ -1506,15 +1524,14 @@ def __init__(self, name, canvas, key, x=0, y=0, guiEvent=None):
15061524
LocationEvent.__init__(self, name, canvas, x, y, guiEvent=guiEvent)
15071525

15081526

1509-
def _get_renderer(figure, print_method=None, *, draw_disabled=False):
1527+
def _get_renderer(figure, print_method=None):
15101528
"""
15111529
Get the renderer that would be used to save a `~.Figure`, and cache it on
15121530
the figure.
15131531
1514-
If *draw_disabled* is True, additionally replace drawing methods on
1515-
*renderer* by no-ops. This is used by the tight-bbox-saving renderer,
1516-
which needs to walk through the artist tree to compute the tight-bbox, but
1517-
for which the output file may be closed early.
1532+
If you need a renderer without any active draw methods use
1533+
renderer._draw_disabled to temporary patch them out at your call site.
1534+
15181535
"""
15191536
# This is implemented by triggering a draw, then immediately jumping out of
15201537
# Figure.draw() by raising an exception.
@@ -1533,12 +1550,6 @@ def _draw(renderer): raise Done(renderer)
15331550
except Done as exc:
15341551
renderer, = figure._cachedRenderer, = exc.args
15351552

1536-
if draw_disabled:
1537-
for meth_name in dir(RendererBase):
1538-
if (meth_name.startswith("draw_")
1539-
or meth_name in ["open_group", "close_group"]):
1540-
setattr(renderer, meth_name, lambda *args, **kwargs: None)
1541-
15421553
return renderer
15431554

15441555

@@ -2097,9 +2108,14 @@ def print_figure(
20972108
renderer = _get_renderer(
20982109
self.figure,
20992110
functools.partial(
2100-
print_method, orientation=orientation),
2101-
draw_disabled=True)
2102-
self.figure.draw(renderer)
2111+
print_method, orientation=orientation)
2112+
)
2113+
ctx = (renderer._draw_disabled()
2114+
if hasattr(renderer, '_draw_disabled')
2115+
else suppress())
2116+
with ctx:
2117+
self.figure.draw(renderer)
2118+
21032119
bbox_inches = self.figure.get_tightbbox(
21042120
renderer, bbox_extra_artists=bbox_extra_artists)
21052121
if pad_inches is None:

lib/matplotlib/figure.py

Lines changed: 8 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -2392,7 +2392,7 @@ def tight_layout(self, renderer=None, pad=1.08, h_pad=None, w_pad=None,
23922392

23932393
from .tight_layout import (
23942394
get_renderer, get_subplotspec_list, get_tight_layout_figure)
2395-
2395+
from contextlib import suppress
23962396
subplotspec_list = get_subplotspec_list(self.axes)
23972397
if None in subplotspec_list:
23982398
cbook._warn_external("This figure includes Axes that are not "
@@ -2401,10 +2401,13 @@ def tight_layout(self, renderer=None, pad=1.08, h_pad=None, w_pad=None,
24012401

24022402
if renderer is None:
24032403
renderer = get_renderer(self)
2404-
2405-
kwargs = get_tight_layout_figure(
2406-
self, self.axes, subplotspec_list, renderer,
2407-
pad=pad, h_pad=h_pad, w_pad=w_pad, rect=rect)
2404+
ctx = (renderer._draw_disabled()
2405+
if hasattr(renderer, '_draw_disabled')
2406+
else suppress())
2407+
with ctx:
2408+
kwargs = get_tight_layout_figure(
2409+
self, self.axes, subplotspec_list, renderer,
2410+
pad=pad, h_pad=h_pad, w_pad=w_pad, rect=rect)
24082411
if kwargs:
24092412
self.subplots_adjust(**kwargs)
24102413

lib/matplotlib/tests/test_bbox_tight.py

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -110,3 +110,26 @@ def test_tight_pcolorfast():
110110
# Previously, the bbox would include the area of the image clipped out by
111111
# the axes, resulting in a very tall image given the y limits of (0, 0.1).
112112
assert width > height
113+
114+
115+
def test_noop_tight_bbox():
116+
from PIL import Image
117+
x_size, y_size = (10, 7)
118+
dpi = 100
119+
# make the figure just the right size up front
120+
fig = plt.figure(frameon=False, dpi=dpi, figsize=(x_size/dpi, y_size/dpi))
121+
ax = plt.Axes(fig, [0., 0., 1., 1.])
122+
fig.add_axes(ax)
123+
ax.set_axis_off()
124+
ax.get_xaxis().set_visible(False)
125+
ax.get_yaxis().set_visible(False)
126+
127+
data = np.arange(x_size * y_size).reshape(y_size, x_size)
128+
ax.imshow(data)
129+
out = BytesIO()
130+
fig.savefig(out, bbox_inches='tight', pad_inches=0)
131+
out.seek(0)
132+
im = np.asarray(Image.open(out))
133+
assert (im[:, :, 3] == 255).all()
134+
assert not (im[:, :, :3] == 255).all()
135+
assert im.shape == (7, 10, 4)

lib/matplotlib/tight_layout.py

Lines changed: 1 addition & 1 deletion
< 6A13 tr class="diff-line-row">
Original file line numberDiff line numberDiff line change
@@ -173,7 +173,7 @@ def get_renderer(fig):
173173
return canvas.get_renderer()
174174
else:
175175
from . import backend_bases
176-
return backend_bases._get_renderer(fig, draw_disabled=True)
176+
return backend_bases._get_renderer(fig)
177177

178178

179179
def get_subplotspec_list(axes_list, grid_spec=None):

0 commit comments

Comments
 (0)
0