ProvenPin Install

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

Any other MCP client: Streamable HTTP at https://mcp.provenpin.com/mcp. No API key, no signup.