all: improve the documentation and comments
- ID
11d6b1c- date
2026-11-07 06:58:25+00:00- author
Alex Chan <alexc@tailscale.com>- parent
34264eb- message
all: improve the documentation and comments- changed files
3 files, 80 additions, 2 deletions
Changed files
src/funkycache/__init__.py (249 → 235)
diff --git a/src/funkycache/__init__.py b/src/funkycache/__init__.py
index 954332b..98069a0 100644
--- a/src/funkycache/__init__.py
+++ b/src/funkycache/__init__.py
@@ -1,5 +1,5 @@
"""
-funkycache is a SQLite-backed function cache that tracks file dependencies.
+funkycache is a function cache that tracks file dependencies.
"""
from .manager import FunkyCache
src/funkycache/manager.py (12972 → 15490)
diff --git a/src/funkycache/manager.py b/src/funkycache/manager.py
index b8f8ae3..e4b0e51 100644
--- a/src/funkycache/manager.py
+++ b/src/funkycache/manager.py
@@ -32,6 +32,10 @@ R = TypeVar("R")
class F[**P, R](Protocol):
"""
A function: that is, a callable that also exposes a __name__ attribute.
+
+ This is the type accepted by register(). It's deliberately narrower
+ than Callable, because the identifier for a cache is derived from
+ a function's __name__, __module__, and __qualname__.
"""
def __call__(self, *args: P.args, **kwargs: P.kwargs) -> R: ... # noqa: D102
@@ -47,20 +51,37 @@ StorageType = Literal["memory", "sqlite"]
class KnownCache[R]:
"""
Tracks the underlying storage for a single function.
+
+ There's one KnownCache for each registered function for the lifetime
+ of the FunkyCache. When a function's dependencies change, the storage
+ is updated and input_sig is updated, and this record is updated in-place.
"""
storage: CacheStorage
data_type: R
+ # func_name is the canonical name of the function, including
+ # module path and function name.
func_name: str
+
+ # input_sig is a hash of everything the function depends on:
+ # the path/content of each dependent file, and each dependent value.
input_sig: str
+ # dep_paths is the set of files and folders the function declared
+ # as dependencies, plus the file the function itself is defined in.
dep_paths: set[Path]
+
+ # dep_values is the list of static values the function declared as
+ # dependencies, in the order they were given.
dep_values: list[Any]
def is_affected_by(self, changed_paths: list[Path]) -> bool:
"""
- Whether this function depends on any of these paths.
+ Whether the cached function depends on any of these paths.
+
+ A function declares on a path if it was declared explicitly,
+ or if the path sits underneath a directory that was declared.
"""
for dp in self.dep_paths:
if dp in changed_paths:
@@ -78,6 +99,19 @@ class FunkyCache:
they depend on. When FunkyCache is notified that a file changes (or it
detects a change when the Python process is restarted), it invalidates
the cache.
+
+ Typical usage looks like this:
+
+ cache = FunkyCache(storage="sqlite", root=".cache")
+
+ @cache.register(files=["content/index.md"])
+ def render_page() -> str:
+ ...
+
+ cache.notify_path_change("content/index.md")
+
+ Functions are cached based on their arguments, so calls must be
+ deterministic for the cache to behave correctly.
"""
_storage: StorageType
@@ -95,6 +129,17 @@ class FunkyCache:
def __init__(
self, *, storage: StorageType = "memory", root: Path | str | None = None
):
+ """
+ Create a new cache manager.
+
+ Arguments:
+ storage: How to store the cached values.
+ "memory" keeps them in-memory for the life of this object.
+ "sqlite" writes them to disk, so they survive process restarts.
+ root: The directory holding persistent artefacts. Only valid
+ SQLite storage.
+
+ """
if storage == "memory" and root is not None:
raise TypeError("Do not specify a root path with memory storage")
@@ -132,6 +177,9 @@ class FunkyCache:
static values. If the files change, use notify_path_change to
invalidate functions that relied on that file. Static values are
presumed to be unchanged for the life of the Python process.
+
+ The function's own source file is always a dependency, so editing
+ the code that produced a value also invalidates it on the next run.
"""
def decorator(func: F) -> F:
@@ -245,6 +293,15 @@ class FunkyCache:
) -> None:
"""
Invalidate cache that depend on this path and/or paths.
+
+ Call this after modifying files that a cached function depends on.
+ The function's cache will be invalidated, and the next call will
+ recompute the value.
+
+ Notification is advisory: it is a hint that something may have
+ changed, but caches are only invalidated when the files they depend
+ on actually change, so notifying about an unrelated or unchanged
+ path is cheap and safe.
"""
# Convert input values to a list of absolute paths.
if isinstance(changed_paths, (Path, str)):
@@ -309,6 +366,9 @@ def _canonical_name(func: F) -> str:
This should uniquely identify a function within a codebase,
and functions with the same name but in different files/classes
should get a different identifier.
+
+ This is used as the key in _known_caches and to identify SQLite caches
+ on disk.
"""
parts = []
src/funkycache/storage.py (6793 → 7577)
diff --git a/src/funkycache/storage.py b/src/funkycache/storage.py
index abfad40..0090a28 100644
--- a/src/funkycache/storage.py
+++ b/src/funkycache/storage.py
@@ -1,5 +1,8 @@
"""
Backing storage for cache, which records the input/output of Python functions.
+
+This module doesn't know which function a cache belongs to; it only maps
+a collection of arguments to the return value.
"""
from abc import ABC, abstractmethod
@@ -19,6 +22,13 @@ class CannotOverwriteValueException(ValueError):
"""
Thrown when you try to overwrite an existing cache key with
a different value.
+
+ A key which already exists with the same value can be written again
+ harmlessly, but a different value is an error.
+
+ This exception means the cached function is non-deterministic: the
+ same input returned two different outputs. This is a bug in the
+ cached function, and cannot be fixed by retrying.
"""
def __init__(self, key: Mapping[str, Any]) -> None:
@@ -53,6 +63,8 @@ class MemoryCacheStorage[V](CacheStorage[V]):
"""
In-memory storage for a cache of the input/output of
a single Python function.
+
+ Values only survive until the end of the Python process.
"""
def __init__(self):
@@ -137,6 +149,8 @@ class SQLiteCacheStorage[V](TypedCacheStorage[V]):
"""
SQLite-backed storage for a cache of the input/output of
a single Python function.
+
+ Values are persisted to disk, and survive the end of the Python process.
"""
con: sqlite3.Connection
@@ -185,6 +199,10 @@ class SQLiteCacheStorage[V](TypedCacheStorage[V]):
def _encode_key(self, key: Mapping[str, Any]) -> tuple[str, str]:
"""
Encode a mapping as a string key.
+
+ The function arguments (which may be arbitrary Python objects)
+ are serialised as JSON, then hashed to reduce it to a fixed size.
+ Using fixed-size keys improves SQLite performance.
"""
json_key = json.dumps(key, sort_keys=True, default=str)
return json_key, _hash(json_key)