Skip to content

Commit e40d9af

Browse files
QuLogictacaswell
authored andcommitted
Backport PR matplotlib#17560: FIX: do not let no-op monkey patches to renderer leak out
Merge pull request matplotlib#17560 from tacaswell/fix_noop_tight_bbox FIX: do not let no-op monkey patches to renderer leak out Conflicts: lib/matplotlib/backend_bases.py - trivial conflicting imports - did not back port other changes to tight box lib/matplotlib/tight_layout.py - ended up not backporting any of the changes
1 parent 29145cf commit e40d9af

File tree

3 files changed

+59
-19
lines changed

3 files changed

+59
-19
lines changed

lib/matplotlib/backend_bases.py

+28-14
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,7 @@
3232
The base class for the messaging area.
3333
"""
3434

35-
from contextlib import contextmanager
35+
from contextlib import contextmanager, suppress
3636
from enum import IntEnum
3737
import functools
3838
import importlib
@@ -52,6 +52,7 @@
5252
from matplotlib._pylab_helpers import Gcf
5353
from matplotlib.transforms import Affine2D
5454
from matplotlib.path import Path
55+
from matplotlib.cbook import _setattr_cm
5556

5657
try:
5758
from PIL import __version__ as PILLOW_VERSION
@@ -712,6 +713,23 @@ def stop_filter(self, filter_func):
712713
Currently only supported by the agg renderer.
713714
"""
714715

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

716734
class GraphicsContextBase:
717735
"""An abstract base class that provides color, line styles, etc."""
@@ -1520,15 +1538,14 @@ def __init__(self, name, canvas, key, x=0, y=0, guiEvent=None):
15201538
LocationEvent.__init__(self, name, canvas, x, y, guiEvent=guiEvent)
15211539

15221540

1523-
def _get_renderer(figure, print_method, *, draw_disabled=False):
1541+
def _get_renderer(figure, print_method):
15241542
"""
15251543
Get the renderer that would be used to save a `~.Figure`, and cache it on
15261544
the figure.
15271545
1528-
If *draw_disabled* is True, additionally replace drawing methods on
1529-
*renderer* by no-ops. This is used by the tight-bbox-saving renderer,
1530-
which needs to walk through the artist tree to compute the tight-bbox, but
1531-
for which the output file may be closed early.
1546+
If you need a renderer without any active draw methods use
1547+
renderer._draw_disabled to temporary patch them out at your call site.
1548+
15321549
"""
15331550
# This is implemented by triggering a draw, then immediately jumping out of
15341551
# Figure.draw() by raising an exception.
@@ -1544,12 +1561,6 @@ def _draw(renderer): raise Done(renderer)
15441561
except Done as exc:
15451562
renderer, = figure._cachedRenderer, = exc.args
15461563

1547-
if draw_disabled:
1548-
for meth_name in dir(RendererBase):
1549-
if (meth_name.startswith("draw_")
1550-
or meth_name in ["open_group", "close_group"]):
1551-
setattr(renderer, meth_name, lambda *args, **kwargs: None)
1552-
15531564
return renderer
15541565

15551566

@@ -2080,8 +2091,11 @@ def print_figure(self, filename, dpi=None, facecolor=None, edgecolor=None,
20802091
self.figure,
20812092
functools.partial(
20822093
print_method, dpi=dpi, orientation=orientation),
2083-
draw_disabled=True)
2084-
self.figure.draw(renderer)
2094+
ctx = (renderer._draw_disabled()
2095+
if hasattr(renderer, '_draw_disabled')
2096+
else suppress())
2097+
with ctx:
2098+
self.figure.draw(renderer)
20852099
bbox_artists = kwargs.pop("bbox_extra_artists", None)
20862100
bbox_inches = self.figure.get_tightbbox(renderer,
20872101
bbox_extra_artists=bbox_artists)

lib/matplotlib/figure.py

+8-5
Original file line numberDiff line numberDiff line change
@@ -2478,7 +2478,7 @@ def tight_layout(self, renderer=None, pad=1.08, h_pad=None, w_pad=None,
24782478

24792479
from .tight_layout import (
24802480
get_renderer, get_subplotspec_list, get_tight_layout_figure)
2481-
2481+
from contextlib import suppress
24822482
subplotspec_list = get_subplotspec_list(self.axes)
24832483
if None in subplotspec_list:
24842484
cbook._warn_external("This figure includes Axes that are not "
@@ -2487,10 +2487,13 @@ def tight_layout(self, renderer=None, pad=1.08, h_pad=None, w_pad=None,
24872487

24882488
if renderer is None:
24892489
renderer = get_renderer(self)
2490-
2491-
kwargs = get_tight_layout_figure(
2492-
self, self.axes, subplotspec_list, renderer,
2493-
pad=pad, h_pad=h_pad, w_pad=w_pad, rect=rect)
2490+
ctx = (renderer._draw_disabled()
2491+
if hasattr(renderer, '_draw_disabled')
2492+
else suppress())
2493+
with ctx:
2494+
kwargs = get_tight_layout_figure(
2495+
self, self.axes, subplotspec_list, renderer,
2496+
pad=pad, h_pad=h_pad, w_pad=w_pad, rect=rect)
24942497
if kwargs:
24952498
self.subplots_adjust(**kwargs)
24962499

lib/matplotlib/tests/test_bbox_tight.py

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

0 commit comments

Comments
 (0)