Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -208,6 +208,9 @@ dev = [
"pytest-markdown-docs>=0.6.0",
"pytest-redis>=3.0.0",
"pymemcache>=4.0.0",
# Competitive comparison suite (tests/competitive/ benchmarks cachekit vs these)
"cachetools>=5.3.0",
"aiocache>=0.12.0",
# Code quality
"basedpyright>=1.32.1",
"ruff>=0.6.0",
Expand Down
4 changes: 2 additions & 2 deletions src/cachekit/backends/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -32,9 +32,9 @@ class BaseBackend(Protocol):

Example:
>>> from cachekit.backends import BaseBackend, RedisBackend
>>> backend = RedisBackend()
>>> isinstance(backend, BaseBackend) # Runtime checkable protocol
>>> issubclass(RedisBackend, BaseBackend) # structural (runtime-checkable) protocol
True
>>> backend = RedisBackend() # doctest: +SKIP
>>> backend.set("key", b"value", ttl=60) # doctest: +SKIP
>>> data = backend.get("key") # doctest: +SKIP
"""
Expand Down
1 change: 0 additions & 1 deletion src/cachekit/reliability/metrics_collection.py
Original file line number Diff line number Diff line change
Expand Up @@ -110,7 +110,6 @@ def get_all_metrics() -> dict[str, dict[str, Any]]:
"""Get all collected metrics.

Examples:
>>> clear_metrics() # Start fresh
>>> counter = MetricsCollector("api_calls")
>>> counter.inc()
>>> metrics = get_all_metrics()
Expand Down
61 changes: 31 additions & 30 deletions tests/competitive/test_head_to_head.py
Original file line number Diff line number Diff line change
Expand Up @@ -33,12 +33,11 @@
import math
import time
import uuid
from datetime import datetime, timedelta, timezone
from datetime import datetime, timezone
from functools import lru_cache
from typing import Any

import pytest
import time_machine
from cachetools import TTLCache, cached

from cachekit import cache
Expand Down Expand Up @@ -210,9 +209,10 @@ def test_tuple_preservation(self):

lru_cache: preserves (in-memory, no serialization)
cachetools: preserves (in-memory, no serialization)
cachekit L1-only: DOES NOT preserve — serializes via MessagePack which
converts tuples to lists, even in L1-only mode. This is a known
tradeoff: consistent serialization behavior regardless of backend.
cachekit L1-only (backend=None): preserves — the L1 cache stores native
object references with no serialization, so tuples survive. MessagePack
serialization (which converts tuples to lists) only happens when values
are written out to an L2 backend.
"""

def fn():
Expand All @@ -224,9 +224,9 @@ def fn():

assert isinstance(lru_result, tuple), "lru_cache preserves tuples"
assert isinstance(ct_result, tuple), "cachetools preserves tuples"
# cachekit serializes even in L1-only mode — tuples become lists
assert isinstance(ck_result, list), "cachekit converts tuples to lists via MessagePack"
assert ck_result == [1, 2, 3]
# cachekit L1-only stores native objects (no serialization) — tuples preserved
assert isinstance(ck_result, tuple), "cachekit L1-only preserves tuples (native object cache)"
assert ck_result == (1, 2, 3)

def test_set_preservation(self):
"""Set type preservation.
Expand Down Expand Up @@ -264,7 +264,8 @@ def fn():
def test_nested_dict_of_lists_of_tuples(self):
"""Complex nested structure preservation.

cachekit serializes even in L1 mode, so inner tuples become lists.
cachekit L1-only stores native objects (no serialization), so inner
tuples are preserved just like lru_cache and cachetools.
"""

def fn():
Expand All @@ -277,8 +278,8 @@ def fn():
# lru_cache and cachetools preserve (in-memory, no serialization)
assert isinstance(lru_result["users"][0], tuple)
assert isinstance(ct_result["users"][0], tuple)
# cachekit serializes — tuples become lists
assert isinstance(ck_result["users"][0], list)
# cachekit L1-only stores native objects — inner tuples preserved
assert isinstance(ck_result["users"][0], tuple)


class TestSpecialFloats:
Expand Down Expand Up @@ -558,30 +559,30 @@ def fn(x):
assert call_count == 2 # Re-executed after TTL

def test_cachekit_has_ttl(self):
"""cachekit supports TTL with decorator parameter."""
call_count = 0

with time_machine.travel(0, tick=False) as traveller:
"""cachekit supports TTL with decorator parameter.

@cache(backend=None, ttl=2)
def fn(x):
nonlocal call_count
call_count += 1
return x * 2
Uses a real (short) sleep rather than time_machine: cachekit's L1 cache
keys expiry on time.time(), and a real-time wait reliably exercises it
without coupling to clock-patching internals.
"""
call_count = 0

fn(1)
first_count = call_count
@cache(backend=None, ttl=1)
def fn(x):
nonlocal call_count
call_count += 1
return x * 2

fn(1)
# May or may not cache depending on L1 implementation details
second_count = call_count
fn(1)
fn(1) # cached: no re-execution
second_count = call_count

traveller.shift(timedelta(seconds=3)) # Advance clock past TTL
fn(1)
# After TTL expiry, function MUST re-execute
assert call_count > second_count, "Function should re-execute after TTL expires"
time.sleep(1.2) # wait past the 1s TTL
fn(1)
# After TTL expiry, the function MUST re-execute
assert call_count > second_count, "Function should re-execute after TTL expires"

fn.cache_clear()
fn.cache_clear()


class TestCacheManagement:
Expand Down
22 changes: 22 additions & 0 deletions uv.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading