Atomic Operations
The atomic() context manager enables multiple ObjectState changes to be recorded as a single undo step. This is critical for operations that logically belong together.
Problem: Multiple Snapshots for One Action
Without atomicity, each ObjectState modification records its own snapshot:
# Adding a pipeline step creates 3 separate snapshots:
ObjectStateRegistry.register(step_state) # Snapshot 1: "register step"
pipeline_state.update_parameter(...) # Snapshot 2: "edit step_scope_ids"
ObjectStateRegistry.register(func_state) # Snapshot 3: "register function"
# User must undo 3 times to revert "add step" action!
Solution: atomic() Context Manager
Wrap related operations in atomic() to coalesce them:
from objectstate import ObjectStateRegistry
with ObjectStateRegistry.atomic("add step"):
ObjectStateRegistry.register(step_state)
pipeline_state.update_parameter("step_scope_ids", new_ids)
ObjectStateRegistry.register(func_state)
# Single snapshot "add step" recorded here
# User undoes once to revert entire "add step" action
How It Works
The atomic mechanism uses depth counting to support nesting:
atomic("outer") _atomic_depth = 1
├── register(...) snapshot deferred
├── atomic("inner") _atomic_depth = 2
│ └── update(...) snapshot deferred
└── (inner exits) _atomic_depth = 1, still deferred
(outer exits) _atomic_depth = 0, record "outer" snapshot
Key behaviors:
_atomic_depthtracks nesting level (0 = not in atomic block)While
_atomic_depth > 0, allrecord_snapshot()calls are deferredOnly the outermost block’s label is used for the final snapshot
The snapshot is recorded when
_atomic_depthreturns to 0
API Reference
- classmethod ObjectStateRegistry.atomic(label: str)
Context manager for atomic operations.
- Parameters:
label (str) – Human-readable label for the coalesced snapshot
Example:
with ObjectStateRegistry.atomic("delete step"): # Unregister function ObjectStates ObjectStateRegistry.unregister_scope_and_descendants(func_scope) # Update pipeline's step list pipeline_state.update_parameter("step_scope_ids", remaining_ids) # Unregister step ObjectState ObjectStateRegistry.unregister(step_state) # Single "delete step" snapshot
Nested Atomic Blocks
Atomic blocks can be nested safely:
with ObjectStateRegistry.atomic("batch import"):
for step_data in steps_to_import:
with ObjectStateRegistry.atomic("add step"):
# Each step add is its own logical unit,
# but all are coalesced under "batch import"
create_step(step_data)
# Only one "batch import" snapshot recorded
The innermost label is ignored - only the outermost label is used.
Real-World Use Cases
Code Mode Apply
When applying code-mode edits, multiple operations occur:
def _apply_edited_pattern(self, new_pattern):
with ObjectStateRegistry.atomic("code edit"):
# Recreates function panes (registers new ObjectStates)
self._populate_function_list(new_pattern)
# Updates step's func parameter
self.function_pattern_changed.emit(new_pattern)
Step Reordering
Drag-and-drop reordering involves multiple state updates:
def _reorder_steps(self, from_idx, to_idx):
with ObjectStateRegistry.atomic("reorder steps"):
# Update step indices
for i, step_state in enumerate(step_states):
step_state.update_parameter("index", new_indices[i])
# Update pipeline's ordering
pipeline_state.update_parameter("step_order", new_order)
Best Practices
Use descriptive labels: Labels appear in time-travel UI and debugging
# Good with ObjectStateRegistry.atomic("add threshold step"): # Bad with ObjectStateRegistry.atomic("operation"):
Keep atomic blocks small: Only wrap logically related operations
Avoid side effects in atomic blocks: Don’t perform I/O or network calls
Always use context manager: Never manually manipulate
_atomic_depth
Thread Safety
The atomic mechanism is not thread-safe. _atomic_depth is a class variable shared across all threads. If your application uses multiple threads modifying ObjectStates, ensure atomic blocks are not entered concurrently from different threads.
For thread-safe alternatives, consider:
Using per-thread ObjectState registries
Serializing all ObjectState modifications through a single thread
Using locks around atomic blocks