Skip to content

Commit 088eae9

Browse files
author
Fabien Coelho
committed
add stats() method
1 parent 6d159dd commit 088eae9

File tree

5 files changed

+61
-11
lines changed

5 files changed

+61
-11
lines changed

CacheToolsUtils.py

+35-4
Original file line numberDiff line numberDiff line change
@@ -72,6 +72,9 @@ class _StatsMix:
7272
def hits(self):
7373
return self._cache.hits() # type: ignore
7474

75+
def stats(self):
76+
return self._cache.stats() # type: ignore
77+
7578
def reset(self):
7679
return self._cache.reset() # type: ignore
7780

@@ -102,7 +105,7 @@ def dbsize(self, *args, **kwargs):
102105
# CACHETOOLS EXTENSIONS
103106
#
104107

105-
class DebugCache(MutableMapping):
108+
class DebugCache(_StatsMix, MutableMapping):
106109
"""Debug class.
107110
108111
:param cache: actual cache
@@ -243,12 +246,23 @@ def __init__(self, cache: MutableMapping):
243246

244247
def hits(self):
245248
"""Return the cache hit ratio."""
246-
return float(self._hits) / float(max(self._reads, 1))
249+
return float(self._hits) / max(self._reads, 1)
247250

248251
def reset(self):
249252
"""Reset internal stats data."""
250253
self._reads, self._writes, self._dels, self._hits = 0, 0, 0, 0
251254

255+
def stats(self):
256+
"""Return available stats data as dict."""
257+
return {
258+
"type": 1,
259+
"reads": self._reads,
260+
"writes": self._writes,
261+
"dels": self._dels,
262+
"hits": self.hits(),
263+
"size": self._cache.__len__()
264+
}
265+
252266
def __getitem__(self, key):
253267
self._reads += 1
254268
res = self._cache.__getitem__(key)
@@ -324,6 +338,20 @@ def clear(self):
324338
# NOTE not passed on cache2…
325339
return self._cache.clear()
326340

341+
def stats(self):
342+
return {
343+
"type": 2,
344+
"cache1": self._cache.stats(), # type: ignore
345+
"cache2": self._cache2.stats() # type: ignore
346+
}
347+
348+
def hits(self):
349+
data = self.stats()
350+
c1, c2 = data["cache1"], data["cache2"]
351+
if c1["type"] == 1 and c2["type"] == 1:
352+
return float(c1["hits"] + c2["hits"]) / max(c1["reads"] + c2["reads"], 1)
353+
# else None
354+
327355
def reset(self): # pragma: no cover
328356
self._cache.reset() # type: ignore
329357
self._cache2.reset() # type: ignore
@@ -484,7 +512,7 @@ def clear(self): # pragma: no cover
484512
"""Flush MemCached contents."""
485513
return self._cache.flush_all() # type: ignore
486514

487-
def hits(self) -> float:
515+
def hits(self):
488516
"""Return overall cache hit ratio."""
489517
stats = self._cache.stats() # type: ignore
490518
return float(stats[b"get_hits"]) / max(stats[b"cmd_get"], 1)
@@ -586,6 +614,9 @@ def info(self, *args, **kwargs):
586614
"""Return redis informations."""
587615
return self._cache.info(*args, **kwargs)
588616

617+
def stats(self):
618+
return self.info(section="stats")
619+
589620
def dbsize(self, *args, **kwargs):
590621
"""Return redis database size."""
591622
return self._cache.dbsize(*args, **kwargs)
@@ -608,7 +639,7 @@ def delete(self, index):
608639
# stats
609640
def hits(self):
610641
"""Return cache hits."""
611-
stats = self.info(section="stats")
642+
stats = self.stats()
612643
return float(stats["keyspace_hits"]) / (
613644
stats["keyspace_hits"] + stats["keyspace_misses"]
614645
)

Makefile

+2-2
Original file line numberDiff line numberDiff line change
@@ -73,8 +73,8 @@ check.pytest: venv
7373
check.coverage: venv
7474
source venv/bin/activate
7575
coverage run -m $(PYTEST) $(PYTOPT) test.py
76-
coverage html $(MODULE).py
77-
coverage report --fail-under=100 --include=CacheToolsUtils.py
76+
# coverage html $(MODULE).py
77+
coverage report --fail-under=100 --precision=1 --show-missing --include=CacheToolsUtils.py
7878

7979
.PHONY: check.docs
8080
check.docs: venv

README.md

+4-3
Original file line numberDiff line numberDiff line change
@@ -31,12 +31,13 @@ with `cachetools`.
3131
class for Memcached.
3232
- `DictCache` a very simple `dict` cache.
3333

34-
### Other wrappers to extend cache capabilities
34+
### Wrappers to extend cache capabilities
3535

3636
- `PrefixedCache`, `PrefixedMemCached` and `PrefixedRedisCache` add a prefix to
3737
distinguish sources on a shared cache.
38-
- `StatsCache`, `StatsMemCached` and `StatsRedisCache` add a `hits()` method
39-
to report the cache hit rate.
38+
- `StatsCache`, `MemCached` and `RedisCache` add a `hits` method
39+
to report the cache hit rate, `stats` to report statistics and
40+
`reset` to reset statistics.
4041
- `LockedCache` use a (thread) lock to control cache accesses.
4142
- `TwoLevelCache` allows to combine two caches.
4243
- `DebugCache` to trace cache calls using `logging`.

docs/VERSIONS.md

+1
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ Install [package](https://pypi.org/project/CacheToolsUtils/) from
2121

2222
## ? on ?
2323

24+
Add `stats` method to return a convenient `dict` of statistics.
2425
Fix doc typo.
2526

2627
## 8.6 on 2024-08-03

test.py

+19-2
Original file line numberDiff line numberDiff line change
@@ -139,6 +139,7 @@ def test_stats_ct():
139139
assert cache[(4, "a", True)] == 114
140140
assert cache[(0, None, False)] == -20
141141
assert cache.hits() > 0.8
142+
assert isinstance(cache.stats(), dict)
142143
cache.clear()
143144
setgetdel(c0)
144145
setgetdel(cache)
@@ -189,6 +190,7 @@ def test_stats_memcached():
189190
assert c1["(1, 'a', True)"] == 111
190191
assert c1["(3, None, False)"] == -17
191192
assert c1.hits() > 0.0
193+
assert isinstance(c1.stats(), dict)
192194
setgetdel(c0)
193195
setgetdel(c1)
194196

@@ -247,6 +249,7 @@ def test_stats_redis():
247249
assert c1[(1, "a", True)] == 111
248250
assert c1[(3, None, False)] == -17
249251
assert c1.hits() > 0.0
252+
assert isinstance(c1.stats(), dict)
250253
setgetdel(c1)
251254

252255

@@ -263,6 +266,7 @@ def test_stacked_redis():
263266
run_cached(c3)
264267
assert len(c3) >= 50
265268
assert c2.hits() > 0.0
269+
assert isinstance(c2.stats(), dict)
266270
setgetdel(c1)
267271
setgetdel(c2)
268272
setgetdel(c3)
@@ -280,7 +284,11 @@ def test_two_level_small():
280284
assert len(c0s) == 50
281285
assert c0s._reads == c1s._reads
282286
assert c1s.hits() == 0.0
287+
assert isinstance(c1s.stats(), dict)
283288
assert c0s.hits() > 0.8
289+
assert isinstance(c0s.stats(), dict)
290+
assert c2.hits() > 0.0
291+
assert isinstance(c2.stats(), dict)
284292
c2.clear()
285293
setgetdel(c0)
286294
setgetdel(c1)
@@ -292,7 +300,7 @@ def test_two_level_small():
292300
def test_two_level_ok():
293301
# front cache is too small, always fallback
294302
c0 = ct.LRUCache(200)
295-
c1 = ct.MRUCache(100)
303+
c1 = ct.LFUCache(100)
296304
c0s = ctu.StatsCache(c0)
297305
c1s = ctu.StatsCache(c1)
298306
c2 = ctu.TwoLevelCache(c1s, c0s)
@@ -304,7 +312,11 @@ def test_two_level_ok():
304312
assert c0s._writes == 0
305313
assert c1s._reads == 500
306314
assert c1s.hits() == 0.9
315+
assert isinstance(c1s.stats(), dict)
307316
assert c0s.hits() == 1.0
317+
assert isinstance(c0s.stats(), dict)
318+
assert c2.hits() > 0.0
319+
assert isinstance(c2.stats(), dict)
308320
c2.clear()
309321
setgetdel(c0)
310322
setgetdel(c1)
@@ -351,6 +363,7 @@ def test_methods():
351363
n = s.sum_n2(i) + s.sum_n1(i)
352364
assert len(c) == 259
353365
assert cs.hits() > 0.6
366+
assert isinstance(cs.stats(), dict)
354367
ctu.cacheMethods(cs, s, sum_n1="x.")
355368
try:
356369
ctu.cacheMethods(cs, s, no_such_method="?.")
@@ -374,6 +387,7 @@ def test_functions():
374387
n = sum_n2(i) + sum_n2(i) + sum_n2(i)
375388
assert len(c) == 129
376389
assert cs.hits() > 0.7
390+
assert isinstance(cs.stats(), dict)
377391

378392

379393
def test_corners():
@@ -463,17 +477,20 @@ def cached(s: str, i: int):
463477
cached("hello", 4)
464478
assert cached.cache_in("hello", 4)
465479
assert cache.hits() == 0.5
480+
assert isinstance(cache.stats(), dict)
466481
cached.cache_del("hello", 4)
467482
assert not cached.cache_in("hello", 4)
468483

469484
def test_debug():
470485
log = logging.getLogger("debug-test")
471486
log.setLevel(logging.DEBUG)
472-
cache = ctu.DebugCache(ctu.DictCache(), log, "test_debug")
487+
cache = ctu.DebugCache(ctu.StatsCache(ctu.DictCache()), log, "test_debug")
473488
run_cached(cache)
474489
cache["Hello"] = "World!"
475490
assert "Hello" in cache
476491
assert len(cache) > 0
492+
assert cache.hits() > 0.0
493+
assert isinstance(cache.stats(), dict)
477494
has_hello = False
478495
for k in iter(cache):
479496
if k == "Hello":

0 commit comments

Comments
 (0)