Skip to main content

storage: add the CacheStorage interface and a basic test suite

ID
d51ee4e
date
2026-11-05 06:51:49+00:00
author
Alex Chan <alex@alexwlchan.net>
parent
1ff707a
message
storage: add the CacheStorage interface and a basic test suite

This establishes the basic properties of a CacheStorage class and sets
up a little test suite we can reuse to test all implementations.
changed files
5 files, 98 additions, 12 deletions

Changed files

pyproject.toml (1363 → 1403)

diff --git a/pyproject.toml b/pyproject.toml
index a182912..33ae166 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -51,6 +51,7 @@ filterwarnings = ["error"]
 [tool.ruff.lint]
 select = ["D", "E", "F"]
 ignore = [
+    "D107",  # undocumented-public-init
     "D200",  # unnecessary-multiline-docstring
     "D203",  # incorrect-blank-line-before-class
     "D205",  # missing-blank-line-after-summary

src/funkycache/__init__.py (107 → 219)

diff --git a/src/funkycache/__init__.py b/src/funkycache/__init__.py
index 1268d3d..f904e09 100644
--- a/src/funkycache/__init__.py
+++ b/src/funkycache/__init__.py
@@ -2,4 +2,7 @@
 funkycache is a SQLite-backed function cache that tracks file dependencies.
 """
 
+from .storage import CannotOverwriteValueException
+
 __version__ = "0.0.1"
+__all__ = ["CacheStorage", "CannotOverwriteValueException"]

src/funkycache/storage.py (0 → 1060)

diff --git a/src/funkycache/storage.py b/src/funkycache/storage.py
new file mode 100644
index 0000000..4d62d10
--- /dev/null
+++ b/src/funkycache/storage.py
@@ -0,0 +1,41 @@
+"""
+Backing storage for cache, which records the input/output of Python functions.
+"""
+
+from abc import ABC, abstractmethod
+from collections.abc import Mapping
+from typing import Any
+
+
+class CannotOverwriteValueException(ValueError):
+    """
+    Thrown when you try to overwrite an existing cache key with
+    a different value.
+    """
+
+    def __init__(self, key: str) -> None:
+        super().__init__(f"Key already exists: {key!r}")
+
+
+class CacheStorage[V](ABC):
+    """
+    The interface for the backing storage of a cache which records
+    the input/output of a single Python function.
+    """
+
+    @abstractmethod
+    def set(self, key: Mapping[str, Any], value: V) -> None:
+        """
+        Save a key-value pair in the cache.
+
+        Throws an exception if you overwrite an existing key with
+        a different value.
+        """
+
+    @abstractmethod
+    def get(self, key: Mapping[str, Any]) -> V:
+        """
+        Look up the value of a key in the cache.
+
+        Throws a KeyError if you look up a key which isn't in the cache.
+        """

tests/test_funkycache.py (204 → 0)

diff --git a/tests/test_funkycache.py b/tests/test_funkycache.py
deleted file mode 100644
index 10cc76d..0000000
--- a/tests/test_funkycache.py
+++ /dev/null
@@ -1,12 +0,0 @@
-"""
-Tests for funkycache.
-"""
-
-from funkycache import __version__
-
-
-def test_version() -> None:
-    """
-    Test the version as a way to exercise the test suite.
-    """
-    assert __version__ == "0.0.1"

tests/test_storage.py (0 → 1405)

diff --git a/tests/test_storage.py b/tests/test_storage.py
new file mode 100644
index 0000000..e1fa30e
--- /dev/null
+++ b/tests/test_storage.py
@@ -0,0 +1,53 @@
+"""
+Tests for backend storage implementations.
+"""
+
+from abc import ABC, abstractmethod
+from collections.abc import Iterator
+
+import pytest
+
+from funkycache.storage import CacheStorage, CannotOverwriteValueException
+
+
+class CacheStorageTestSuite(ABC):
+    """
+    A common test suite for all implementations of CacheStorage.
+
+    This should be subclassed as a `test*` class, where you write fixtures
+    and tests that are specific to a single implementation.
+    """
+
+    @abstractmethod
+    @pytest.fixture
+    def storage(self) -> Iterator[CacheStorage[int]]:
+        """
+        Return an empty cache storage backend that holds integer values.
+        """
+
+    def test_cache(self, storage: CacheStorage[int]) -> None:
+        """
+        Test the basic properties of a cache.
+        """
+        key = {"n": "10"}
+
+        fib10 = 55
+        fib20 = 6765
+
+        # Test empty storage
+        with pytest.raises(KeyError):
+            storage.get(key)
+
+        # Store and retrieve a value
+        storage.set(key, value=fib10)
+        assert storage.get(key) == fib10
+
+        # Overwriting an existing value is an error.
+        with pytest.raises(CannotOverwriteValueException):
+            storage.set(key, value=fib20)
+
+        assert storage.get(key) == fib10
+
+        # Writing a key with the same value as the already-stored
+        # value is allowed.
+        storage.set(key, value=fib10)