Skip to content

Commit db0c7c3

Browse files
committed
FIX: do not cache exceptions
This leads to caching the tracebacks which can keep user's objects in local namespaces alive indefinitely. This can lead to very surprising memory issues for users and will result in incorrect tracebacks. Responsive to matplotlib#25406
1 parent 370547e commit db0c7c3

File tree

2 files changed

+35
-8
lines changed

2 files changed

+35
-8
lines changed

lib/matplotlib/font_manager.py

Lines changed: 11 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@
2626
# - 'light' is an invalid weight value, remove it.
2727

2828
from base64 import b64encode
29+
from collections import namedtuple
2930
import copy
3031
import dataclasses
3132
from functools import lru_cache
@@ -128,6 +129,7 @@
128129
'sans',
129130
}
130131

132+
_ExceptionProxy = namedtuple('_ExceptionProxy', ['klass', 'message'])
131133

132134
# OS Font paths
133135
try:
@@ -1288,8 +1290,8 @@ def findfont(self, prop, fontext='ttf', directory=None,
12881290
ret = self._findfont_cached(
12891291
prop, fontext, directory, fallback_to_default, rebuild_if_missing,
12901292
rc_params)
1291-
if isinstance(ret, Exception):
1292-
raise ret
1293+
if isinstance(ret, _ExceptionProxy):
1294+
raise ret.klass(ret.message)
12931295
return ret
12941296

12951297
def get_font_names(self):
@@ -1440,10 +1442,12 @@ def _findfont_cached(self, prop, fontext, directory, fallback_to_default,
14401442
fallback_to_default=False)
14411443
else:
14421444
# This return instead of raise is intentional, as we wish to
1443-
# cache the resulting exception, which will not occur if it was
1445+
# cache that it was not found, which will not occur if it was
14441446
# actually raised.
1445-
return ValueError(f"Failed to find font {prop}, and fallback "
1446-
f"to the default font was disabled")
1447+
return _ExceptionProxy(
1448+
ValueError,
1449+
f"Failed to find font {prop}, and fallback to the default font was disabled"
1450+
)
14471451
else:
14481452
_log.debug('findfont: Matching %s to %s (%r) with score of %f.',
14491453
prop, best_font.name, best_font.fname, best_score)
@@ -1463,9 +1467,9 @@ def _findfont_cached(self, prop, fontext, directory, fallback_to_default,
14631467
prop, fontext, directory, rebuild_if_missing=False)
14641468
else:
14651469
# This return instead of raise is intentional, as we wish to
1466-
# cache the resulting exception, which will not occur if it was
1470+
# cache that it was not found, which will not occur if it was
14671471
# actually raised.
1468-
return ValueError("No valid font could be found")
1472+
return _ExceptionProxy(ValueError, "No valid font could be found")
14691473

14701474
return _cached_realpath(result)
14711475

lib/matplotlib/tests/test_font_manager.py

Lines changed: 24 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
from io import BytesIO, StringIO
2+
import gc
23
import multiprocessing
34
import os
45
from pathlib import Path
@@ -16,7 +17,7 @@
1617
json_dump, json_load, get_font, is_opentype_cff_font,
1718
MSUserFontDirectories, _get_fontconfig_fonts, ft2font,
1819
ttfFontProperty, cbook)
19-
from matplotlib import pyplot as plt, rc_context
20+
from matplotlib import pyplot as plt, rc_context, figure as mfigure
2021

2122
has_fclist = shutil.which('fc-list') is not None
2223

@@ -324,3 +325,25 @@ def test_get_font_names():
324325
assert set(available_fonts) == set(mpl_font_names)
325326
assert len(available_fonts) == len(mpl_font_names)
326327
assert available_fonts == mpl_font_names
328+
329+
330+
def test_donot_cache_tracebacks():
331+
332+
class SomeObject:
333+
pass
334+
335+
def inner():
336+
x = SomeObject()
337+
fig = mfigure.Figure()
338+
ax = fig.subplots()
339+
fig.text(.5, .5, 'aardvark', family='doesnotexist')
340+
with BytesIO() as out:
341+
with warnings.catch_warnings():
342+
warnings.filterwarnings('ignore')
343+
fig.savefig(out, format='png')
344+
345+
inner()
346+
347+
for obj in gc.get_objects():
348+
if isinstance(obj, SomeObject):
349+
pytest.fail("object from inner stack still alive")

0 commit comments

Comments
 (0)