ai-station/.venv/lib/python3.12/site-packages/textual/cache.py

315 lines
9.1 KiB
Python

"""
Cache classes are dict-like containers used to avoid recalculating expensive operations such as rendering.
You can also use them in your own apps for similar reasons.
"""
from __future__ import annotations
from typing import TYPE_CHECKING, Dict, Generic, KeysView, TypeVar, overload
CacheKey = TypeVar("CacheKey")
CacheValue = TypeVar("CacheValue")
DefaultValue = TypeVar("DefaultValue")
__all__ = ["LRUCache", "FIFOCache"]
class LRUCache(Generic[CacheKey, CacheValue]):
"""
A dictionary-like container with a maximum size.
If an additional item is added when the LRUCache is full, the least
recently used key is discarded to make room for the new item.
The implementation is similar to functools.lru_cache, which uses a (doubly)
linked list to keep track of the most recently used items.
Each entry is stored as [PREV, NEXT, KEY, VALUE] where PREV is a reference
to the previous entry, and NEXT is a reference to the next value.
Note that stdlib's @lru_cache is implemented in C and faster! It's best to use
@lru_cache where you are caching things that are fairly quick and called many times.
Use LRUCache where you want increased flexibility and you are caching slow operations
where the overhead of the cache is a small fraction of the total processing time.
"""
__slots__ = [
"_maxsize",
"_cache",
"_full",
"_head",
"hits",
"misses",
]
def __init__(self, maxsize: int) -> None:
"""Initialize a LRUCache.
Args:
maxsize: Maximum size of the cache, before old items are discarded.
"""
self._maxsize = maxsize
self._cache: Dict[CacheKey, list[object]] = {}
self._full = False
self._head: list[object] = []
self.hits = 0
self.misses = 0
super().__init__()
@property
def maxsize(self) -> int:
"""int: Maximum size of cache, before new values evict old values."""
return self._maxsize
@maxsize.setter
def maxsize(self, maxsize: int) -> None:
self._maxsize = maxsize
def __bool__(self) -> bool:
return bool(self._cache)
def __len__(self) -> int:
return len(self._cache)
def __repr__(self) -> str:
return f"<LRUCache size={len(self)} maxsize={self._maxsize} hits={self.hits} misses={self.misses}>"
def grow(self, maxsize: int) -> None:
"""Grow the maximum size to at least `maxsize` elements.
Args:
maxsize: New maximum size.
"""
self.maxsize = max(self.maxsize, maxsize)
def clear(self) -> None:
"""Clear the cache."""
self._cache.clear()
self._full = False
self._head = []
def keys(self) -> KeysView[CacheKey]:
"""Get cache keys."""
# Mostly for tests
return self._cache.keys()
def set(self, key: CacheKey, value: CacheValue) -> None:
"""Set a value.
Args:
key: Key.
value: Value.
"""
if self._cache.get(key) is None:
head = self._head
if not head:
# First link references itself
self._head[:] = [head, head, key, value]
else:
# Add a new root to the beginning
self._head = [head[0], head, key, value]
# Updated references on previous root
head[0][1] = self._head # type: ignore[index]
head[0] = self._head
self._cache[key] = self._head
if self._full or len(self._cache) > self._maxsize:
# Cache is full, we need to evict the oldest one
self._full = True
head = self._head
last = head[0]
last[0][1] = head # type: ignore[index]
head[0] = last[0] # type: ignore[index]
del self._cache[last[2]] # type: ignore[index]
__setitem__ = set
if TYPE_CHECKING:
@overload
def get(self, key: CacheKey) -> CacheValue | None: ...
@overload
def get(
self, key: CacheKey, default: DefaultValue
) -> CacheValue | DefaultValue: ...
def get(
self, key: CacheKey, default: DefaultValue | None = None
) -> CacheValue | DefaultValue | None:
"""Get a value from the cache, or return a default if the key is not present.
Args:
key: Key
default: Default to return if key is not present.
Returns:
Either the value or a default.
"""
if (link := self._cache.get(key)) is None:
self.misses += 1
return default
if link is not self._head:
# Remove link from list
link[0][1] = link[1] # type: ignore[index]
link[1][0] = link[0] # type: ignore[index]
head = self._head
# Move link to head of list
link[0] = head[0]
link[1] = head
self._head = head[0][1] = head[0] = link # type: ignore[index]
self.hits += 1
return link[3] # type: ignore[return-value]
def __getitem__(self, key: CacheKey) -> CacheValue:
link = self._cache.get(key)
if (link := self._cache.get(key)) is None:
self.misses += 1
raise KeyError(key)
if link is not self._head:
link[0][1] = link[1] # type: ignore[index]
link[1][0] = link[0] # type: ignore[index]
head = self._head
link[0] = head[0]
link[1] = head
self._head = head[0][1] = head[0] = link # type: ignore[index]
self.hits += 1
return link[3] # type: ignore[return-value]
def __contains__(self, key: CacheKey) -> bool:
return key in self._cache
def discard(self, key: CacheKey) -> None:
"""Discard item in cache from key.
Args:
key: Cache key.
"""
if key not in self._cache:
return
link = self._cache[key]
# Remove link from list
link[0][1] = link[1] # type: ignore[index]
link[1][0] = link[0] # type: ignore[index]
# Remove link from cache
if self._head[2] == key:
self._head = self._head[1] # type: ignore[assignment]
if self._head[2] == key: # type: ignore[index]
self._head = []
del self._cache[key]
self._full = False
class FIFOCache(Generic[CacheKey, CacheValue]):
"""A simple cache that discards the first added key when full (First In First Out).
This has a lower overhead than LRUCache, but won't manage a working set as efficiently.
It is most suitable for a cache with a relatively low maximum size that is not expected to
do many lookups.
"""
__slots__ = [
"_maxsize",
"_cache",
"hits",
"misses",
]
def __init__(self, maxsize: int) -> None:
"""Initialize a FIFOCache.
Args:
maxsize: Maximum size of cache before discarding items.
"""
self._maxsize = maxsize
self._cache: dict[CacheKey, CacheValue] = {}
self.hits = 0
self.misses = 0
def __bool__(self) -> bool:
return bool(self._cache)
def __len__(self) -> int:
return len(self._cache)
def __repr__(self) -> str:
return (
f"<FIFOCache maxsize={self._maxsize} hits={self.hits} misses={self.misses}>"
)
def clear(self) -> None:
"""Clear the cache."""
self._cache.clear()
def keys(self) -> KeysView[CacheKey]:
"""Get cache keys."""
# Mostly for tests
return self._cache.keys()
def set(self, key: CacheKey, value: CacheValue) -> None:
"""Set a value.
Args:
key: Key.
value: Value.
"""
if key not in self._cache and len(self._cache) >= self._maxsize:
for first_key in self._cache:
self._cache.pop(first_key)
break
self._cache[key] = value
__setitem__ = set
if TYPE_CHECKING:
@overload
def get(self, key: CacheKey) -> CacheValue | None: ...
@overload
def get(
self, key: CacheKey, default: DefaultValue
) -> CacheValue | DefaultValue: ...
def get(
self, key: CacheKey, default: DefaultValue | None = None
) -> CacheValue | DefaultValue | None:
"""Get a value from the cache, or return a default if the key is not present.
Args:
key: Key
default: Default to return if key is not present.
Returns:
Either the value or a default.
"""
try:
result = self._cache[key]
except KeyError:
self.misses += 1
return default
else:
self.hits += 1
return result
def __getitem__(self, key: CacheKey) -> CacheValue:
try:
result = self._cache[key]
except KeyError:
self.misses += 1
raise KeyError(key) from None
else:
self.hits += 1
return result
def __contains__(self, key: CacheKey) -> bool:
return key in self._cache