Typed Disk Cache: opt.cache¶
Alfred invokes a Script Filter on every keystroke. If your handler calls an expensive operation — a network request, a disk scan, a slow computation — the result should be cached between invocations so Alfred stays responsive.
TypedCache provides a persistent, disk-backed
cache with a type-hint-safe memoize decorator. It requires the afwf[cache]
extra (diskcache >= 5.4.0).
Why TypedCache Instead of diskcache Directly¶
diskcache.Cache ships its own memoize decorator, but applying it erases
the decorated function’s type hints — IDE auto-complete and static analysis stop
working on the wrapped function.
TypedCache inherits everything from
diskcache.Cache and adds a single method,
typed_memoize(), that wraps
cache.memoize() in a way that preserves the original function’s signature.
Everything else — cache location, eviction, expiry — is standard diskcache.
Basic Usage¶
Create a TypedCache instance at module level (once per process) and
decorate the expensive function:
from afwf.opt.cache.api import TypedCache
from afwf.paths import path_enum
cache = TypedCache(path_enum.dir_afwf / ".cache")
@cache.typed_memoize(expire=60)
def fetch_results(query: str) -> list[str]:
# slow network call or heavy computation here
...
The first call with a given query runs the function and stores the result.
Subsequent calls with the same query within 60 seconds return the cached
value without executing the function body.
typed_memoize() Parameters¶
typed_memoize passes all arguments through to diskcache.Cache.memoize.
The most commonly used ones:
Parameter |
Default |
Meaning |
|---|---|---|
|
|
Time-to-live in seconds. |
|
|
String tag attached to every entry written by this decorator.
Useful for bulk-invalidating a group of entries:
|
|
|
Override the cache key prefix. By default the function’s qualified name is used. |
|
|
When |
|
|
Tuple of argument names to exclude from the cache key — useful for arguments that change every call but do not affect the result (e.g. a logger or a request context). |
The memoize.py Example¶
The memoize example generates a random integer for a given query key and
caches it for 5 seconds. Repeated queries with the same key return the same
value until the TTL expires, demonstrating that the function body runs only
once per unique key per TTL window:
# -*- coding: utf-8 -*-
"""
Example: Memoize
================
**What it demonstrates**
Shows how to use :func:`afwf.opt.cache.api.TypedCache.typed_memoize` to cache
expensive function results across Alfred invocations. The Script Filter
generates a random integer for a given query key and caches it for 5 seconds,
so repeated queries with the same key return the same value until the TTL
expires. Type ``error`` as the query to trigger a simulated error and see how
:func:`afwf.log_error` writes a traceback to a log file.
"""
import random
import afwf.api as afwf
from afwf.opt.cache.api import TypedCache
from afwf.paths import path_enum
cache = TypedCache(path_enum.dir_afwf / ".cache")
@cache.typed_memoize(tag="memoize", expire=5)
def _get_value(key: str) -> int:
return random.randint(1, 1000)
@afwf.log_error(log_file=path_enum.dir_afwf / "memoize.log")
def main(query: str) -> afwf.ScriptFilter:
query = str(query)
if query.strip() == "error":
raise ValueError("This is a simulated Python error triggered by query='error'")
value = _get_value(query)
sf = afwf.ScriptFilter()
sf.items.append(afwf.Item(title=f"value is {value}"))
return sf
Typing error as the query triggers a deliberate exception so you can
verify that log_error() writes the traceback to the
log file even when the cached path is not taken.
Cache Location Convention¶
Store the cache directory inside path_enum.dir_afwf (~/.alfred-afwf/)
so all workflow-related state lives in one place and can be wiped cleanly:
cache = TypedCache(path_enum.dir_afwf / ".cache")
The directory is created automatically by diskcache on first use.
Cache Invalidation¶
TypedCache inherits the full diskcache.Cache API. The most useful
invalidation methods:
cache.clear() # delete everything
cache.evict(tag="my_tag") # delete all entries with this tag
cache.delete(key) # delete one specific key
In tests, call cache.clear() in the test setup to start from a clean
state — see tests/opt/test_opt_cache.py for the pattern.
Installation¶
pip install "afwf[cache]"
# or with uv:
uv add "afwf[cache]"