Skip to content

Commit

Permalink
feat: on-set callback
Browse files Browse the repository at this point in the history
  • Loading branch information
uncle-lv committed Nov 14, 2023
1 parent bcabcdb commit 6a15b37
Show file tree
Hide file tree
Showing 2 changed files with 38 additions and 8 deletions.
25 changes: 21 additions & 4 deletions src/cacheout/cache.py
Original file line number Diff line number Diff line change
Expand Up @@ -33,18 +33,18 @@ class RemovalCause(Enum):
Attributes:
DELETE: indicates that the cache entry was deleted by delete() or delete_many() explicitly.
SET: indicates that the cache entry was replaced with a new value by set() or set_many().
EXPIRED: indicates that the cache entry was removed because it expired.
FULL: indicates that the cache entry was removed because cache has been full (reached the
maximum size limit).
POPITEM: indicates that the cache entry was deleted by popitem().
_IGNORE: It's an internal member indicates you don't want to call on_delete callback.
"""

DELETE = auto()
SET = auto()
EXPIRED = auto()
FULL = auto()
POPITEM = auto()
_IGNORE = auto()


#: Callback that will be executed when a cache entry is retrieved.
Expand All @@ -54,6 +54,13 @@ class RemovalCause(Enum):
#: and `exists` is whether the cache key exists or not.
T_ON_GET_CALLBACK = t.Optional[t.Callable[[t.Hashable, t.Any, bool], None]]

#: Callback that will be executed when a cache entry is set.

#: It is called with arguments ``(key, new_value, old_value)`` where `key` is the cache key,
#: `new_value` is the value is set,
#: and `old_value` is the value is replaced(if the key didn't exist before, it's ``UNSET``).
T_ON_SET_CALLBACK = t.Optional[t.Callable[[t.Hashable, t.Any, t.Any], None]]

#: Callback that will be executed when a cache entry is removed.

#: It is called with arguments ``(key, value, cause)`` where `key` is the cache key,
Expand Down Expand Up @@ -92,6 +99,8 @@ class Cache:
cache key.
on_get: Callback which will be executed when a cache entry is retrieved.
See :class:`T_ON_GET_CALLBACK` for details.
on_set: Callback which will be executed when a cache entry is set.
See :class:`T_ON_SET_CALLBACK` for details.
on_delete: Callback which will be executed when a cache entry is removed.
See :class:`T_ON_DELETE_CALLBACK` for details.
stats: Cache statistics.
Expand All @@ -110,13 +119,15 @@ def __init__(
default: t.Any = None,
enable_stats: bool = False,
on_get: T_ON_GET_CALLBACK = None,
on_set: T_ON_SET_CALLBACK = None,
on_delete: T_ON_DELETE_CALLBACK = None,
):
self.maxsize = maxsize
self.ttl = ttl
self.timer = timer
self.default = default
self.on_get = on_get
self.on_set = on_set
self.on_delete = on_delete
self.stats = CacheStatsTracker(self, enable=enable_stats)

Expand Down Expand Up @@ -379,17 +390,23 @@ def _set(self, key: t.Hashable, value: t.Any, ttl: t.Optional[T_TTL] = None) ->
if ttl is None:
ttl = self.ttl

old_value = UNSET
if key not in self._cache:
self.evict()
else:
old_value = self._cache[key]

# Delete key before setting it so that it moves to the end of the OrderedDict key list.
# Needed for cache strategies that rely on the ordering of when keys were last inserted.
self._delete(key, RemovalCause.SET)
self._delete(key, RemovalCause._IGNORE)
self._cache[key] = value

if ttl and ttl > 0:
self._expire_times[key] = self.timer() + ttl

if self.on_set:
self.on_set(key, value, old_value)

def set_many(self, items: t.Mapping, ttl: t.Optional[T_TTL] = None) -> None:
"""
Set multiple cache keys at once.
Expand Down Expand Up @@ -425,7 +442,7 @@ def _delete(self, key: t.Hashable, cause: RemovalCause) -> int:
try:
value = self._cache[key]
del self._cache[key]
if self.on_delete:
if cause != RemovalCause._IGNORE and self.on_delete:
self.on_delete(key, value, cause)
count = 1
if cause == RemovalCause.FULL:
Expand Down
21 changes: 17 additions & 4 deletions tests/test_cache.py
Original file line number Diff line number Diff line change
Expand Up @@ -726,10 +726,6 @@ def on_delete(key, value, cause):
cache.delete("DELETE")
assert log == f"DELETE=1, RemovalCause={RemovalCause.DELETE.value}"

cache.set("SET", 1)
cache.set("SET", 2)
assert log == f"SET=1, RemovalCause={RemovalCause.SET.value}"

cache.clear()
cache.set("POPITEM", 1)
cache.popitem()
Expand Down Expand Up @@ -765,6 +761,23 @@ def on_get(key, value, existed):
assert log == "miss=None, existed=False"


def test_cache_on_set(cache: Cache):
"""Test that on_set(cache) callback."""
log = ""

def on_set(key, new_value, old_value):
nonlocal log
log = f"{key}={new_value}, old_value={old_value}"

cache.on_set = on_set

cache.set("a", 1)
assert re.match(r"^a=1, old_value=<object object at 0x.*>$", log)

cache.set("a", 2)
assert log == "a=2, old_value=1"


def test_cache_stats__disabled_by_default(cache: Cache):
"""Test that cache stats are disabled by default."""
assert cache.stats.is_enabled() is False
Expand Down

0 comments on commit 6a15b37

Please sign in to comment.