Repository navigation
Keep atomic and deferral block state per context (#30); rename legacy_entries (#34 item 3) - #40
Merged
Merged
Conversation
… payload atomic, atomic_success and defer_live_invalidations kept their nesting in class attributes shared by the process, while config_context keeps its scope in context variables. The open block is now an AtomicBlock held in a ContextVar and the deferral nesting is a ContextVar count, so a thread outside any block no longer sees another thread's block (#30). TimeTravelChangeSet.legacy_entries is the only completion-callback payload, so it is renamed states_needing_navigation (#34 item 3). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This was referenced Oct 12, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Fixes #30. Addresses item 3 of #34 (items 1–2 are in
context_manager.py, owned by another PR, so this does not close #34).#30: block state belongs to the context that opened the block
Decision: what is per context and what is process-wide
atomic/atomic_successblock am I inside (label, triggering scope, snapshot requested, entry signature, nesting depth)ContextVar)config_contextanswers for its scope.defer_live_invalidationsContextVar)_deferred_invalidations)record_snapshotflushes before any snapshot), so it cannot belong to the block.Before, one class attribute was the nesting counter for every thread, so "depth 2" could mean one thread nested twice or two threads once each, and nothing retained could tell them apart ([P10] Proposition 6.2). The same location was also one thread's own block state and another thread's reading of "am I in a block" ([P9] Proposition 7.1). The two entry/exit bodies in
atomicandatomic_successwere identical, so they now share_enter_atomic_block/_leave_atomic_blockwhile the two context managers stay distinct because their exit rules differ ([P1] Theorem 4.8). The atomic block and the deferral nesting give different answers and stay separate variables.Mechanism
Searched
src/objectstateforContextVarandthreading.local.context_manager.pykeepsconfig_contextscope in module-levelcontextvars.ContextVars;global_config.pyusesthreading.localfor a different concern. The block state uses the first, the same mechanism as the context it nests in. No new kind of thread-local was added.AtomicBlock(dataclass) holds one open block._atomic_block: ContextVar[AtomicBlock | None]says which block the running context is in.ObjectStateRegistry.current_atomic_block()is the one public read: the block, orNoneoutside any block._live_invalidation_deferrals: ContextVar[int]replaces_deferred_invalidation_depth._atomic_*class attributes and_deferred_invalidation_depthare deleted.Acceptance
Met. The reproducer is
tests/test_atomic_block_context.py::test_thread_outside_any_block_does_not_see_another_threads_block: with a worker thread held insideatomic("worker edit"), the main thread reads(0, None)outside its block and(1, "main edit")inside. The reproducer read the private class attributes, which no longer exist; the test reads the same two facts throughcurrent_atomic_block().The other tests in that file cover the consequences named in the issue: a snapshot requested by the main thread is recorded at once rather than absorbed by the worker's block; the main thread's own
atomic/atomic_successrecords on exit; nesting in one context still shares the outer block; an asyncio task started inside a block inherits it and a sibling task does not (same rule asconfig_context); a deferral open in one thread does not queue another thread's invalidation. All six failed onorigin/mainbefore the fix.#34 item 3:
TimeTravelChangeSet.legacy_entriesCase: it is the current format, so it was renamed, not replaced.
Evidence:
TimeTravelCompleteCallbackisCallable[[List[Tuple[str, ObjectState]], Optional[str]], None]and is the only completion-callback type;_fire_time_travel_complete_callbacksis the only producer and this method builds its payload. Every subscriber takes that shape as(dirty_states, triggering_scope): pyqt-reactivetime_travel_navigation.py:123,abstract_manager_widget.py:165,function_list_editor.py:234; OpenHCSpipeline_editor.py:586,plate_manager.py:329,dual_editor_window.py. No callback takes aTimeTravelChangeSet.New name:
states_needing_navigation. Callers updated:object_state_registry.py(one) andtests/test_undo_redo_cascade.py(one).External callers of
legacy_entries: none.grep -rn "legacy_entries"overexternal/pyqt-reactive/srcandopenhcs/returns nothing, so no matching change is needed at release time.grep -n "legacy\|DEPRECATED" src/objectstate/object_state_registry.pynow returns nothing.Behaviour changes for callers (pyqt-reactive, OpenHCS)
atomicblock or deferral. Code that opened a block on one thread and relied on a different thread's snapshots or invalidations being coalesced into it will now see those recorded/applied immediately. Single-threaded use (the Qt main thread) is unchanged.ObjectStateRegistry._atomic_depth,_atomic_label,_atomic_triggering_scope,_atomic_snapshot_requested,_atomic_entry_signatureand_deferred_invalidation_depthare gone. Neither pyqt-reactive nor OpenHCS reads them (grepped). Usecurrent_atomic_block().ObjectStateRegistry.clear()resets the block state of the calling context only. A block dropped byclear()while open does not record on exit (before, the depth went negative and it also did not record).TimeTravelChangeSet.legacy_entriesis renamedstates_needing_navigation.Files outside
object_state_registry.pyTests only: the new
tests/test_atomic_block_context.py; the rename intests/test_undo_redo_cascade.py; and removal of the reset lines that assigned the deleted_atomic_*attributes in four test files' reset helpers (they would otherwise create dead attributes).Checks
pytest -n 2: 246 passed.ruff check src/ --select E9,F63,F7,F82: clean.scripts/check_ruff_added_lines.py --base origin/main: no diagnostics on added lines.🤖 Generated with Claude Code