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

expire

None

Time-to-live in seconds. None means the entry never expires.

tag

None

String tag attached to every entry written by this decorator. Useful for bulk-invalidating a group of entries: cache.evict(tag="my_tag").

name

None

Override the cache key prefix. By default the function’s qualified name is used.

typed

False

When True, f(1) and f(1.0) are cached separately.

ignore

()

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]"