Skip to content

FIX: do not let no-op monkey patches to renderer leak out #17560

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 2 commits into from
Jun 9, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
46 changes: 31 additions & 15 deletions lib/matplotlib/backend_bases.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@
The base class for the Toolbar class of each interactive backend.
"""

from contextlib import contextmanager
from contextlib import contextmanager, suppress
from enum import Enum, IntEnum
import functools
import importlib
Expand All @@ -46,6 +46,7 @@
from matplotlib.backend_managers import ToolManager
from matplotlib.transforms import Affine2D
from matplotlib.path import Path
from matplotlib.cbook import _setattr_cm


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

def _draw_disabled(self):
"""
Context manager to temporary disable drawing.

This is used for getting the drawn size of Artists. This lets us
run the draw process to update any Python state but does not pay the
cost of the draw_XYZ calls on the canvas.
"""
no_ops = {
meth_name: lambda *args, **kwargs: None
for meth_name in dir(RendererBase)
if (meth_name.startswith("draw_")
or meth_name in ["open_group", "close_group"])
}

return _setattr_cm(self, **no_ops)


class GraphicsContextBase:
"""An abstract base class that provides color, line styles, etc."""
Expand Down Expand Up @@ -1502,15 +1520,14 @@ def __init__(self, name, canvas, key, x=0, y=0, guiEvent=None):
self.key = key


def _get_renderer(figure, print_method=None, *, draw_disabled=False):
def _get_renderer(figure, print_method=None):
"""
Get the renderer that would be used to save a `~.Figure`, and cache it on
the figure.

If *draw_disabled* is True, additionally replace drawing methods on
*renderer* by no-ops. This is used by the tight-bbox-saving renderer,
which needs to walk through the artist tree to compute the tight-bbox, but
for which the output file may be closed early.
If you need a renderer without any active draw methods use
renderer._draw_disabled to temporary patch them out at your call site.

"""
# This is implemented by triggering a draw, then immediately jumping out of
# Figure.draw() by raising an exception.
Expand All @@ -1529,12 +1546,6 @@ def _draw(renderer): raise Done(renderer)
except Done as exc:
renderer, = figure._cachedRenderer, = exc.args

if draw_disabled:
for meth_name in dir(RendererBase):
if (meth_name.startswith("draw_")
or meth_name in ["open_group", "close_group"]):
setattr(renderer, meth_name, lambda *args, **kwargs: None)

return renderer


Expand Down Expand Up @@ -2093,9 +2104,14 @@ def print_figure(
renderer = _get_renderer(
self.figure,
functools.partial(
print_method, orientation=orientation),
draw_disabled=True)
self.figure.draw(renderer)
print_method, orientation=orientation)
)
ctx = (renderer._draw_disabled()
if hasattr(renderer, '_draw_disabled')
else suppress())
with ctx:
self.figure.draw(renderer)

bbox_inches = self.figure.get_tightbbox(
renderer, bbox_extra_artists=bbox_extra_artists)
if pad_inches is None:
Expand Down
13 changes: 8 additions & 5 deletions lib/matplotlib/figure.py
Original file line number Diff line number Diff line change
Expand Up @@ -2392,7 +2392,7 @@ def tight_layout(self, renderer=None, pad=1.08, h_pad=None, w_pad=None,

from .tight_layout import (
get_renderer, get_subplotspec_list, get_tight_layout_figure)

from contextlib import suppress
subplotspec_list = get_subplotspec_list(self.axes)
if None in subplotspec_list:
cbook._warn_external("This figure includes Axes that are not "
Expand All @@ -2401,10 +2401,13 @@ def tight_layout(self, renderer=None, pad=1.08, h_pad=None, w_pad=None,

if renderer is None:
renderer = get_renderer(self)

kwargs = get_tight_layout_figure(
self, self.axes, subplotspec_list, renderer,
pad=pad, h_pad=h_pad, w_pad=w_pad, rect=rect)
ctx = (renderer._draw_disabled()
if hasattr(renderer, '_draw_disabled')
else suppress())
with ctx:
kwargs = get_tight_layout_figure(
self, self.axes, subplotspec_list, renderer,
pad=pad, h_pad=h_pad, w_pad=w_pad, rect=rect)
if kwargs:
self.subplots_adjust(**kwargs)

Expand Down
23 changes: 23 additions & 0 deletions lib/matplotlib/tests/test_bbox_tight.py
Original file line number Diff line number Diff line change
Expand Up @@ -110,3 +110,26 @@ def test_tight_pcolorfast():
# Previously, the bbox would include the area of the image clipped out by
# the axes, resulting in a very tall image given the y limits of (0, 0.1).
assert width > height


def test_noop_tight_bbox():
from PIL import Image
x_size, y_size = (10, 7)
dpi = 100
# make the figure just the right size up front
fig = plt.figure(frameon=False, dpi=dpi, figsize=(x_size/dpi, y_size/dpi))
ax = plt.Axes(fig, [0., 0., 1., 1.])
fig.add_axes(ax)
ax.set_axis_off()
ax.get_xaxis().set_visible(False)
ax.get_yaxis().set_visible(False)

data = np.arange(x_size * y_size).reshape(y_size, x_size)
ax.imshow(data)
out = BytesIO()
fig.savefig(out, bbox_inches='tight', pad_inches=0)
out.seek(0)
im = np.asarray(Image.open(out))
assert (im[:, :, 3] == 255).all()
assert not (im[:, :, :3] == 255).all()
assert im.shape == (7, 10, 4)
2 changes: 1 addition & 1 deletion lib/matplotlib/tight_layout.py
Original file line number Diff line number Diff line change
Expand Up @@ -173,7 +173,7 @@ def get_renderer(fig):
return canvas.get_renderer()
else:
from . import backend_bases
return backend_bases._get_renderer(fig, draw_disabled=True)
return backend_bases._get_renderer(fig)


def get_subplotspec_list(axes_list, grid_spec=None):
Expand Down