diff --git a/doc/api/next_api_changes/deprecations/22323-GL.rst b/doc/api/next_api_changes/deprecations/22323-GL.rst new file mode 100644 index 000000000000..b755c7804731 --- /dev/null +++ b/doc/api/next_api_changes/deprecations/22323-GL.rst @@ -0,0 +1,4 @@ +``matplotlib.cbook.maxdict`` is deprecated +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +This class has been deprecated in favor of the standard library +``functools.lru_cache``. diff --git a/lib/matplotlib/cbook/__init__.py b/lib/matplotlib/cbook/__init__.py index efaabd1e09d4..2005f04c1ab6 100644 --- a/lib/matplotlib/cbook/__init__.py +++ b/lib/matplotlib/cbook/__init__.py @@ -540,6 +540,7 @@ def flatten(seq, scalarp=is_scalar_or_string): yield from flatten(item, scalarp) +@_api.deprecated("3.6", alternative="functools.lru_cache") class maxdict(dict): """ A dictionary with a maximum size. diff --git a/lib/matplotlib/tests/test_text.py b/lib/matplotlib/tests/test_text.py index ac029b6deddb..4a3ecb69307a 100644 --- a/lib/matplotlib/tests/test_text.py +++ b/lib/matplotlib/tests/test_text.py @@ -768,6 +768,8 @@ def test_pdf_chars_beyond_bmp(): @needs_usetex def test_metrics_cache(): + mpl.text._get_text_metrics_with_cache_impl.cache_clear() + fig = plt.figure() fig.text(.3, .5, "foo\nbar") fig.text(.3, .5, "foo\nbar", usetex=True) @@ -788,3 +790,8 @@ def call(*args, **kwargs): # collision with the non-TeX string (drawn first here) whose metrics would # get incorrectly reused by the first TeX string. assert len(ys["foo"]) == len(ys["bar"]) == 1 + + info = mpl.text._get_text_metrics_with_cache_impl.cache_info() + # Every string gets a miss for the first layouting (extents), then a hit + # when drawing, but "foo\nbar" gets two hits as it's drawn twice. + assert info.hits > info.misses diff --git a/lib/matplotlib/text.py b/lib/matplotlib/text.py index fc25391d27ef..ec83379742b6 100644 --- a/lib/matplotlib/text.py +++ b/lib/matplotlib/text.py @@ -2,6 +2,7 @@ Classes for including text in a figure. """ +import functools import logging import math import numbers @@ -89,6 +90,21 @@ def _get_textbox(text, renderer): return x_box, y_box, w_box, h_box +def _get_text_metrics_with_cache(renderer, text, fontprop, ismath, dpi): + """Call ``renderer.get_text_width_height_descent``, caching the results.""" + # Cached based on a copy of fontprop so that later in-place mutations of + # the passed-in argument do not mess up the cache. + return _get_text_metrics_with_cache_impl( + weakref.ref(renderer), text, fontprop.copy(), ismath, dpi) + + +@functools.lru_cache(4096) +def _get_text_metrics_with_cache_impl( + renderer_ref, text, fontprop, ismath, dpi): + # dpi is unused, but participates in cache invalidation (via the renderer). + return renderer_ref().get_text_width_height_descent(text, fontprop, ismath) + + @docstring.interpd @cbook._define_aliases({ "color": ["c"], @@ -108,7 +124,6 @@ class Text(Artist): """Handle storing and drawing of text in window or data coordinates.""" zorder = 3 - _cached = cbook.maxdict(50) def __repr__(self): return "Text(%s, %s, %s)" % (self._x, self._y, repr(self._text)) @@ -273,23 +288,6 @@ def update_from(self, other): self._linespacing = other._linespacing self.stale = True - def _get_text_metrics_with_cache( - self, renderer, text, fontproperties, ismath): - """ - Call ``renderer.get_text_width_height_descent``, caching the results. - """ - cache_key = ( - weakref.ref(renderer), - text, - hash(fontproperties), - ismath, - self.figure.dpi, - ) - if cache_key not in self._cached: - self._cached[cache_key] = renderer.get_text_width_height_descent( - text, fontproperties, ismath) - return self._cached[cache_key] - def _get_layout(self, renderer): """ Return the extent (bbox) of the text together with @@ -305,16 +303,17 @@ def _get_layout(self, renderer): ys = [] # Full vertical extent of font, including ascenders and descenders: - _, lp_h, lp_d = self._get_text_metrics_with_cache( + _, lp_h, lp_d = _get_text_metrics_with_cache( renderer, "lp", self._fontproperties, - ismath="TeX" if self.get_usetex() else False) + ismath="TeX" if self.get_usetex() else False, dpi=self.figure.dpi) min_dy = (lp_h - lp_d) * self._linespacing for i, line in enumerate(lines): clean_line, ismath = self._preprocess_math(line) if clean_line: - w, h, d = self._get_text_metrics_with_cache( - renderer, clean_line, self._fontproperties, ismath) + w, h, d = _get_text_metrics_with_cache( + renderer, clean_line, self._fontproperties, + ismath=ismath, dpi=self.figure.dpi) else: w = h = d = 0