from __future__ import annotations
import weakref
from collections.abc import Iterator, MutableMapping
from zict.common import KT, VT, ZictBase, close, discard, flush, locked
[docs]class Cache(ZictBase[KT, VT]):
"""Transparent write-through cache around a MutableMapping with an expensive
__getitem__ method.
Parameters
----------
data: MutableMapping
Persistent, slow to read mapping to be cached
cache: MutableMapping
Fast cache for reads from data. This mapping may lose keys on its own; e.g. it
could be a LRU.
update_on_set: bool, optional
If True (default), the cache will be updated both when writing and reading.
If False, update the cache when reading, but just invalidate it when writing.
Notes
-----
If you call methods of this class from multiple threads, access will be fast as long
as all methods of ``cache``, plus ``data.__delitem__``, are fast. Other methods of
``data`` are not protected by locks.
Examples
--------
Keep the latest 100 accessed values in memory
>>> from zict import Cache, File, LRU, WeakValueMapping
>>> d = Cache(File('myfile'), LRU(100, {})) # doctest: +SKIP
Read data from disk every time, unless it was previously accessed and it's still in
use somewhere else in the application
>>> d = Cache(File('myfile'), WeakValueMapping()) # doctest: +SKIP
"""
data: MutableMapping[KT, VT]
cache: MutableMapping[KT, VT]
update_on_set: bool
_gen: int
_last_updated: dict[KT, int]
def __init__(
self,
data: MutableMapping[KT, VT],
cache: MutableMapping[KT, VT],
update_on_set: bool = True,
):
super().__init__()
self.data = data
self.cache = cache
self.update_on_set = update_on_set
self._gen = 0
self._last_updated = {}
@locked
def __getitem__(self, key: KT) -> VT:
try:
return self.cache[key]
except KeyError:
pass
gen = self._last_updated[key]
with self.unlock():
value = self.data[key]
# Could another thread have called __setitem__ or __delitem__ on the
# same key in the meantime? If not, update the cache
if gen == self._last_updated.get(key):
self.cache[key] = value
self._last_updated[key] += 1
return value
@locked
def __setitem__(self, key: KT, value: VT) -> None:
# If the item was already in cache and data.__setitem__ fails, e.g. because
# it's a File and the disk is full, make sure that the cache is invalidated.
discard(self.cache, key)
gen = self._gen
gen += 1
self._last_updated[key] = self._gen = gen
with self.unlock():
self.data[key] = value
if key not in self._last_updated:
# Another thread called __delitem__ in the meantime
discard(self.data, key)
elif gen != self._last_updated[key]:
# Another thread called __setitem__ in the meantime. We have no idea which
# of the two ended up actually setting self.data.
# Case 1: the other thread did not enter this locked code block yet.
# Prevent it from setting the cache.
self._last_updated[key] += 1
# Case 2: the other thread already exited this locked code block and set the
# cache. Invalidate it.
discard(self.cache, key)
else:
# No race condition
self._last_updated[key] += 1
if self.update_on_set:
self.cache[key] = value
@locked
def __delitem__(self, key: KT) -> None:
del self.data[key]
del self._last_updated[key]
discard(self.cache, key)
def __len__(self) -> int:
return len(self.data)
def __iter__(self) -> Iterator[KT]:
return iter(self.data)
def __contains__(self, key: object) -> bool:
# Do not let MutableMapping call self.data[key]
return key in self.data
def flush(self) -> None:
flush(self.cache, self.data)
[docs] def close(self) -> None:
close(self.cache, self.data)
[docs]class WeakValueMapping(weakref.WeakValueDictionary[KT, VT]):
"""Variant of weakref.WeakValueDictionary which silently ignores objects that
can't be referenced by a weakref.ref
"""
def __setitem__(self, key: KT, value: VT) -> None:
try:
super().__setitem__(key, value)
except TypeError:
pass