storage: add an in-memory implementation of CacheStorage
- ID
6d99bba- date
2026-11-05 07:16:21+00:00- author
Alex Chan <alex@alexwlchan.net>- parent
d51ee4e- message
storage: add an in-memory implementation of CacheStorage This serialises function arguments as a JSON string, casting values that can't be serialised using `json.dumps` to a string. We add an additional test case that such keys can be properly cached.- changed files
2 files, 73 additions, 7 deletions
Changed files
src/funkycache/storage.py (1060 → 2069)
diff --git a/src/funkycache/storage.py b/src/funkycache/storage.py
index 4d62d10..8e5a473 100644
--- a/src/funkycache/storage.py
+++ b/src/funkycache/storage.py
@@ -4,6 +4,7 @@ Backing storage for cache, which records the input/output of Python functions.
from abc import ABC, abstractmethod
from collections.abc import Mapping
+import json
from typing import Any
@@ -13,7 +14,7 @@ class CannotOverwriteValueException(ValueError):
a different value.
"""
- def __init__(self, key: str) -> None:
+ def __init__(self, key: Mapping[str, Any]) -> None:
super().__init__(f"Key already exists: {key!r}")
@@ -39,3 +40,39 @@ class CacheStorage[V](ABC):
Throws a KeyError if you look up a key which isn't in the cache.
"""
+
+
+class MemoryCacheStorage[V](CacheStorage[V]):
+ """
+ In-memory storage for a cache of the input/output of
+ a single Python function.
+ """
+
+ def __init__(self):
+ self._underlying: dict[str, V] = {}
+
+ @staticmethod
+ def _encode_key(key: Mapping[str, Any]) -> str:
+ """
+ Encode a mapping as a string key.
+ """
+ return json.dumps(key, sort_keys=True, default=str)
+
+ def set(self, key: Mapping[str, Any], value: V) -> None:
+ """
+ Save a key-value pair in the cache.
+ """
+ enc_key = self._encode_key(key)
+
+ try:
+ if self._underlying[enc_key] != value:
+ raise CannotOverwriteValueException(key)
+ except KeyError:
+ self._underlying[enc_key] = value
+
+ def get(self, key: Mapping[str, Any]) -> V:
+ """
+ Look up the value of a key in the cache.
+ """
+ enc_key = self._encode_key(key)
+ return self._underlying[enc_key]
tests/test_storage.py (1405 → 2236)
diff --git a/tests/test_storage.py b/tests/test_storage.py
index e1fa30e..626d742 100644
--- a/tests/test_storage.py
+++ b/tests/test_storage.py
@@ -3,11 +3,17 @@ Tests for backend storage implementations.
"""
from abc import ABC, abstractmethod
-from collections.abc import Iterator
+from collections.abc import Iterator, Mapping
+from pathlib import Path
+from typing import Any
import pytest
-from funkycache.storage import CacheStorage, CannotOverwriteValueException
+from funkycache.storage import (
+ CacheStorage,
+ MemoryCacheStorage,
+ CannotOverwriteValueException,
+)
class CacheStorageTestSuite(ABC):
@@ -25,12 +31,21 @@ class CacheStorageTestSuite(ABC):
Return an empty cache storage backend that holds integer values.
"""
- def test_cache(self, storage: CacheStorage[int]) -> None:
+ @pytest.mark.parametrize(
+ "key",
+ [
+ pytest.param({"n": "10"}, id="str"),
+ pytest.param({"n": 10}, id="int"),
+ #
+ # A Path isn't serialisable by json.dumps(…) with the
+ # default arguments, but we still want to cache it.
+ pytest.param({"n": Path("10.txt")}, id="path"),
+ ],
+ )
+ def test_cache(self, storage: CacheStorage[int], key: Mapping[str, Any]) -> None:
"""
Test the basic properties of a cache.
"""
- key = {"n": "10"}
-
fib10 = 55
fib20 = 6765
@@ -43,11 +58,25 @@ class CacheStorageTestSuite(ABC):
assert storage.get(key) == fib10
# Overwriting an existing value is an error.
- with pytest.raises(CannotOverwriteValueException):
+ with pytest.raises(CannotOverwriteValueException) as exc:
storage.set(key, value=fib20)
+ assert repr(key) in exc.value.args[0]
assert storage.get(key) == fib10
# Writing a key with the same value as the already-stored
# value is allowed.
storage.set(key, value=fib10)
+
+
+class TestMemoryCacheStorage(CacheStorageTestSuite):
+ """
+ Tests for MemoryCacheStorage.
+ """
+
+ @pytest.fixture
+ def storage(self) -> Iterator[CacheStorage[int]]:
+ """
+ Return an empty in-memory cache that returns integer values.
+ """
+ yield MemoryCacheStorage[int]()