xref: /petsc/lib/petsc/bin/maint/petsclinter/petsclinter/classes/_attr_cache.py (revision 4c7cc9c8e01791debde927bc0816c9a347055c8f)
1#!/usr/bin/env python3
2"""
3# Created: Wed Aug  2 11:30:15 2023 (-0400)
4# @author: Jacob Faibussowitsch
5"""
6from __future__ import annotations
7
8from .._typing import *
9
10_T = TypeVar('_T')
11
12class AttributeCache:
13  r"""
14  An attribute cache (i.e. a `dict`) to enable lazy-evaluated default values. See the `_get_cached()`
15  method for a description and further motivation for the existence of this class.
16  """
17  __slots__ = ('_cache',)
18
19  _cache: dict[str, Any]
20
21  def __init__(self, init_cache: Optional[dict[str, Any]] = None) -> None:
22    r"""Construct an `AttributeCache`
23
24    Parameters
25    ----------
26    init_cache : optional
27      an existing dict to seed the cache with, or none to start fresh
28
29    Raises
30    ------
31    TypeError
32      if `init_cache` is not None but not a `dict`
33    """
34    if init_cache is None:
35      init_cache = {}
36    elif not isinstance(init_cache, dict):
37      raise TypeError(type(init_cache))
38    self._cache = init_cache
39    return
40
41  def _get_cached(self, attr: str, func: Callable[..., _T], *args, **kwargs) -> _T:
42    r"""Get a cached value
43
44    Parameters
45    ----------
46    attr :
47      the attribute name to retrieve
48    func :
49      a function to produce a default value in case `attr` is not in the cache
50    *args :
51      positional arguments to `func`, if any
52    **kwargs :
53      keyword arguments to `func`, if any
54
55    Returns
56    -------
57    ret :
58      the value
59
60    Notes
61    -----
62    If `attr` does not exist in the cache, this routine inserts the result of `func(*args, **kwargs)`
63    into the cache.
64
65    If all this sounds very similar to `dict.setdefault()`, then that's because it is. Normally you
66    would have some `dict` member and use `dict.setdefault()` to get a default value from it.
67
68    But this has one glaring weakness. `dict.setdefault()` (being a function call) obviously needs to
69    evaluate its arguments, i.e. the default value is computed whether or not the key already exists in
70    the dict (i.e. cache). This is OK if your default value is trivial, but what if producing it is
71    expensive?
72
73    This is why this class exists. Instead of taking the default by value, it takes the function which
74    computes it by parts, and only calls it if needed.
75    """
76    cache = self._cache
77    if attr in cache:
78      return TYPE_CAST(_T, cache[attr])
79    ret = cache[attr] = func(*args, **kwargs)
80    return ret
81