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