From 5324adaec6a7fd3d78dea7b28451d5f6e95392a6 Mon Sep 17 00:00:00 2001 From: Thomas A Caswell Date: Wed, 3 Jun 2020 16:28:21 -0400 Subject: [PATCH 1/2] FIX: do not let no-op monkey patches to renderer leak out closes #17542 --- lib/matplotlib/backend_bases.py | 31 ++++++++++++++----------- lib/matplotlib/figure.py | 16 ++++++++++--- lib/matplotlib/tests/test_bbox_tight.py | 23 ++++++++++++++++++ lib/matplotlib/tight_layout.py | 2 +- 4 files changed, 54 insertions(+), 18 deletions(-) diff --git a/lib/matplotlib/backend_bases.py b/lib/matplotlib/backend_bases.py index 71eb153f24ab..8009207dd2a9 100644 --- a/lib/matplotlib/backend_bases.py +++ b/lib/matplotlib/backend_bases.py @@ -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__) @@ -1502,15 +1503,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 + cbook._setattr_cm 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. @@ -1529,12 +1529,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 @@ -2093,9 +2087,18 @@ 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) + ) + 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"]) + } + + with _setattr_cm(renderer, **no_ops): + self.figure.draw(renderer) + bbox_inches = self.figure.get_tightbbox( renderer, bbox_extra_artists=bbox_extra_artists) if pad_inches is None: diff --git a/lib/matplotlib/figure.py b/lib/matplotlib/figure.py index 9e7fc4c2d16e..83577ac92b73 100644 --- a/lib/matplotlib/figure.py +++ b/lib/matplotlib/figure.py @@ -2392,6 +2392,8 @@ 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 .cbook import _setattr_cm + from .backend_bases import RendererBase subplotspec_list = get_subplotspec_list(self.axes) if None in subplotspec_list: @@ -2402,9 +2404,17 @@ 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) + 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"]) + } + + with _setattr_cm(renderer, **no_ops): + 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) diff --git a/lib/matplotlib/tests/test_bbox_tight.py b/lib/matplotlib/tests/test_bbox_tight.py index 4d52580e8b5d..235e02461f37 100644 --- a/lib/matplotlib/tests/test_bbox_tight.py +++ b/lib/matplotlib/tests/test_bbox_tight.py @@ -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) diff --git a/lib/matplotlib/tight_layout.py b/lib/matplotlib/tight_layout.py index 43b578fef625..df55005047f9 100644 --- a/lib/matplotlib/tight_layout.py +++ b/lib/matplotlib/tight_layout.py @@ -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): From f777177971f12c79d46e85819caad5539c5c221b Mon Sep 17 00:00:00 2001 From: Thomas A Caswell Date: Mon, 8 Jun 2020 18:58:42 -0400 Subject: [PATCH 2/2] MNT: consolidate the no-op logic into a RenderBase method Be forgiving about renderer instances that do not inherit from RendereBase. --- lib/matplotlib/backend_bases.py | 33 +++++++++++++++++++++++---------- lib/matplotlib/figure.py | 17 +++++------------ 2 files changed, 28 insertions(+), 22 deletions(-) diff --git a/lib/matplotlib/backend_bases.py b/lib/matplotlib/backend_bases.py index 8009207dd2a9..447fa0aa0e31 100644 --- a/lib/matplotlib/backend_bases.py +++ b/lib/matplotlib/backend_bases.py @@ -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 @@ -709,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.""" @@ -1509,7 +1526,7 @@ def _get_renderer(figure, print_method=None): the figure. If you need a renderer without any active draw methods use - cbook._setattr_cm to temporary patch them out at your call site. + renderer._draw_disabled to temporary patch them out at your call site. """ # This is implemented by triggering a draw, then immediately jumping out of @@ -2089,14 +2106,10 @@ def print_figure( functools.partial( print_method, orientation=orientation) ) - 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"]) - } - - with _setattr_cm(renderer, **no_ops): + ctx = (renderer._draw_disabled() + if hasattr(renderer, '_draw_disabled') + else suppress()) + with ctx: self.figure.draw(renderer) bbox_inches = self.figure.get_tightbbox( diff --git a/lib/matplotlib/figure.py b/lib/matplotlib/figure.py index 83577ac92b73..6af6870832af 100644 --- a/lib/matplotlib/figure.py +++ b/lib/matplotlib/figure.py @@ -2392,9 +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 .cbook import _setattr_cm - from .backend_bases import RendererBase - + 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 " @@ -2403,15 +2401,10 @@ def tight_layout(self, renderer=None, pad=1.08, h_pad=None, w_pad=None, if renderer is None: renderer = get_renderer(self) - - 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"]) - } - - with _setattr_cm(renderer, **no_ops): + 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) pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy