Manage nested objects¶
Roots define the entry points to a frame. Traversal follows managed fields, including node references inside supported containers. Ordinary fields and caches do not establish membership. A manager can have multiple roots; their graph is committed and navigated together.
from yidl_undoable import UndoRedoManager, ObjectAbsentError, managed, undoable
@undoable
class Item:
text: str = managed(default="first")
@undoable
class Document:
items: tuple[Item, ...] = managed(default=(), thaw=list, freeze=tuple)
history = UndoRedoManager()
child = Item(transaction_manager=history)
document = Document(items=(child,), transaction_manager=history)
history.initialize(document)
with history.begin():
document.working.items.clear()
assert not child.present
try:
child.text
except ObjectAbsentError:
pass
else:
raise AssertionError("absent managed read should fail")
assert history.undo() and child.present and child.text == "first"
The Python child handle still exists after removal. Its managed presence and reads
follow the selected frame. By default an absent managed read raises; management
fields remain accessible. UndoRedoManager(absent_policy=ReadLastVisible()) instead
permits reads from the last visible surviving frame at or before the cursor,
with descendants pinned read-only to that fallback. Neither policy permits absent
managed writes.
Create and attach during an edit¶
with history.begin():
second = Item(text="second", transaction_manager=history)
document.working.items.append(second)
assert len(document.items) == 2
assert history.undo() and len(document.items) == 1
assert history.redo() and document.items[1].text == "second"
After initialization, construction requires a transaction. New objects are
provisional until attached by a managed edge or add_root; unattached provisional
objects are not published. Aliases are allowed; cycles and cross-manager edges
are rejected. This is an acyclic graph, not necessarily a strict tree.
Several roots¶
other = UndoRedoManager()
a = Item(text="A", transaction_manager=other)
b = Item(text="B", transaction_manager=other)
c = Item(text="C", transaction_manager=other)
other.initialize(a, b, c)
assert other.roots == (a, b, c)
with other.begin():
other.remove_root(b)
assert other.undo() and b.present
Call initialize(*roots) once. Later use add_root/remove_root inside an edit.
The proposed undoable_leaf helper is not implemented. Objects excluded from the
initial graph, aborted creations, or objects existing only in a future frame at an
earlier cursor have no usable baseline there; they cannot simply be resurrected.
See snapshots for intentional historical reads.