Skip to main content

manager: error on missing return signature with SQLite

ID
b919468
date
2026-11-06 22:51:28+00:00
author
Alex Chan <alexc@tailscale.com>
parent
447f672
message
manager: error on missing return signature with SQLite

If we don't have a return signature, we can't create the SQLite cache,
but this manifests as a confusing Pydantic error when we try to create
the TypeAdapter. Replace it with a TypeError explaining that you need
to annotate the return type of the decorated function.
changed files
2 files, 54 additions, 13 deletions

Changed files

src/funkycache/manager.py (10969 → 11579)

diff --git a/src/funkycache/manager.py b/src/funkycache/manager.py
index ae1d0d1..d7c7cfc 100644
--- a/src/funkycache/manager.py
+++ b/src/funkycache/manager.py
@@ -135,8 +135,7 @@ class FunkyCache:
         """
 
         def decorator(func: F) -> F:
-            sig = inspect.signature(func)
-
+            data_type = _return_type(func, self._storage)
             dep_paths = _dep_paths(func, files=[Path(p) for p in (files or [])])
             dep_values = values or []
 
@@ -149,22 +148,15 @@ class FunkyCache:
                 raise TypeError(f"Duplicate cache for {func_name}")
 
             input_sig = _input_signature(dep_paths, dep_values)
-            storage = self._create_storage(
-                func_name,
-                data_type=sig.return_annotation,
-                input_sig=input_sig,
-            )
+            storage = self._create_storage(func_name, data_type, input_sig)
+
             self._known_caches[func_name] = KnownCache(
-                storage,
-                data_type=sig.return_annotation,
-                func_name=func_name,
-                input_sig=input_sig,
-                dep_paths=dep_paths,
-                dep_values=dep_values,
+                storage, data_type, func_name, input_sig, dep_paths, dep_values
             )
 
             @wraps(func)
             def wrapper(*args: P.args, **kwargs: P.kwargs) -> R:
+                sig = inspect.signature(func)
                 bound = sig.bind(*args, **kwargs)
                 bound.apply_defaults()
                 key = bound.arguments
@@ -274,6 +266,30 @@ class FunkyCache:
             known_cache.input_sig = new_input_sig
 
 
+def _return_type(func: F, storage: StorageType) -> Any:
+    """
+    Look up the return annotation of a function.
+    """
+    sig = inspect.signature(func)
+
+    if sig.return_annotation == inspect.Signature.empty:
+        match storage:
+            case "memory":
+                warnings.warn(
+                    f"{func.__name__} has no return annotation, "
+                    "which is required for non-memory storage"
+                )
+            case "sqlite":
+                raise TypeError(
+                    f"{func.__name__} has no return annotation; "
+                    "annotate it to use SQLite storage"
+                )
+            case _:  # pragma: no cover
+                raise TypeError(f"unrecognised storage: {storage!r}")
+
+    return sig.return_annotation
+
+
 def _canonical_spec(func: F) -> str:
     """
     Return a canonical identifier for this function.

tests/test_manager.py (18597 → 19451)

diff --git a/tests/test_manager.py b/tests/test_manager.py
index cd4b969..662674f 100644
--- a/tests/test_manager.py
+++ b/tests/test_manager.py
@@ -467,6 +467,19 @@ class TestMemoryFunkyCache(FunkyCacheTestSuite):
         ):
             FunkyCache(storage="memory", root=tmp_path / ".cache")  # type: ignore
 
+    def test_warns_on_missing_return_signature(self, tmp_path: Path) -> None:
+        """
+        A function cached with memory storage warns if it doesn't have
+        a return signature.
+        """
+        cache = FunkyCache(storage="memory")
+
+        with pytest.warns(match="has no return annotation"):
+
+            @cache.register
+            def double(n: int):  # pragma: no cover
+                return n * 2
+
 
 class TestSQLiteFunkyCache(FunkyCacheTestSuite):
     """
@@ -591,3 +604,15 @@ class TestSQLiteFunkyCache(FunkyCacheTestSuite):
             assert 0, "should not be called"
 
         assert initial("apple") == "A"
+
+    def test_errors_on_missing_return_signature(self, tmp_path: Path) -> None:
+        """
+        A function cached with SQLite storage must have a return signature.
+        """
+        cache = FunkyCache(storage="sqlite", root=tmp_path / ".cache")
+
+        with pytest.raises(TypeError, match="has no return annotation"):
+
+            @cache.register
+            def double(n: int):  # pragma: no cover
+                return n * 2