xdol.memoize#
Persistent memoization with record/replay modes, for costly calls.
A transparent wrapper that persists a function’s results in a MutableMapping keyed by
a stable hash of the call. It is a VCR cassette for any function, not just HTTP: the
first run records, every later run replays from the store – no call, no cost, no network.
The driving use is tests of code that calls paid APIs (LLMs, image/video generation),
which must stay free and fast, but it serves any expensive, deterministic-enough call.
>>> import tempfile
>>> calls = []
>>> @memoize_calls(store=tempfile.mkdtemp(), mode="record")
... def slow_square(x):
... calls.append(x)
... return {"result": x * x}
>>> slow_square(3), slow_square(3)
({'result': 9}, {'result': 9})
>>> calls
[3]
Replay is the default; recording is an opt-in. Under the default mode=None a call
whose key is not in the store raises CacheMiss instead of calling the function,
unless the run sets the XDOL_MEMOIZE_RECORD environment variable (see
recording_enabled()). A key that quietly moved would otherwise read as a normal,
billable test run. Spending is an act that carries a flag. For a plain memoizer that
records on a miss, pass mode="record" (as above).
Four pieces, each a keyword argument with a good default (a seam):
key_func– input to key; defaultxdol.keys.robust_key()(see alsodrop=);codec– result to bytes;"json"(default),"text","bytes"for binary assets, or"paired"for a(metadata, asset_bytes)result stored as<key>.json+<key>.bin(seexdol.x_codecs);store– a directory path (a stable, committable folder: seefile_cache_store()), or anyMutableMapping. Pass a bytes store such ass3dol.s3_store(bucket)and the codec is applied on top, so swapping the filesystem for S3 changes nothing at the call site;mode–"replay","record","refresh"or"off"; seeMODES.
Cache the top-level composite: wrap the whole prompt -> JSON -> asset URL ->
downloaded bytes pipeline, not each intermediate API step, so one intent maps to one
final result. Nothing enforces this; it keeps the surface area (and the number of
cassettes to review) small.
The wrapper preserves the function’s signature and metadata (functools.wraps) and
supports coroutine functions. Exceptions raised by the function are never cached.
Module Attributes
Set to a truthy value to let |
|
Environment-variable spellings that count as "on". |
|
Errors that mean "this stored entry cannot be decoded" (as opposed to a store/network failure, which must never be mistaken for a missing entry and cost a real call). |
Functions
|
A persistent, atomic, codec-aware |
|
Persistently memoize |
|
True when the environment variable |
Classes
|
A bytes store over a directory whose writes are atomic. |
Exceptions
A stored entry exists but cannot be decoded (replay mode refuses to guess). |
|
A memoized call missed its store while recording was disabled. |
|
|
The function ran but its result could not be stored (e.g. not JSON-serializable). |
- class xdol.memoize.AtomicFiles(*args, delete_func=<built-in function remove>, **kwargs)[source]#
Bases:
FilesA bytes store over a directory whose writes are atomic.
Plain file writes can leave a truncated cassette if the process dies mid-write, which then fails to decode (or worse, decodes). Here each value is written to a temporary sibling and renamed into place (
os.replace), and temporary files are invisible to iteration. Missing sub-directories of a key ("a/b") are created.
- exception xdol.memoize.CacheCorrupt[source]#
Bases:
RuntimeErrorA stored entry exists but cannot be decoded (replay mode refuses to guess).
- exception xdol.memoize.CacheMiss[source]#
Bases:
LookupErrorA memoized call missed its store while recording was disabled.
The alternative to this exception is a real, possibly billable call nobody asked for. Two things cause it: an input the store has genuinely never seen, and a cache key that moved under an unchanged input – usually because the key was computed over something incidental (a rendered global registry, a timestamp, a path).
- exception xdol.memoize.CacheWriteError(message, result)[source]#
Bases:
RuntimeErrorThe function ran but its result could not be stored (e.g. not JSON-serializable).
The result is not lost: it is the
resultattribute.
- xdol.memoize.DFLT_CORRUPT_ERRORS = (<class 'xdol.x_codecs.CodecDecodeError'>,)#
Errors that mean “this stored entry cannot be decoded” (as opposed to a store/network failure, which must never be mistaken for a missing entry and cost a real call).
- xdol.memoize.RECORD_ENV = 'XDOL_MEMOIZE_RECORD'#
Set to a truthy value to let
mode=Nonecalls record on a miss.
- xdol.memoize.TRUTHY_ENV_VALUES = frozenset({'1', 'on', 'true', 'yes'})#
Environment-variable spellings that count as “on”.
- xdol.memoize.file_cache_store(rootdir, *, codec='json')[source]#
A persistent, atomic, codec-aware
MutableMappingover a directory.Keys are filenames, values are decoded objects.
rootdiris created if missing and is meant to be a stable folder you commit (a cassette directory), not a temp dir.codec="paired"stores(metadata, asset_bytes)values as<key>.json+<key>.bin.- Return type:
>>> import tempfile >>> store = file_cache_store(tempfile.mkdtemp()) >>> store["k"] = {"a": 1} >>> store["k"], list(store) ({'a': 1}, ['k'])
- xdol.memoize.memoize_calls(func=None, *, store, codec=None, key_func=None, drop=(), mode=None, default_mode='replay', record_env='XDOL_MEMOIZE_RECORD', corrupt_errors=(<class 'xdol.x_codecs.CodecDecodeError'>, ), on_write_error='raise', copy_results=None, reread_after_write=True)[source]#
Persistently memoize
funcintostore(record/replay).Usable as
memoize_calls(fn, store=...)or as a decorator@memoize_calls(store=...).- Parameters:
func (
Callable|None) – the (costly) function; wrap the composite top-level call.store – a directory path (->
file_cache_store()), or aMutableMapping.codec –
None(default) means a path gets"json"and a mapping is used as it is (its values are the results). Otherwisestoreis a bytes mapping and this is applied on top:"json","text","bytes","paired", axdol.x_codecs.Codec, or an(encode, decode)pair.key_func (
Callable[...,str] |None) – input to key. Defaultxdol.keys.robust_key().drop (
Iterable[str]) – parameter names offuncto leave out of the key (callbacks, loggers,self); builds the key withxdol.keys.make_key_func(), which also binds calls to the signature. Not combinable withkey_func.mode (
Union[Literal['replay','record','refresh','off'],Callable[[],Literal['replay','record','refresh','off']],None]) – one ofMODES, a zero-argument callable returning one (evaluated on every call), orNone(default) for:"record"if therecord_envvariable is truthy, elsedefault_mode. Read at call time, so setting the variable after import (e.g. in a pytest hook) works.default_mode (
Literal['replay','record','refresh','off']) – whatmode=Nonemeans when the environment does not opt in."replay"(default) makes spending an explicit act.record_env (
str) – name of the opt-in environment variable.corrupt_errors (
tuple[type[BaseException],...]) – exception types raised by reading an entry that mean “undecodable” (default:xdol.x_codecs.CodecDecodeError, which the codecs raise; add your own for an object-valued store that can fail to load). In replay mode they raiseCacheCorrupt; when recording, the entry is recomputed and overwritten, with a warning. Any other error – a network failure, a misconfigured store – propagates and never costs a real call.copy_results (
bool|None) – deep-copy values read from the store, so callers cannot mutate the cache.None(default) means: yes whenstoreis an object-valued mapping used as is (a codec makes a fresh object per read anyway).reread_after_write (
bool) – after recording, return the value as read back from the store, so a recording run and a replay run return the same thing (JSON turns tuples into lists and int keys into strings). Turn off to skip the extra read of a large asset.on_write_error (
Literal['raise','warn']) – what to do if a fresh result cannot be stored:"raise"CacheWriteError(carrying the result) or"warn"and return it.
- Return type:
The returned wrapper has attributes
cache_store,key_func,cache_key(\*args, \*\*kwargs)andcache_mode()(the mode that would apply now).
- xdol.memoize.recording_enabled(env='XDOL_MEMOIZE_RECORD')[source]#
True when the environment variable
envis set to a truthy value.The opt-in is environment-level rather than per-call, so “this run may spend” is one visible fact about the run instead of a property of each test.
- Return type:
>>> recording_enabled("XDOL_SURELY_UNSET_VARIABLE") False