Architecture
This page documents how reliev works under the hood. Nothing here is part of the public API; it exists to help contributors understand the single module the library consists of: store.py.
Two views of one state
A Store holds the same underlying state behind two observ proxies:
self._present— areactive(writable) proxy, the store's private handle for applying changes;self.state— areadonlyproxy over the same target, the public handle. Reads through it are dependency-tracked; writes raise.
Both wrap the same object, so a change applied through _present is immediately visible through state, and notifies exactly the watchers that depend on the touched locations.
The undo/redo stacks, _past and _future, are shallow_reactive lists of HistoryEntry tuples. Shallow is deliberate: pushes and pops must be observable (they drive can_undo, can_redo and the context properties), but the patch operations inside an entry are never mutated, so there is no reason to pay for deep proxying them.
Anatomy of a mutation call
The wrapper installed by @mutation runs each call in five steps:
- Reentrancy check. If the store is already inside a mutation (
_in_mutation), the method body runs directly against the current draft and returns — this is what collapses nested mutations into the outer entry, and why nestedstrictandcontextsettings are ignored. - Context resolution. A
mutation_contextkeyword passed by the caller wins; otherwise a callablecontextis invoked with the call's own arguments; otherwise the staticcontextvalue is used as-is. - Recording. The body is wrapped in a recipe and handed to patchdiff's
produce(self._present, recipe=recipe, in_place=True). Produce proxies the draft, runs the recipe, and returns the forward and reverse patch lists. For the duration of the recipe,self.stateis swapped to the writable draft so the method body can write through the same attribute it normally reads from; afinallyswaps the readonly proxy back even if the body raises.in_place=Truematters twice: the store wants the real state mutated (not a copy), and it keeps patchdiff writing through the reactive proxy so observ sees every change. - History bookkeeping. If any patches were recorded, a
HistoryEntry(ops, reverse_ops, context)is appended to_pastand_futureis cleared — a new change invalidates previously undone ones. If nothing was recorded, strict mode (the decorator's setting, falling back to the store's) decides between raising and ignoring. - Restore.
self.statepoints back at the readonly proxy.
Undo and redo
undo() pops the newest entry off _past, applies its reverse_ops to _present with patchdiff's iapply, and pushes the entry onto _future; redo() mirrors it with ops. Since iapply writes through the reactive proxy, undo/redo trigger exactly the same fine-grained notifications the original mutation did. Entries keep both patch directions forever, so walking the history any number of times costs only patch application — no diffing, no snapshots.
Computed properties
@computed replaces a method with a stdlib property. Its getter lazily creates one observ computed expression per instance — computed_expression(deep=deep)(partial(fn, self)) — and caches it in the instance __dict__ under a name derived from the method. Later accesses reuse the cached expression, which only re-evaluates when its reactive dependencies changed. Using a plain property keeps semantics unsurprising: assignment raises AttributeError, isinstance(cls.__dict__[name], property) holds, and there is no custom descriptor to learn.
For typing purposes the decorator is declared to return a ComputedProperty[T] protocol, so type checkers see attribute access typed as the getter's return type; at runtime the object really is just a property.
Division of labor
Reliev itself is intentionally thin. The heavy lifting lives upstream:
- observ implements the proxies, dependency tracking and scheduling;
- patchdiff implements patch recording (
produce) and application (iapply).
What reliev adds is the transactional glue: one decorator that turns method calls into history entries, and a store that knows how to walk them.