diff --git a/lib/matplotlib/cbook.py b/lib/matplotlib/cbook.py index 7cf32c4d5f6a..95e857891190 100644 --- a/lib/matplotlib/cbook.py +++ b/lib/matplotlib/cbook.py @@ -32,6 +32,29 @@ from matplotlib import _api, _c_internal_utils +class _ExceptionInfo: + """ + A class to carry exception information around. + + This is used to store and later raise exceptions. It's an alternative to + directly storing Exception instances that circumvents traceback-related + issues: caching tracebacks can keep user's objects in local namespaces + alive indefinitely, which can lead to very surprising memory issues for + users and result in incorrect tracebacks. + """ + + def __init__(self, cls, *args): + self._cls = cls + self._args = args + + @classmethod + def from_exception(cls, exc): + return cls(type(exc), *exc.args) + + def to_exception(self): + return self._cls(*self._args) + + def _get_running_interactive_framework(): """ Return the interactive framework whose event loop is currently running, if diff --git a/lib/matplotlib/dviread.py b/lib/matplotlib/dviread.py index 0eae1852a91b..bd21367ce73d 100644 --- a/lib/matplotlib/dviread.py +++ b/lib/matplotlib/dviread.py @@ -340,7 +340,7 @@ def _read(self): byte = self.file.read(1)[0] self._dtable[byte](self, byte) if self._missing_font: - raise self._missing_font + raise self._missing_font.to_exception() name = self._dtable[byte].__name__ if name == "_push": down_stack.append(down_stack[-1]) @@ -368,14 +368,14 @@ def _read_arg(self, nbytes, signed=False): @_dispatch(min=0, max=127, state=_dvistate.inpage) def _set_char_immediate(self, char): self._put_char_real(char) - if isinstance(self.fonts[self.f], FileNotFoundError): + if isinstance(self.fonts[self.f], cbook._ExceptionInfo): return self.h += self.fonts[self.f]._width_of(char) @_dispatch(min=128, max=131, state=_dvistate.inpage, args=('olen1',)) def _set_char(self, char): self._put_char_real(char) - if isinstance(self.fonts[self.f], FileNotFoundError): + if isinstance(self.fonts[self.f], cbook._ExceptionInfo): return self.h += self.fonts[self.f]._width_of(char) @@ -390,7 +390,7 @@ def _put_char(self, char): def _put_char_real(self, char): font = self.fonts[self.f] - if isinstance(font, FileNotFoundError): + if isinstance(font, cbook._ExceptionInfo): self._missing_font = font elif font._vf is None: self.text.append(Text(self.h, self.v, font, char, @@ -504,7 +504,7 @@ def _fnt_def_real(self, k, c, s, d, a, l): # and throw that error in Dvi._read. For Vf, _finalize_packet # checks whether a missing glyph has been used, and in that case # skips the glyph definition. - self.fonts[k] = exc.with_traceback(None) + self.fonts[k] = cbook._ExceptionInfo.from_exception(exc) return if c != 0 and tfm.checksum != 0 and c != tfm.checksum: raise ValueError(f'tfm checksum mismatch: {n}') diff --git a/lib/matplotlib/font_manager.py b/lib/matplotlib/font_manager.py index 890663381b3d..98731af3463f 100644 --- a/lib/matplotlib/font_manager.py +++ b/lib/matplotlib/font_manager.py @@ -28,7 +28,6 @@ from __future__ import annotations from base64 import b64encode -from collections import namedtuple import copy import dataclasses from functools import lru_cache @@ -133,8 +132,6 @@ 'sans', } -_ExceptionProxy = namedtuple('_ExceptionProxy', ['klass', 'message']) - # OS Font paths try: _HOME = Path.home() @@ -1355,8 +1352,8 @@ def findfont(self, prop, fontext='ttf', directory=None, ret = self._findfont_cached( prop, fontext, directory, fallback_to_default, rebuild_if_missing, rc_params) - if isinstance(ret, _ExceptionProxy): - raise ret.klass(ret.message) + if isinstance(ret, cbook._ExceptionInfo): + raise ret.to_exception() return ret def get_font_names(self): @@ -1509,7 +1506,7 @@ def _findfont_cached(self, prop, fontext, directory, fallback_to_default, # This return instead of raise is intentional, as we wish to # cache that it was not found, which will not occur if it was # actually raised. - return _ExceptionProxy( + return cbook._ExceptionInfo( ValueError, f"Failed to find font {prop}, and fallback to the default font was " f"disabled" @@ -1535,7 +1532,7 @@ def _findfont_cached(self, prop, fontext, directory, fallback_to_default, # This return instead of raise is intentional, as we wish to # cache that it was not found, which will not occur if it was # actually raised. - return _ExceptionProxy(ValueError, "No valid font could be found") + return cbook._ExceptionInfo(ValueError, "No valid font could be found") return _cached_realpath(result)