Skip to content

Commit bcabcdb

Browse files
authored
feat: on-get callback (#38)
* feat: on-get callback * style: add alias types for callbacks * style: rename callback types
1 parent 346c6b4 commit bcabcdb

File tree

2 files changed

+51
-7
lines changed

2 files changed

+51
-7
lines changed

src/cacheout/cache.py

Lines changed: 27 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@
2323
T_TTL = t.Union[int, float]
2424
T_FILTER = t.Union[str, t.List[t.Hashable], t.Pattern, t.Callable]
2525

26+
2627
UNSET = object()
2728

2829

@@ -46,6 +47,21 @@ class RemovalCause(Enum):
4647
POPITEM = auto()
4748

4849

50+
#: Callback that will be executed when a cache entry is retrieved.
51+
52+
#: It is called with arguments ``(key, value, exists)`` where `key` is the cache key,
53+
#: `value` is the value retrieved (could be the default),
54+
#: and `exists` is whether the cache key exists or not.
55+
T_ON_GET_CALLBACK = t.Optional[t.Callable[[t.Hashable, t.Any, bool], None]]
56+
57+
#: Callback that will be executed when a cache entry is removed.
58+
59+
#: It is called with arguments ``(key, value, cause)`` where `key` is the cache key,
60+
#: `value` is the cached value at the time of deletion,
61+
#: and `cause` is the reason the key was removed (see :class:`RemovalCause` for enumerated causes).
62+
T_ON_DELETE_CALLBACK = t.Optional[t.Callable[[t.Hashable, t.Any, RemovalCause], None]]
63+
64+
4965
class Cache:
5066
"""
5167
An in-memory, FIFO cache object.
@@ -74,7 +90,10 @@ class Cache:
7490
default: Default value or function to use in :meth:`get` when key is not found. If callable,
7591
it will be passed a single argument, ``key``, and its return value will be set for that
7692
cache key.
93+
on_get: Callback which will be executed when a cache entry is retrieved.
94+
See :class:`T_ON_GET_CALLBACK` for details.
7795
on_delete: Callback which will be executed when a cache entry is removed.
96+
See :class:`T_ON_DELETE_CALLBACK` for details.
7897
stats: Cache statistics.
7998
"""
8099

@@ -90,12 +109,14 @@ def __init__(
90109
timer: t.Callable[[], T_TTL] = time.time,
91110
default: t.Any = None,
92111
enable_stats: bool = False,
93-
on_delete: t.Optional[t.Callable[[t.Hashable, t.Any, RemovalCause], None]] = None,
112+
on_get: T_ON_GET_CALLBACK = None,
113+
on_delete: T_ON_DELETE_CALLBACK = None,
94114
):
95115
self.maxsize = maxsize
96116
self.ttl = ttl
97117
self.timer = timer
98118
self.default = default
119+
self.on_get = on_get
99120
self.on_delete = on_delete
100121
self.stats = CacheStatsTracker(self, enable=enable_stats)
101122

@@ -255,6 +276,7 @@ def get(self, key: t.Hashable, default: t.Any = None) -> t.Any:
255276
return self._get(key, default=default)
256277

257278
def _get(self, key: t.Hashable, default: t.Any = None) -> t.Any:
279+
existed = True
258280
try:
259281
value = self._cache[key]
260282

@@ -263,6 +285,7 @@ def _get(self, key: t.Hashable, default: t.Any = None) -> t.Any:
263285
raise KeyError
264286
self.stats.inc_hit_count()
265287
except KeyError:
288+
existed = False
266289
self.stats.inc_miss_count()
267290
if default is None:
268291
default = self.default
@@ -273,6 +296,9 @@ def _get(self, key: t.Hashable, default: t.Any = None) -> t.Any:
273296
else:
274297
value = default
275298

299+
if self.on_get:
300+
self.on_get(key, value, existed)
301+
276302
return value
277303

278304
def get_many(self, iteratee: T_FILTER) -> dict:

tests/test_cache.py

Lines changed: 24 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -719,32 +719,50 @@ def test_cache_on_delete(cache: Cache, timer: Timer):
719719

720720
def on_delete(key, value, cause):
721721
nonlocal log
722-
log = f"{key}:{value} {cause.value}"
722+
log = f"{key}={value}, RemovalCause={cause.value}"
723723

724724
cache.on_delete = on_delete
725725
cache.set("DELETE", 1)
726726
cache.delete("DELETE")
727-
assert log == f"DELETE:1 {RemovalCause.DELETE.value}"
727+
assert log == f"DELETE=1, RemovalCause={RemovalCause.DELETE.value}"
728728

729729
cache.set("SET", 1)
730730
cache.set("SET", 2)
731-
assert log == f"SET:1 {RemovalCause.SET.value}"
731+
assert log == f"SET=1, RemovalCause={RemovalCause.SET.value}"
732732

733733
cache.clear()
734734
cache.set("POPITEM", 1)
735735
cache.popitem()
736-
assert log == f"POPITEM:1 {RemovalCause.POPITEM.value}"
736+
assert log == f"POPITEM=1, RemovalCause={RemovalCause.POPITEM.value}"
737737

738738
cache.set("EXPIRED", 1, ttl=1)
739739
timer.time = 1
740740
cache.delete_expired()
741-
assert log == f"EXPIRED:1 {RemovalCause.EXPIRED.value}"
741+
assert log == f"EXPIRED=1, RemovalCause={RemovalCause.EXPIRED.value}"
742742

743743
cache.clear()
744744
cache.maxsize = 1
745745
cache.set("FULL", 1)
746746
cache.set("OVERFLOW", 2)
747-
assert log == f"FULL:1 {RemovalCause.FULL.value}"
747+
assert log == f"FULL=1, RemovalCause={RemovalCause.FULL.value}"
748+
749+
750+
def test_cache_on_get(cache: Cache):
751+
"""Test that on_get(cache) callback."""
752+
log = ""
753+
754+
def on_get(key, value, existed):
755+
nonlocal log
756+
log = f"{key}={value}, existed={existed}"
757+
758+
cache.on_get = on_get
759+
cache.set("hit", 1)
760+
761+
cache.get("hit")
762+
assert log == "hit=1, existed=True"
763+
764+
cache.get("miss")
765+
assert log == "miss=None, existed=False"
748766

749767

750768
def test_cache_stats__disabled_by_default(cache: Cache):

0 commit comments

Comments
 (0)