cachetools TTLCache + LRUCache memoization: working versions (tested 2026-10-15)
Memoize calls with cachetools TTLCache and LRUCache, control expiry with an injectable timer, and cache methods with cachedmethod.
passed6 passed · 0 failed2026-10-15 18:09 UTC- Status
- passed
- Last passed
- 2026-10-15 18:09 UTC
- Tests
- 6 / 6
- Language
- Python
- Runtime
- Python 3.13.16
pinned versions2
cachetools@7.2.1pytest@9.1.1
run it
uv venv -q .venv && uv pip install -q --python .venv/bin/python -r requirements.txt .venv/bin/python -m pytest -q
test environment
Python 3.13.16 linux 6.17.0-1022-azure x64 runner: github-actions 38783276983
The code
Exactly the files that passed. Your agent can pull them with get_recipe("cachetools-ttl-memoize") (install), or see them on GitHub.
requirements.txt
cachetools==7.2.1 pytest==9.1.1
memoize.py
from cachetools import LRUCache, TTLCache, cached, cachedmethod
def build_square(timer, ttl=10.0, maxsize=128):
cache = TTLCache(maxsize=maxsize, ttl=ttl, timer=timer)
stats = {"calls": 0}
@cached(cache)
def square(n):
stats["calls"] += 1
return n * n
return square, cache, stats
def build_row_lookup(rows):
cache = TTLCache(maxsize=8, ttl=60)
@cached(cache, key=lambda row: row["id"])
def lookup(row):
return rows[row["id"]]
return lookup, cache
class RepoClient:
shared = TTLCache(maxsize=32, ttl=60)
def __init__(self, owner):
self.owner = owner
self.calls = 0
@cachedmethod(lambda self: self.shared)
def repo(self, name):
self.calls += 1
return f"{self.owner}/{name}"
def build_lru(maxsize=2):
return LRUCache(maxsize=maxsize)
tests/test_memoize.py
import pytest
from cachetools import TTLCache, cached
from memoize import RepoClient, build_lru, build_row_lookup, build_square
def test_ttl_expiry_recomputes():
now = [100.0]
square, cache, stats = build_square(timer=lambda: now[0], ttl=10.0)
assert square(3) == 9
assert square(3) == 9
assert stats["calls"] == 1
now[0] += 10.0
assert square(3) == 9
assert stats["calls"] == 2
assert cache.currsize == 1
def test_unhashable_args_raise_with_default_key():
cache = TTLCache(maxsize=8, ttl=60)
@cached(cache)
def lookup(row):
return row["id"]
with pytest.raises(TypeError):
lookup({"id": 1})
def test_custom_key_memoizes_unhashable_args():
seen = []
@cached(TTLCache(maxsize=8, ttl=60), key=lambda row: row["id"])
def lookup(row):
seen.append(row["id"])
return row["id"] * 10
assert lookup({"id": 1, "v": "a"}) == 10
assert lookup({"id": 1, "v": "b"}) == 10
assert seen == [1]
def test_row_lookup_stores_one_entry_per_id():
lookup, cache = build_row_lookup({1: "one", 2: "two"})
assert lookup({"id": 1}) == "one"
assert lookup({"id": 2}) == "two"
assert cache.currsize == 2
def test_lru_membership_does_not_refresh_recency():
cache = build_lru(maxsize=2)
cache["a"] = 1
cache["b"] = 2
assert "a" in cache
cache["c"] = 3
assert "a" not in cache
assert set(cache.keys()) == {"b", "c"}
def test_cachedmethod_shared_cache_ignores_self():
RepoClient.shared.clear()
alice, bob = RepoClient("alice"), RepoClient("bob")
assert alice.repo("x") == "alice/x"
assert bob.repo("x") == "alice/x"
assert bob.calls == 0
pytest.ini
[pytest] pythonpath = .
Connect to ProvenPin
Add the free MCP server so your agent can pull this recipe and others like it. Same setup as the docs.
terminal
claude mcp add --transport http provenpin https://mcp.provenpin.com/mcp
~/.cursor/mcp.json
{ "mcpServers": { "provenpin": { "url": "https://mcp.provenpin.com/mcp" } } }.vscode/mcp.json
{ "servers": { "provenpin": { "type": "http", "url": "https://mcp.provenpin.com/mcp" } } }~/.codeium/windsurf/mcp_config.json
{ "mcpServers": { "provenpin": { "serverUrl": "https://mcp.provenpin.com/mcp" } } }MCP settings
{ "mcpServers": { "provenpin": { "type": "streamableHttp", "url": "https://mcp.provenpin.com/mcp" } } }Any other MCP client: Streamable HTTP at https://mcp.provenpin.com/mcp. No API key, no signup.