Skip to content

catena.cache

Caching primitives for memoising partial-quotient generator calls.

This module provides:

  • :class:Cache — an immutable-key :class:~collections.UserDict that prevents accidental overwriting of stored results.
  • :class:OrdinalCache — a :class:Cache specialised for integer keys, tracking the smallest and largest key seen.
  • :class:CacheHandler — a bookkeeping wrapper that owns a :class:Cache instance and records call/read statistics.
  • :func:SetCache — a decorator / decorator-factory that memoises a function using arbitrary (args, kwargs) keys.
  • :func:SetLightCache — a lighter variant of :func:SetCache for single-argument functions, using the argument directly as the cache key.

BaseCache

Bases: UserDict[int, Any]

A self-computing, append-only cache that unifies the roles of the old :class:OrdinalCache and :class:CacheHandler into a single object.

On a cache miss, the bound function func is called automatically and the result stored; on a hit, the stored value is returned directly. Both paths are counted separately via :attr:call_count and :attr:read_count. Integer keys additionally maintain :attr:smallest_key and :attr:largest_key without iterating the dictionary.

Stored entries are immutable: attempting to overwrite an existing key raises :exc:KeyError. The mutating :class:~collections.UserDict methods (pop, popitem, __delitem__, update, setdefault, fromkeys, __or__) are disabled and raise :exc:NotImplementedError. Use :meth:prune or :meth:reset to reduce the cache size.

When maxsize is set, :meth:prune is called automatically before each new insertion that would exceed the limit, using prune_key to determine how many entries to retain.

Note

func must accept a single argument; the argument itself is used as the cache key.

Warning

This class is experimental. Its interface is subject to change without notice.

Source code in catena/cache.py
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
class BaseCache(UserDict[int, Any]):
    """
    A self-computing, append-only cache that unifies the roles of the old
    :class:`OrdinalCache` and :class:`CacheHandler` into a single object.

    On a cache *miss*, the bound function ``func`` is called automatically
    and the result stored; on a *hit*, the stored value is returned directly.
    Both paths are counted separately via :attr:`call_count` and
    :attr:`read_count`.  Integer keys additionally maintain :attr:`smallest_key`
    and :attr:`largest_key` without iterating the dictionary.

    Stored entries are *immutable*: attempting to overwrite an existing key
    raises :exc:`KeyError`.  The mutating :class:`~collections.UserDict`
    methods (``pop``, ``popitem``, ``__delitem__``, ``update``, ``setdefault``,
    ``fromkeys``, ``__or__``) are disabled and raise
    :exc:`NotImplementedError`.  Use :meth:`prune` or :meth:`reset` to
    reduce the cache size.

    When ``maxsize`` is set, :meth:`prune` is called automatically before
    each new insertion that would exceed the limit, using ``prune_key`` to
    determine how many entries to retain.

    Note:
        ``func`` must accept a single argument; the argument itself is used
        as the cache key.

    Warning:
        This class is experimental.  Its interface is subject to change
        without notice.
    """
    def __init__(self, *args: Any, func: Callable[[int], Any], maxsize: Optional[int] = None, prune_key: Optional[Callable[[int], tuple[int, Optional[str]]]] = None, seed: Optional[dict[int, Any]] = None, **kwargs: Any) -> None:
        """
        Initialises the cache.

        Args:
            func (Callable): The single-argument function whose results are
                memoised.  Called automatically on a cache miss.
            maxsize (int, optional): Maximum number of entries before an
                automatic :meth:`prune` is triggered.  ``None`` means
                unlimited.  Must be a strictly positive integer or ``None``.
            prune_key (Callable[[int], tuple], optional): A callable that
                receives the current ``maxsize`` and returns a tuple of
                positional arguments forwarded to :meth:`prune`.  Defaults
                to ``lambda k: (max(k - 1, 2),)``, which retains the
                ``max(maxsize - 1, 2)`` largest-keyed entries.
            seed (Optional[dict[int, Any]], optional): Pre-computed ``{key: value}`` pairs to
                load into the cache at construction time.  Entries are
                inserted directly into the underlying store without
                incrementing :attr:`call_count` or :attr:`read_count`.
                Subsequent reads of seeded keys are counted as normal hits.
                ``maxsize`` is not enforced against the seed.
            *args: Forwarded to :class:`~collections.UserDict`.
            **kwargs: Forwarded to :class:`~collections.UserDict`.

        Raises:
            ValueError: If ``maxsize`` is not a strictly positive integer or
                ``None``.
        """
        super().__init__(*args, **kwargs)
        self._func = func
        self._func_name = func.__name__ if hasattr(func, "__name__") else repr(func)
        self._read_count = 0
        self._call_count = 0
        self._write_count = 0 # As oposed to read_count and call_count, this counts all writes including those from seeding and pruning, not just misses.
        self._smallest_key = float('inf')
        self._largest_key = float('-inf')
        self._maxsize = maxsize
        self._prune_key = prune_key if prune_key is not None else (lambda k: (max(k - 1, 2), None))
        if self._maxsize is not None and (not isinstance(self._maxsize, int) or self._maxsize <= 0):
            raise ValueError(f"BaseCache maxsize must be a positive integer or None, got {self._maxsize!r}")
        if seed:
            for key, value in seed.items():
                self.data[key] = value
                if isinstance(key, int):
                    if key < self._smallest_key:
                        self._smallest_key = key
                    if key > self._largest_key:
                        self._largest_key = key

    def __setitem__(self, key: Any, value: Any) -> None:
        """
        Stores ``value`` under ``key``.

        If ``maxsize`` is set and the cache is at capacity, :meth:`prune` is
        called first to make room.  Integer keys update :attr:`smallest_key`
        and :attr:`largest_key` automatically.

        Args:
            key: The cache key.
            value: The value to store.

        Raises:
            KeyError: If ``key`` already exists (entries are immutable).
        """
        if key in self.data:
            raise KeyError(f"Cache key '{key}' is immutable and cannot be modified.")

        if self._maxsize is not None and len(self.data) >= self._maxsize:
            self.prune(*self._prune_key(self._maxsize))

        if isinstance(key, int):
            if key < self._smallest_key:
                self._smallest_key = key
            if key > self._largest_key:
                self._largest_key = key

        self._write_count += 1
        super().__setitem__(key, value)

    def __getitem__(self, key: Any) -> Any:
        """
        Returns the cached value for ``key``, computing and storing it on a miss.

        A *hit* (key already in the cache) increments :attr:`read_count`.
        A *miss* delegates to :meth:`__missing__`, which calls ``func``,
        stores the result, and increments :attr:`call_count`.

        Args:
            key: The cache key.

        Returns:
            The cached (or freshly computed) value for ``key``.
        """
        if key in self.data:
            self._read_count += 1
            return self.data[key]
        return self.__missing__(key)

    def __missing__(self, key: Any) -> Any:
        """
        Called by :meth:`__getitem__` on a cache miss.

        Invokes ``func(key)``, stores the result via :meth:`__setitem__`
        (which enforces ``maxsize`` and updates the key-range trackers), and
        increments :attr:`call_count`.

        Args:
            key: The missing cache key.

        Returns:
            The computed value ``func(key)``.
        """
        self._call_count += 1
        result = self._func(key)
        self[key] = result
        return result

    def __repr__(self) -> str:
        """
        Returns a concise string representation showing the bound function,
        call/read counts, and current size.
        """
        return f"BaseCache(func={self.func_name}, calls={self.call_count}, reads={self.read_count}, size={len(self)})"

    @property
    def func(self) -> Callable[[int], Any]:
        """The function whose results are being cached."""
        return self._func

    @property
    def func_name(self) -> str:
        """The name of the cached function, if available."""
        return self._func_name

    @property
    def read_count(self) -> int:
        """Number of cache hits since initialisation."""
        return self._read_count

    @property
    def write_count(self) -> int:
        """Number of cache writes since initialisation."""
        return self._write_count

    @property
    def call_count(self) -> int:
        """Number of cache misses (underlying function calls) since initialisation."""
        return self._call_count

    @property
    def smallest_key(self) -> Optional[int]:
        """The smallest key stored so far, or ``None`` if the cache is empty."""
        if not self.data:
            return None
        return cast(Optional[int], self._smallest_key if self._smallest_key != float('inf') else None)

    @property
    def largest_key(self) -> Optional[int]:
        """The largest key stored so far, or ``None`` if the cache is empty."""
        if not self.data:
            return None
        return cast(Optional[int], self._largest_key if self._largest_key != float('-inf') else None)

    @property
    def maxsize(self) -> Optional[int]:
        """The maximum size of the cache, or ``None`` if unlimited."""
        return self._maxsize

    @property
    def stats(self) -> dict[str, Any]:
        """A summary of the cache's current statistics as a dictionary."""
        return {
            "func": self.func_name,
            "size": len(self),
            "calls": self.call_count,
            "reads": self.read_count,
            "writes": self._write_count, 
            "smallest_key": self.smallest_key,
            "largest_key": self.largest_key,
            "maxsize": self.maxsize,
            "prune_key": self._prune_key(cast(int, self.maxsize)) if self._maxsize is not None else None
        }

    @property
    def cache(self) -> Self:
        """Returns self for backward compatibility."""
        return self

    def prune(self, n: int = 2, order: Optional[str] = 'asc') -> None:
        """
        Retains only the ``n`` largest-keyed entries, discarding the rest.

        Args:
            n (int): Number of entries to keep.  Defaults to ``2``.
            order (str): Whether to keep the entries with the
                largest keys (``'asc'``) or smallest keys (``'desc'``).
                Defaults to ``'asc'``.

        Raises:
            ValueError: If ``order`` is not 'asc' or 'desc'.
        """
        if n <= 0:
            self.clear()
            return

        if order is None:
             order = 'asc'

        if order == 'asc':
            keys_to_keep = heapq.nlargest(n, self.data)
            new_data = {k: self.data[k] for k in keys_to_keep}
            self.clear()
            self.data.update(new_data)
        elif order == 'desc':
            keys_to_keep = heapq.nsmallest(n, self.data)
            new_data = {k: self.data[k] for k in keys_to_keep}
            self.clear()
            self.data.update(new_data)
        else:
            raise ValueError(f"Invalid prune order: {order!r}. Expected 'asc' or 'desc'.")

        if new_data:
            self._smallest_key = min(new_data)
            self._largest_key = max(new_data)

    def reset(self) -> None:
        """
        Clears all cached entries and resets :attr:`call_count`,
        :attr:`read_count`, :attr:`smallest_key`, and :attr:`largest_key`
        to their initial values.

        The bound ``func`` and ``maxsize`` are preserved.
        """
        self.clear()
        self._read_count = 0
        self._call_count = 0

    @override
    def clear(self) -> None:
        """
        Clears all cached entries and resets :attr:`smallest_key` and
        :attr:`largest_key`, but *preserves* :attr:`call_count` and
        :attr:`read_count`.

        To reset statistics as well, use :meth:`reset`.
        """
        self.data.clear()
        self._smallest_key = float('inf')
        self._largest_key = float('-inf')

    @override
    def copy(self) -> BaseCache:
        """
        Returns a shallow copy of the cache.

        The new instance shares the same ``func``, ``maxsize``, and
        ``prune_key``, and carries over the current data, statistics
        (:attr:`call_count`, :attr:`read_count`), and key-range sentinels
        (:attr:`smallest_key`, :attr:`largest_key`).

        Returns:
            BaseCache: A new :class:`BaseCache` instance with the same state.
        """
        new_cache = BaseCache(func=self.func, maxsize=self.maxsize, prune_key=self._prune_key)
        new_cache.data = self.data.copy()
        new_cache._read_count = self._read_count
        new_cache._call_count = self._call_count
        new_cache._smallest_key = self._smallest_key
        new_cache._largest_key = self._largest_key
        return new_cache

    @override
    def get(self, key: Any, default: Any = None) -> Any:
        """
        Returns the cached value for ``key``, or ``default`` on a miss.

        Unlike :meth:`__getitem__`, a missing key does *not* trigger a call
        to ``func`` and is *not* stored.  A hit increments :attr:`read_count`.

        Args:
            key: The cache key to look up.
            default: Value to return when ``key`` is absent.  Defaults to
                ``None``.

        Returns:
            The stored value, or ``default`` if the key is not in the cache.
        """
        if key in self.data:
            self._read_count += 1
            return self.data[key]
        return default

    @override
    def setdefault(self, key: Any, default: Any = None) -> NoReturn:
        """
        Not supported.  Raises :exc:`NotImplementedError`.

        Raises:
            NotImplementedError: Always.
        """
        raise NotImplementedError("setdefault is not supported by BaseCache due to immutability guarantees. Use direct assignment instead.")

    @override
    def update(self, *args: Any, **kwargs: Any) -> NoReturn:
        """
        Not supported.  Raises :exc:`NotImplementedError`.

        Raises:
            NotImplementedError: Always.
        """
        raise NotImplementedError("update is not supported by BaseCache due to immutability guarantees. Use direct assignment instead.")

    @override
    def pop(self, key: Any, *args: Any) -> NoReturn:
        """
        Not supported.  Raises :exc:`NotImplementedError`.

        Raises:
            NotImplementedError: Always.
        """
        raise NotImplementedError("pop is not supported by BaseCache due to immutability guarantees. Use prune() to remove entries instead.")

    @classmethod
    @override
    def fromkeys(cls, iterable: Iterable[Any], value: Any = None) -> NoReturn:
        """
        Not supported.  Raises :exc:`NotImplementedError`.

        Raises:
            NotImplementedError: Always.
        """
        raise NotImplementedError("fromkeys is not supported by BaseCache due to construnction requirements. Create a new instance and assign values directly instead.")

    @override
    def __delitem__(self, key: Any) -> NoReturn:
        """
        Not supported.  Raises :exc:`NotImplementedError`.

        Raises:
            NotImplementedError: Always.
        """
        raise NotImplementedError("Deletion of individual keys is not supported by BaseCache due to immutability guarantees. Use prune() to remove entries instead.")

    @override
    def popitem(self) -> NoReturn:
        """
        Not supported.  Raises :exc:`NotImplementedError`.

        Raises:
            NotImplementedError: Always.
        """
        raise NotImplementedError("popitem is not supported by BaseCache due to immutability guarantees. Use prune() to remove entries instead.")

    @override
    def __or__(self, other: Any) -> NoReturn:
        """
        Not supported.  Raises :exc:`NotImplementedError`.

        Raises:
            NotImplementedError: Always.
        """
        raise NotImplementedError("Merging of BaseCache instances is not supported due to immutability guarantees. Create a new instance and assign values directly instead.")

cache property

Returns self for backward compatibility.

call_count property

Number of cache misses (underlying function calls) since initialisation.

func property

The function whose results are being cached.

func_name property

The name of the cached function, if available.

largest_key property

The largest key stored so far, or None if the cache is empty.

maxsize property

The maximum size of the cache, or None if unlimited.

read_count property

Number of cache hits since initialisation.

smallest_key property

The smallest key stored so far, or None if the cache is empty.

stats property

A summary of the cache's current statistics as a dictionary.

write_count property

Number of cache writes since initialisation.

__delitem__(key)

Not supported. Raises :exc:NotImplementedError.

Raises:

Type Description
NotImplementedError

Always.

Source code in catena/cache.py
388
389
390
391
392
393
394
395
396
@override
def __delitem__(self, key: Any) -> NoReturn:
    """
    Not supported.  Raises :exc:`NotImplementedError`.

    Raises:
        NotImplementedError: Always.
    """
    raise NotImplementedError("Deletion of individual keys is not supported by BaseCache due to immutability guarantees. Use prune() to remove entries instead.")

__getitem__(key)

Returns the cached value for key, computing and storing it on a miss.

A hit (key already in the cache) increments :attr:read_count. A miss delegates to :meth:__missing__, which calls func, stores the result, and increments :attr:call_count.

Parameters:

Name Type Description Default
key Any

The cache key.

required

Returns:

Type Description
Any

The cached (or freshly computed) value for key.

Source code in catena/cache.py
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
def __getitem__(self, key: Any) -> Any:
    """
    Returns the cached value for ``key``, computing and storing it on a miss.

    A *hit* (key already in the cache) increments :attr:`read_count`.
    A *miss* delegates to :meth:`__missing__`, which calls ``func``,
    stores the result, and increments :attr:`call_count`.

    Args:
        key: The cache key.

    Returns:
        The cached (or freshly computed) value for ``key``.
    """
    if key in self.data:
        self._read_count += 1
        return self.data[key]
    return self.__missing__(key)

__init__(*args, func, maxsize=None, prune_key=None, seed=None, **kwargs)

Initialises the cache.

Parameters:

Name Type Description Default
func Callable

The single-argument function whose results are memoised. Called automatically on a cache miss.

required
maxsize int

Maximum number of entries before an automatic :meth:prune is triggered. None means unlimited. Must be a strictly positive integer or None.

None
prune_key Callable[[int], tuple]

A callable that receives the current maxsize and returns a tuple of positional arguments forwarded to :meth:prune. Defaults to lambda k: (max(k - 1, 2),), which retains the max(maxsize - 1, 2) largest-keyed entries.

None
seed Optional[dict[int, Any]]

Pre-computed {key: value} pairs to load into the cache at construction time. Entries are inserted directly into the underlying store without incrementing :attr:call_count or :attr:read_count. Subsequent reads of seeded keys are counted as normal hits. maxsize is not enforced against the seed.

None
*args Any

Forwarded to :class:~collections.UserDict.

()
**kwargs Any

Forwarded to :class:~collections.UserDict.

{}

Raises:

Type Description
ValueError

If maxsize is not a strictly positive integer or None.

Source code in catena/cache.py
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
def __init__(self, *args: Any, func: Callable[[int], Any], maxsize: Optional[int] = None, prune_key: Optional[Callable[[int], tuple[int, Optional[str]]]] = None, seed: Optional[dict[int, Any]] = None, **kwargs: Any) -> None:
    """
    Initialises the cache.

    Args:
        func (Callable): The single-argument function whose results are
            memoised.  Called automatically on a cache miss.
        maxsize (int, optional): Maximum number of entries before an
            automatic :meth:`prune` is triggered.  ``None`` means
            unlimited.  Must be a strictly positive integer or ``None``.
        prune_key (Callable[[int], tuple], optional): A callable that
            receives the current ``maxsize`` and returns a tuple of
            positional arguments forwarded to :meth:`prune`.  Defaults
            to ``lambda k: (max(k - 1, 2),)``, which retains the
            ``max(maxsize - 1, 2)`` largest-keyed entries.
        seed (Optional[dict[int, Any]], optional): Pre-computed ``{key: value}`` pairs to
            load into the cache at construction time.  Entries are
            inserted directly into the underlying store without
            incrementing :attr:`call_count` or :attr:`read_count`.
            Subsequent reads of seeded keys are counted as normal hits.
            ``maxsize`` is not enforced against the seed.
        *args: Forwarded to :class:`~collections.UserDict`.
        **kwargs: Forwarded to :class:`~collections.UserDict`.

    Raises:
        ValueError: If ``maxsize`` is not a strictly positive integer or
            ``None``.
    """
    super().__init__(*args, **kwargs)
    self._func = func
    self._func_name = func.__name__ if hasattr(func, "__name__") else repr(func)
    self._read_count = 0
    self._call_count = 0
    self._write_count = 0 # As oposed to read_count and call_count, this counts all writes including those from seeding and pruning, not just misses.
    self._smallest_key = float('inf')
    self._largest_key = float('-inf')
    self._maxsize = maxsize
    self._prune_key = prune_key if prune_key is not None else (lambda k: (max(k - 1, 2), None))
    if self._maxsize is not None and (not isinstance(self._maxsize, int) or self._maxsize <= 0):
        raise ValueError(f"BaseCache maxsize must be a positive integer or None, got {self._maxsize!r}")
    if seed:
        for key, value in seed.items():
            self.data[key] = value
            if isinstance(key, int):
                if key < self._smallest_key:
                    self._smallest_key = key
                if key > self._largest_key:
                    self._largest_key = key

__missing__(key)

Called by :meth:__getitem__ on a cache miss.

Invokes func(key), stores the result via :meth:__setitem__ (which enforces maxsize and updates the key-range trackers), and increments :attr:call_count.

Parameters:

Name Type Description Default
key Any

The missing cache key.

required

Returns:

Type Description
Any

The computed value func(key).

Source code in catena/cache.py
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
def __missing__(self, key: Any) -> Any:
    """
    Called by :meth:`__getitem__` on a cache miss.

    Invokes ``func(key)``, stores the result via :meth:`__setitem__`
    (which enforces ``maxsize`` and updates the key-range trackers), and
    increments :attr:`call_count`.

    Args:
        key: The missing cache key.

    Returns:
        The computed value ``func(key)``.
    """
    self._call_count += 1
    result = self._func(key)
    self[key] = result
    return result

__or__(other)

Not supported. Raises :exc:NotImplementedError.

Raises:

Type Description
NotImplementedError

Always.

Source code in catena/cache.py
408
409
410
411
412
413
414
415
416
@override
def __or__(self, other: Any) -> NoReturn:
    """
    Not supported.  Raises :exc:`NotImplementedError`.

    Raises:
        NotImplementedError: Always.
    """
    raise NotImplementedError("Merging of BaseCache instances is not supported due to immutability guarantees. Create a new instance and assign values directly instead.")

__repr__()

Returns a concise string representation showing the bound function, call/read counts, and current size.

Source code in catena/cache.py
172
173
174
175
176
177
def __repr__(self) -> str:
    """
    Returns a concise string representation showing the bound function,
    call/read counts, and current size.
    """
    return f"BaseCache(func={self.func_name}, calls={self.call_count}, reads={self.read_count}, size={len(self)})"

__setitem__(key, value)

Stores value under key.

If maxsize is set and the cache is at capacity, :meth:prune is called first to make room. Integer keys update :attr:smallest_key and :attr:largest_key automatically.

Parameters:

Name Type Description Default
key Any

The cache key.

required
value Any

The value to store.

required

Raises:

Type Description
KeyError

If key already exists (entries are immutable).

Source code in catena/cache.py
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
def __setitem__(self, key: Any, value: Any) -> None:
    """
    Stores ``value`` under ``key``.

    If ``maxsize`` is set and the cache is at capacity, :meth:`prune` is
    called first to make room.  Integer keys update :attr:`smallest_key`
    and :attr:`largest_key` automatically.

    Args:
        key: The cache key.
        value: The value to store.

    Raises:
        KeyError: If ``key`` already exists (entries are immutable).
    """
    if key in self.data:
        raise KeyError(f"Cache key '{key}' is immutable and cannot be modified.")

    if self._maxsize is not None and len(self.data) >= self._maxsize:
        self.prune(*self._prune_key(self._maxsize))

    if isinstance(key, int):
        if key < self._smallest_key:
            self._smallest_key = key
        if key > self._largest_key:
            self._largest_key = key

    self._write_count += 1
    super().__setitem__(key, value)

clear()

Clears all cached entries and resets :attr:smallest_key and :attr:largest_key, but preserves :attr:call_count and :attr:read_count.

To reset statistics as well, use :meth:reset.

Source code in catena/cache.py
292
293
294
295
296
297
298
299
300
301
302
303
@override
def clear(self) -> None:
    """
    Clears all cached entries and resets :attr:`smallest_key` and
    :attr:`largest_key`, but *preserves* :attr:`call_count` and
    :attr:`read_count`.

    To reset statistics as well, use :meth:`reset`.
    """
    self.data.clear()
    self._smallest_key = float('inf')
    self._largest_key = float('-inf')

copy()

Returns a shallow copy of the cache.

The new instance shares the same func, maxsize, and prune_key, and carries over the current data, statistics (:attr:call_count, :attr:read_count), and key-range sentinels (:attr:smallest_key, :attr:largest_key).

Returns:

Name Type Description
BaseCache BaseCache

A new :class:BaseCache instance with the same state.

Source code in catena/cache.py
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
@override
def copy(self) -> BaseCache:
    """
    Returns a shallow copy of the cache.

    The new instance shares the same ``func``, ``maxsize``, and
    ``prune_key``, and carries over the current data, statistics
    (:attr:`call_count`, :attr:`read_count`), and key-range sentinels
    (:attr:`smallest_key`, :attr:`largest_key`).

    Returns:
        BaseCache: A new :class:`BaseCache` instance with the same state.
    """
    new_cache = BaseCache(func=self.func, maxsize=self.maxsize, prune_key=self._prune_key)
    new_cache.data = self.data.copy()
    new_cache._read_count = self._read_count
    new_cache._call_count = self._call_count
    new_cache._smallest_key = self._smallest_key
    new_cache._largest_key = self._largest_key
    return new_cache

fromkeys(iterable, value=None) classmethod

Not supported. Raises :exc:NotImplementedError.

Raises:

Type Description
NotImplementedError

Always.

Source code in catena/cache.py
377
378
379
380
381
382
383
384
385
386
@classmethod
@override
def fromkeys(cls, iterable: Iterable[Any], value: Any = None) -> NoReturn:
    """
    Not supported.  Raises :exc:`NotImplementedError`.

    Raises:
        NotImplementedError: Always.
    """
    raise NotImplementedError("fromkeys is not supported by BaseCache due to construnction requirements. Create a new instance and assign values directly instead.")

get(key, default=None)

Returns the cached value for key, or default on a miss.

Unlike :meth:__getitem__, a missing key does not trigger a call to func and is not stored. A hit increments :attr:read_count.

Parameters:

Name Type Description Default
key Any

The cache key to look up.

required
default Any

Value to return when key is absent. Defaults to None.

None

Returns:

Type Description
Any

The stored value, or default if the key is not in the cache.

Source code in catena/cache.py
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
@override
def get(self, key: Any, default: Any = None) -> Any:
    """
    Returns the cached value for ``key``, or ``default`` on a miss.

    Unlike :meth:`__getitem__`, a missing key does *not* trigger a call
    to ``func`` and is *not* stored.  A hit increments :attr:`read_count`.

    Args:
        key: The cache key to look up.
        default: Value to return when ``key`` is absent.  Defaults to
            ``None``.

    Returns:
        The stored value, or ``default`` if the key is not in the cache.
    """
    if key in self.data:
        self._read_count += 1
        return self.data[key]
    return default

pop(key, *args)

Not supported. Raises :exc:NotImplementedError.

Raises:

Type Description
NotImplementedError

Always.

Source code in catena/cache.py
367
368
369
370
371
372
373
374
375
@override
def pop(self, key: Any, *args: Any) -> NoReturn:
    """
    Not supported.  Raises :exc:`NotImplementedError`.

    Raises:
        NotImplementedError: Always.
    """
    raise NotImplementedError("pop is not supported by BaseCache due to immutability guarantees. Use prune() to remove entries instead.")

popitem()

Not supported. Raises :exc:NotImplementedError.

Raises:

Type Description
NotImplementedError

Always.

Source code in catena/cache.py
398
399
400
401
402
403
404
405
406
@override
def popitem(self) -> NoReturn:
    """
    Not supported.  Raises :exc:`NotImplementedError`.

    Raises:
        NotImplementedError: Always.
    """
    raise NotImplementedError("popitem is not supported by BaseCache due to immutability guarantees. Use prune() to remove entries instead.")

prune(n=2, order='asc')

Retains only the n largest-keyed entries, discarding the rest.

Parameters:

Name Type Description Default
n int

Number of entries to keep. Defaults to 2.

2
order str

Whether to keep the entries with the largest keys ('asc') or smallest keys ('desc'). Defaults to 'asc'.

'asc'

Raises:

Type Description
ValueError

If order is not 'asc' or 'desc'.

Source code in catena/cache.py
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
def prune(self, n: int = 2, order: Optional[str] = 'asc') -> None:
    """
    Retains only the ``n`` largest-keyed entries, discarding the rest.

    Args:
        n (int): Number of entries to keep.  Defaults to ``2``.
        order (str): Whether to keep the entries with the
            largest keys (``'asc'``) or smallest keys (``'desc'``).
            Defaults to ``'asc'``.

    Raises:
        ValueError: If ``order`` is not 'asc' or 'desc'.
    """
    if n <= 0:
        self.clear()
        return

    if order is None:
         order = 'asc'

    if order == 'asc':
        keys_to_keep = heapq.nlargest(n, self.data)
        new_data = {k: self.data[k] for k in keys_to_keep}
        self.clear()
        self.data.update(new_data)
    elif order == 'desc':
        keys_to_keep = heapq.nsmallest(n, self.data)
        new_data = {k: self.data[k] for k in keys_to_keep}
        self.clear()
        self.data.update(new_data)
    else:
        raise ValueError(f"Invalid prune order: {order!r}. Expected 'asc' or 'desc'.")

    if new_data:
        self._smallest_key = min(new_data)
        self._largest_key = max(new_data)

reset()

Clears all cached entries and resets :attr:call_count, :attr:read_count, :attr:smallest_key, and :attr:largest_key to their initial values.

The bound func and maxsize are preserved.

Source code in catena/cache.py
280
281
282
283
284
285
286
287
288
289
290
def reset(self) -> None:
    """
    Clears all cached entries and resets :attr:`call_count`,
    :attr:`read_count`, :attr:`smallest_key`, and :attr:`largest_key`
    to their initial values.

    The bound ``func`` and ``maxsize`` are preserved.
    """
    self.clear()
    self._read_count = 0
    self._call_count = 0

setdefault(key, default=None)

Not supported. Raises :exc:NotImplementedError.

Raises:

Type Description
NotImplementedError

Always.

Source code in catena/cache.py
347
348
349
350
351
352
353
354
355
@override
def setdefault(self, key: Any, default: Any = None) -> NoReturn:
    """
    Not supported.  Raises :exc:`NotImplementedError`.

    Raises:
        NotImplementedError: Always.
    """
    raise NotImplementedError("setdefault is not supported by BaseCache due to immutability guarantees. Use direct assignment instead.")

update(*args, **kwargs)

Not supported. Raises :exc:NotImplementedError.

Raises:

Type Description
NotImplementedError

Always.

Source code in catena/cache.py
357
358
359
360
361
362
363
364
365
@override
def update(self, *args: Any, **kwargs: Any) -> NoReturn:
    """
    Not supported.  Raises :exc:`NotImplementedError`.

    Raises:
        NotImplementedError: Always.
    """
    raise NotImplementedError("update is not supported by BaseCache due to immutability guarantees. Use direct assignment instead.")