Decorator-first caching for graphs of expensive Python computations.
EvalCache turns calls to pure functions into deferred expression nodes. The result of every node can be reused from memory or a persistent cache, including intermediate nodes shared by several computations.
python -m pip install evalcacheimport evalcache
evalcache.configure(
cache_store=evalcache.DirectoryCacheStore(".evalcache"),
)
@evalcache.operation
def preprocess(source: str) -> list[int]:
print("preprocessing", source)
return [1, 2, 3]
@evalcache.operation
def total(values: list[int]) -> int:
return sum(values)
result = total(preprocess("model.step"))
# Decorated calls build a graph and return Deferred values.
assert isinstance(result, evalcache.Deferred)
print(result.compute())On the first run, EvalCache evaluates and stores both operations. A fresh
process can restore their results from .evalcache when their operation
identity and arguments have not changed.
The default serializer uses pickle. Only open cache directories trusted by the current user.
By default, EvalCache derives an operation id from the function's module and qualified name and derives a version from its Python implementation. For long-lived caches, explicit identity makes invalidation intentional:
@evalcache.operation(
operation_id="my-project.build-mesh",
operation_version="3",
result=Mesh,
)
def build_mesh(source: Source) -> Mesh:
...Keep operation_id stable while the operation retains the same meaning.
Increment operation_version when old cached results must no longer be used.
A return annotation normally supplies the result contract; result= can
instead accept a runtime type or a ResultSpec with a validator or custom
serializer.
Domain arguments must have deterministic identities. Register an encoder with
HashRegistry or implement __evalcache_key__() -> bytes:
hashes = evalcache.HashRegistry()
hashes.register(Source, lambda source: source.digest.encode("ascii"))
@evalcache.operation(hash_registry=hashes)
def convert(source: Source) -> Mesh:
...See advanced_decorators.py for operation versions, validation, domain hashing, progress events, and non-cacheable operations.
Module-level @evalcache.operation uses the default evaluator at call time.
configure() replaces its global policies without redecorating functions.
For libraries or isolated jobs, own the evaluator explicitly:
evaluator = evalcache.Evaluator(
cache_store=evalcache.DirectoryCacheStore(".evalcache"),
cache_policy=evalcache.CachePolicy(namespace="geometry"),
)
@evaluator.operation
def triangulate(shape: Shape) -> Mesh:
...using_evaluator() temporarily changes the default evaluator. Deferred
values owned by different evaluators cannot be mixed in one graph.
CachePolicy controls persistent reads, writes, namespaces, and corrupt
record recovery. In-memory reuse within an evaluator remains enabled even when
CachePolicy.disabled() disables persistent caching.
Arguments may contain other Deferred values inside lists, tuples, sets, frozensets, and mappings. Operators with unambiguous lazy semantics create new expression nodes without computing their operands:
@evalcache.operation
def load_values() -> tuple[int, ...]:
return (6, -4, 3)
values = load_values()
result = 10 + values[0] * 2 - abs(values[1])
assert result.compute() == 18Unary +, -, abs, and ~ are supported, together with arithmetic,
matrix, bitwise, shift, reflected binary operators, and indexing. Comparisons,
truth testing, and implicit iteration require an explicit .compute().
See expression_tree.py and operators.py.
A file-producing operation can return immutable contents while leaving the destination path outside expression identity:
@evalcache.operation(
operation_id="my-project.render-report",
operation_version="1",
result=evalcache.file_artifact_result(),
)
def render_report(value: int) -> evalcache.FileArtifact:
return evalcache.FileArtifact(
name="report.txt",
data=("result={}\n".format(value)).encode("utf-8"),
media_type="text/plain",
)
report = render_report(42)
report.materialize("first.txt")
report.materialize("second.txt")FileArtifact.from_path() snapshots a backend that can only produce a file.
materialize() atomically replaces the selected destination. The current
serializer stores artifact bytes inside the cache record.
See file_artifact.py.
Decorators are the normal entry point. Applications that need explicit domain
handles may construct Expression[T] values and submit or evaluate them with
Evaluator. Storage and serialization are replaceable through the
CacheStore and Serializer protocols; MemoryCacheStore,
DirectoryCacheStore, and MappingCacheStore are included.
The original implementation remains available under evalcache.legacy:
from evalcache.legacy import DirCache, Lazy
lazy = Lazy(cache=DirCache(".evalcache"))
@lazy
def calculate(value):
return value * 2
assert calculate(21).unlazy() == 42The historical top-level imports remain as compatibility aliases, but new code
should use @evalcache.operation. See Legacy API and migration
and the legacy examples.
EvalCache is distributed under the MIT License.
Contact: mirmik (mirmikns@yandex.ru)