diff --git a/lib/matplotlib/artist.py b/lib/matplotlib/artist.py index bbc489502262..c5e54f97a2f2 100644 --- a/lib/matplotlib/artist.py +++ b/lib/matplotlib/artist.py @@ -880,13 +880,8 @@ def _update_property(self, k, v): raise AttributeError('Unknown property %s' % k) return func(v) - store = self.eventson - self.eventson = False - try: - ret = [_update_property(self, k, v) - for k, v in props.items()] - finally: - self.eventson = store + with cbook._setattr_cm(self, eventson=False): + ret = [_update_property(self, k, v) for k, v in props.items()] if len(ret): self.pchanged() diff --git a/lib/matplotlib/backend_bases.py b/lib/matplotlib/backend_bases.py index ddd4e6d3b0ba..4415b6cf8cdc 100644 --- a/lib/matplotlib/backend_bases.py +++ b/lib/matplotlib/backend_bases.py @@ -2113,17 +2113,6 @@ def print_figure(self, filename, dpi=None, facecolor=None, edgecolor=None, tight bbox is calculated. """ - self._is_saving = True - # Remove the figure manager, if any, to avoid resizing the GUI widget. - # Having *no* manager and a *None* manager are currently different (see - # Figure.show); should probably be normalized to None at some point. - _no_manager = object() - if hasattr(self, 'manager'): - manager = self.manager - del self.manager - else: - manager = _no_manager - if format is None: # get format from filename, or from backend's default filetype if isinstance(filename, getattr(os, "PathLike", ())): @@ -2142,104 +2131,107 @@ def print_figure(self, filename, dpi=None, facecolor=None, edgecolor=None, if dpi is None: dpi = rcParams['savefig.dpi'] - if dpi == 'figure': dpi = getattr(self.figure, '_original_dpi', self.figure.dpi) - if facecolor is None: - facecolor = rcParams['savefig.facecolor'] - if edgecolor is None: - edgecolor = rcParams['savefig.edgecolor'] - - origDPI = self.figure.dpi - origfacecolor = self.figure.get_facecolor() - origedgecolor = self.figure.get_edgecolor() - - self.figure.dpi = dpi - self.figure.set_facecolor(facecolor) - self.figure.set_edgecolor(edgecolor) - - bbox_inches = kwargs.pop("bbox_inches", None) - if bbox_inches is None: - bbox_inches = rcParams['savefig.bbox'] - - if bbox_inches: - # call adjust_bbox to save only the given area - if bbox_inches == "tight": - # When bbox_inches == "tight", it saves the figure twice. The - # first save command (to a BytesIO) is just to estimate the - # bounding box of the figure. + # Remove the figure manager, if any, to avoid resizing the GUI widget. + # Some code (e.g. Figure.show) differentiates between having *no* + # manager and a *None* manager, which should be fixed at some point, + # but this should be fine. + with cbook._setattr_cm(self, _is_saving=True, manager=None), \ + cbook._setattr_cm(self.figure, dpi=dpi): + + if facecolor is None: + facecolor = rcParams['savefig.facecolor'] + if edgecolor is None: + edgecolor = rcParams['savefig.edgecolor'] + + origfacecolor = self.figure.get_facecolor() + origedgecolor = self.figure.get_edgecolor() + + self.figure.dpi = dpi + self.figure.set_facecolor(facecolor) + self.figure.set_edgecolor(edgecolor) + + bbox_inches = kwargs.pop("bbox_inches", None) + if bbox_inches is None: + bbox_inches = rcParams['savefig.bbox'] + + if bbox_inches: + # call adjust_bbox to save only the given area + if bbox_inches == "tight": + # When bbox_inches == "tight", it saves the figure twice. + # The first save command (to a BytesIO) is just to estimate + # the bounding box of the figure. + result = print_method( + io.BytesIO(), + dpi=dpi, + facecolor=facecolor, + edgecolor=edgecolor, + orientation=orientation, + dryrun=True, + **kwargs) + renderer = self.figure._cachedRenderer + bbox_inches = self.figure.get_tightbbox(renderer) + + bbox_artists = kwargs.pop("bbox_extra_artists", None) + if bbox_artists is None: + bbox_artists = \ + self.figure.get_default_bbox_extra_artists() + + bbox_filtered = [] + for a in bbox_artists: + bbox = a.get_window_extent(renderer) + if a.get_clip_on(): + clip_box = a.get_clip_box() + if clip_box is not None: + bbox = Bbox.intersection(bbox, clip_box) + clip_path = a.get_clip_path() + if clip_path is not None and bbox is not None: + clip_path = \ + clip_path.get_fully_transformed_path() + bbox = Bbox.intersection( + bbox, clip_path.get_extents()) + if bbox is not None and ( + bbox.width != 0 or bbox.height != 0): + bbox_filtered.append(bbox) + + if bbox_filtered: + _bbox = Bbox.union(bbox_filtered) + trans = Affine2D().scale(1.0 / self.figure.dpi) + bbox_extra = TransformedBbox(_bbox, trans) + bbox_inches = Bbox.union([bbox_inches, bbox_extra]) + + pad = kwargs.pop("pad_inches", None) + if pad is None: + pad = rcParams['savefig.pad_inches'] + + bbox_inches = bbox_inches.padded(pad) + + restore_bbox = tight_bbox.adjust_bbox(self.figure, bbox_inches, + canvas.fixed_dpi) + + _bbox_inches_restore = (bbox_inches, restore_bbox) + else: + _bbox_inches_restore = None + + try: result = print_method( - io.BytesIO(), + filename, dpi=dpi, facecolor=facecolor, edgecolor=edgecolor, orientation=orientation, - dryrun=True, + bbox_inches_restore=_bbox_inches_restore, **kwargs) - renderer = self.figure._cachedRenderer - bbox_inches = self.figure.get_tightbbox(renderer) - - bbox_artists = kwargs.pop("bbox_extra_artists", None) - if bbox_artists is None: - bbox_artists = self.figure.get_default_bbox_extra_artists() - - bbox_filtered = [] - for a in bbox_artists: - bbox = a.get_window_extent(renderer) - if a.get_clip_on(): - clip_box = a.get_clip_box() - if clip_box is not None: - bbox = Bbox.intersection(bbox, clip_box) - clip_path = a.get_clip_path() - if clip_path is not None and bbox is not None: - clip_path = clip_path.get_fully_transformed_path() - bbox = Bbox.intersection(bbox, - clip_path.get_extents()) - if bbox is not None and (bbox.width != 0 or - bbox.height != 0): - bbox_filtered.append(bbox) - - if bbox_filtered: - _bbox = Bbox.union(bbox_filtered) - trans = Affine2D().scale(1.0 / self.figure.dpi) - bbox_extra = TransformedBbox(_bbox, trans) - bbox_inches = Bbox.union([bbox_inches, bbox_extra]) - - pad = kwargs.pop("pad_inches", None) - if pad is None: - pad = rcParams['savefig.pad_inches'] - - bbox_inches = bbox_inches.padded(pad) - - restore_bbox = tight_bbox.adjust_bbox(self.figure, bbox_inches, - canvas.fixed_dpi) - - _bbox_inches_restore = (bbox_inches, restore_bbox) - else: - _bbox_inches_restore = None - - try: - result = print_method( - filename, - dpi=dpi, - facecolor=facecolor, - edgecolor=edgecolor, - orientation=orientation, - bbox_inches_restore=_bbox_inches_restore, - **kwargs) - finally: - if bbox_inches and restore_bbox: - restore_bbox() - - self.figure.dpi = origDPI - self.figure.set_facecolor(origfacecolor) - self.figure.set_edgecolor(origedgecolor) - self.figure.set_canvas(self) - if manager is not _no_manager: - self.manager = manager - self._is_saving = False - return result + finally: + if bbox_inches and restore_bbox: + restore_bbox() + + self.figure.set_facecolor(origfacecolor) + self.figure.set_edgecolor(origedgecolor) + self.figure.set_canvas(self) + return result @classmethod def get_default_filetype(cls): diff --git a/lib/matplotlib/backends/backend_agg.py b/lib/matplotlib/backends/backend_agg.py index c102044cc974..9c854fb09474 100644 --- a/lib/matplotlib/backends/backend_agg.py +++ b/lib/matplotlib/backends/backend_agg.py @@ -486,53 +486,33 @@ def buffer_rgba(self): def print_raw(self, filename_or_obj, *args, **kwargs): FigureCanvasAgg.draw(self) renderer = self.get_renderer() - original_dpi = renderer.dpi - renderer.dpi = self.figure.dpi - if isinstance(filename_or_obj, six.string_types): - fileobj = open(filename_or_obj, 'wb') - close = True - else: - fileobj = filename_or_obj - close = False - try: - fileobj.write(renderer._renderer.buffer_rgba()) - finally: - if close: - fileobj.close() - renderer.dpi = original_dpi + with cbook._setattr_cm(renderer, dpi=self.figure.dpi), \ + cbook.open_file_cm(filename_or_obj, "wb") as fh: + fh.write(renderer._renderer.buffer_rgba()) print_rgba = print_raw def print_png(self, filename_or_obj, *args, **kwargs): FigureCanvasAgg.draw(self) renderer = self.get_renderer() - original_dpi = renderer.dpi - renderer.dpi = self.figure.dpi - version_str = 'matplotlib version ' + __version__ + \ - ', http://matplotlib.org/' + version_str = ( + 'matplotlib version ' + __version__ + ', http://matplotlib.org/') metadata = OrderedDict({'Software': version_str}) user_metadata = kwargs.pop("metadata", None) if user_metadata is not None: metadata.update(user_metadata) - try: - with cbook.open_file_cm(filename_or_obj, "wb") as fh: - _png.write_png(renderer._renderer, fh, - self.figure.dpi, metadata=metadata) - finally: - renderer.dpi = original_dpi + with cbook._setattr_cm(renderer, dpi=self.figure.dpi), \ + cbook.open_file_cm(filename_or_obj, "wb") as fh: + _png.write_png(renderer._renderer, fh, + self.figure.dpi, metadata=metadata) def print_to_buffer(self): FigureCanvasAgg.draw(self) renderer = self.get_renderer() - original_dpi = renderer.dpi - renderer.dpi = self.figure.dpi - try: - result = (renderer._renderer.buffer_rgba(), - (int(renderer.width), int(renderer.height))) - finally: - renderer.dpi = original_dpi - return result + with cbook._setattr_cm(renderer, dpi=self.figure.dpi): + return (renderer._renderer.buffer_rgba(), + (int(renderer.width), int(renderer.height))) if _has_pil: # add JPEG support diff --git a/lib/matplotlib/backends/backend_qt5.py b/lib/matplotlib/backends/backend_qt5.py index a3757d6000bf..9158677c3f29 100644 --- a/lib/matplotlib/backends/backend_qt5.py +++ b/lib/matplotlib/backends/backend_qt5.py @@ -12,6 +12,7 @@ import matplotlib +from matplotlib import backend_tools, cbook from matplotlib._pylab_helpers import Gcf from matplotlib.backend_bases import ( _Backend, FigureCanvasBase, FigureManagerBase, NavigationToolbar2, @@ -20,7 +21,6 @@ from matplotlib.backends.qt_editor.formsubplottool import UiSubplotTool from matplotlib.figure import Figure from matplotlib.backend_managers import ToolManager -from matplotlib import backend_tools from .qt_compat import ( QtCore, QtGui, QtWidgets, _getSaveFileName, is_pyqt5, __version__, QT_API) @@ -169,12 +169,9 @@ def cooperative_qwidget_init(self, *args, **kwargs): @functools.wraps(__init__) def wrapper(self, **kwargs): - try: - QtWidgets.QWidget.__init__ = cooperative_qwidget_init + with cbook._setattr_cm(QtWidgets.QWidget, + __init__=cooperative_qwidget_init): __init__(self, **kwargs) - finally: - # Restore __init__ - QtWidgets.QWidget.__init__ = qwidget_init return wrapper @@ -492,11 +489,8 @@ def draw(self): # that uses the result of the draw() to update plot elements. if self._is_drawing: return - self._is_drawing = True - try: + with cbook._setattr_cm(self, _is_drawing=True): super().draw() - finally: - self._is_drawing = False self.update() def draw_idle(self): diff --git a/lib/matplotlib/cbook/__init__.py b/lib/matplotlib/cbook/__init__.py index 73c48fa728f6..4489d0dbb322 100644 --- a/lib/matplotlib/cbook/__init__.py +++ b/lib/matplotlib/cbook/__init__.py @@ -2070,3 +2070,21 @@ def method(self, *args, **kwargs): raise NotImplementedError("Parent class already defines aliases") cls._alias_map = alias_d return cls + + +@contextlib.contextmanager +def _setattr_cm(obj, **kwargs): + """Temporarily set some attributes; restore original state at context exit. + """ + sentinel = object() + origs = [(attr, getattr(obj, attr, sentinel)) for attr in kwargs] + try: + for attr, val in kwargs.items(): + setattr(obj, attr, val) + yield + finally: + for attr, orig in origs: + if orig is sentinel: + delattr(obj, attr) + else: + setattr(obj, attr, orig) diff --git a/lib/matplotlib/font_manager.py b/lib/matplotlib/font_manager.py index 02cbd9633fcd..f497de51eb23 100644 --- a/lib/matplotlib/font_manager.py +++ b/lib/matplotlib/font_manager.py @@ -218,11 +218,7 @@ def win32InstalledFonts(directory=None, fontext='ttf'): direc = os.path.abspath(direc).lower() if os.path.splitext(direc)[1][1:] in fontext: items.add(direc) - except EnvironmentError: - continue - except WindowsError: - continue - except MemoryError: + except (EnvironmentError, MemoryError, WindowsError): continue return list(items) finally: @@ -520,17 +516,14 @@ def createFontList(fontfiles, fontext='ttf'): seen.add(fname) if fontext == 'afm': try: - fh = open(fpath, 'rb') + with open(fpath, 'rb') as fh: + font = afm.AFM(fh) except EnvironmentError: _log.info("Could not open font file %s", fpath) continue - try: - font = afm.AFM(fh) except RuntimeError: _log.info("Could not parse font file %s", fpath) continue - finally: - fh.close() try: prop = afmFontProperty(fpath, font) except KeyError: diff --git a/lib/matplotlib/style/core.py b/lib/matplotlib/style/core.py index d8a6c2efa4f1..2a836721882b 100644 --- a/lib/matplotlib/style/core.py +++ b/lib/matplotlib/style/core.py @@ -141,11 +141,6 @@ def context(style, after_reset=False): mpl.rcdefaults() try: use(style) - except: - # Restore original settings before raising errors during the update. - mpl.rcParams.update(initial_settings) - raise - else: yield finally: mpl.rcParams.update(initial_settings) diff --git a/lib/matplotlib/testing/conftest.py b/lib/matplotlib/testing/conftest.py index 9dc180c96000..5f7cac230297 100644 --- a/lib/matplotlib/testing/conftest.py +++ b/lib/matplotlib/testing/conftest.py @@ -4,6 +4,7 @@ import pytest import matplotlib +from matplotlib import cbook def pytest_configure(config): @@ -50,8 +51,7 @@ def mpl_test_settings(request): finally: if backend is not None: plt.switch_backend(prev_backend) - _do_cleanup(original_units_registry, - original_settings) + _do_cleanup(original_units_registry, original_settings) @pytest.fixture @@ -71,11 +71,9 @@ def mpl_image_comparison_parameters(request, extension): baseline_images = request.getfixturevalue('baseline_images') func = request.function - func.__wrapped__.parameters = (baseline_images, extension) - try: + with cbook._setattr_cm(func.__wrapped__, + parameters=(baseline_images, extension)): yield - finally: - delattr(func.__wrapped__, 'parameters') @pytest.fixture diff --git a/lib/matplotlib/testing/decorators.py b/lib/matplotlib/testing/decorators.py index 4d985f8c4895..d579a4713535 100644 --- a/lib/matplotlib/testing/decorators.py +++ b/lib/matplotlib/testing/decorators.py @@ -116,8 +116,7 @@ def wrapped_callable(*args, **kwargs): try: yield from func(*args, **kwargs) finally: - _do_cleanup(original_units_registry, - original_settings) + _do_cleanup(original_units_registry, original_settings) else: @functools.wraps(func) def wrapped_callable(*args, **kwargs): @@ -127,8 +126,7 @@ def wrapped_callable(*args, **kwargs): try: func(*args, **kwargs) finally: - _do_cleanup(original_units_registry, - original_settings) + _do_cleanup(original_units_registry, original_settings) return wrapped_callable diff --git a/lib/matplotlib/tests/test_axes.py b/lib/matplotlib/tests/test_axes.py index e8cb3a243fde..479e55b37251 100644 --- a/lib/matplotlib/tests/test_axes.py +++ b/lib/matplotlib/tests/test_axes.py @@ -875,11 +875,8 @@ def test_inverted_limits(): def test_nonfinite_limits(): x = np.arange(0., np.e, 0.01) # silence divide by zero warning from log(0) - olderr = np.seterr(divide='ignore') - try: + with np.errstate(divide='ignore'): y = np.log(x) - finally: - np.seterr(**olderr) x[len(x)//2] = np.nan fig = plt.figure() ax = fig.add_subplot(111) diff --git a/lib/matplotlib/tests/test_backend_ps.py b/lib/matplotlib/tests/test_backend_ps.py index 8768b2669ceb..fd0d192c3e38 100644 --- a/lib/matplotlib/tests/test_backend_ps.py +++ b/lib/matplotlib/tests/test_backend_ps.py @@ -1,12 +1,15 @@ import io +import os +from pathlib import Path import re +import tempfile import numpy as np import pytest import matplotlib import matplotlib.pyplot as plt -from matplotlib import patheffects +from matplotlib import cbook, patheffects from matplotlib.testing.decorators import image_comparison from matplotlib.testing.determinism import (_determinism_source_date_epoch, _determinism_check) @@ -79,40 +82,22 @@ def test_patheffects(): @needs_usetex @needs_ghostscript -def test_tilde_in_tempfilename(): +def test_tilde_in_tempfilename(tmpdir): # Tilde ~ in the tempdir path (e.g. TMPDIR, TMP or TEMP on windows # when the username is very long and windows uses a short name) breaks # latex before https://github.com/matplotlib/matplotlib/pull/5928 - import tempfile - import shutil - import os - import os.path - - tempdir = None - old_tempdir = tempfile.tempdir - try: - # change the path for new tempdirs, which is used - # internally by the ps backend to write a file - tempdir = tempfile.mkdtemp() - base_tempdir = os.path.join(tempdir, "short~1") - os.makedirs(base_tempdir) - tempfile.tempdir = base_tempdir - + base_tempdir = Path(str(tmpdir), "short-1") + base_tempdir.mkdir() + # Change the path for new tempdirs, which is used internally by the ps + # backend to write a file. + with cbook._setattr_cm(tempfile, tempdir=str(base_tempdir)): # usetex results in the latex call, which does not like the ~ plt.rc('text', usetex=True) plt.plot([1, 2, 3, 4]) plt.xlabel(r'\textbf{time} (s)') - output_eps = os.path.join(base_tempdir, 'tex_demo.eps') + output_eps = os.path.join(str(base_tempdir), 'tex_demo.eps') # use the PS backend to write the file... plt.savefig(output_eps, format="ps") - finally: - tempfile.tempdir = old_tempdir - if tempdir: - try: - shutil.rmtree(tempdir) - except Exception as e: - # do not break if this is not removable... - print(e) def test_source_date_epoch():