Skip to main content

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)