Repository: matplotlib/matplotlib
File: lib/matplotlib/text.py (Python)

Code before:

        xt1, yt1 = tr.transform((x, y))
        yt1 -= d
        xt2, yt2 = xt1 + w, yt1 + h

        projected_xs.extend([xt1, xt2])
        projected_ys.extend([yt1, yt2])

    xt_box, yt_box = min(projected_xs), min(projected_ys)
    w_box, h_box = max(projected_xs) - xt_box, max(projected_ys) - yt_box

    x_box, y_box = Affine2D().rotate(theta).transform((xt_box, yt_box))

    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.
    func = _get_text_metrics_function(renderer)
    return func(text, fontprop.copy(), ismath, dpi)


# use mutable default to carry hidden global state
def _get_text_metrics_function(inp_renderer, _cache=weakref.WeakKeyDictionary()):
    if (_text_metrics := _cache.get(inp_renderer, None)) is None:
        renderer_ref = weakref.ref(inp_renderer)
        @functools.lru_cache(4096)
        def _text_metrics(text, fontprop, ismath, dpi):
            # dpi is unused, but participates in cache invalidation (via the renderer).
            if (lcl_renderer := renderer_ref()) is None:
                raise RuntimeError(
                    "Trying to get text metrics for a renderer that no longer exists.  "
                    "This should never happen and is evidence of a bug elsewhere."
                    )
            return lcl_renderer.get_text_width_height_descent(text, fontprop, ismath)
        _cache[inp_renderer] = _text_metrics

    return _text_metrics


@_docstring.interpd
@_api.define_aliases({
    "color": ["c"],
    "fontproperties": ["font", "font_properties"],
    "fontfamily": ["family"],
    "fontname": ["name"],
    "fontsize": ["size"],
    "fontstretch": ["stretch"],
    "fontstyle": ["style"],

Review comment (performance):
Hiding the cache in a hidden mutable kwarg seems a bit weird to me (but sure, why not); I would rather have defined it as a custom attribute on the function (`_get_text_metrics_function._cache = WeakKeyDictionary()`).  Just a stylistic choice, though.

Code after:

        xt1, yt1 = tr.transform((x, y))
        yt1 -= d
        xt2, yt2 = xt1 + w, yt1 + h

        projected_xs.extend([xt1, xt2])
        projected_ys.extend([yt1, yt2])

    xt_box, yt_box = min(projected_xs), min(projected_ys)
    w_box, h_box = max(projected_xs) - xt_box, max(projected_ys) - yt_box

    x_box, y_box = Affine2D().rotate(theta).transform((xt_box, yt_box))

    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."""

    # hit the outer cache layer and get the function to compute the metrics
    # for this renderer instance
    get_text_metrics = _get_text_metrics_function(renderer)
    # call the function to compute the metrics and return
    #
    # We pass a copy of the fontprop because FontProperties is both mutable and
    # has a `__hash__` that depends on that mutable state.  This is not ideal
    # as it means the hash of an object is not stable over time which leads to
    # very confusing behavior when used as keys in dictionaries or hashes.
    return get_text_metrics(text, fontprop.copy(), ismath, dpi)


def _get_text_metrics_function(input_renderer, _cache=weakref.WeakKeyDictionary()):
    """
    Helper function to provide a two-layered cache for font metrics


    To get the rendered size of a size of string we need to know:
      - what renderer we are using
      - the current dpi of the renderer
      - the string
      - the font properties
      - is it math text or not

    We do this as a two-layer cache with the outer layer being tied to a
    renderer instance and the inner layer handling everything else.

    The outer layer is implemented as `.WeakKeyDictionary` keyed on the
    renderer.  As long as someone else is holding a hard ref to the renderer
    we will keep the cache alive, but it will be automatically dropped when
    the renderer is garbage collected.