Repository navigation
4.0.0: the state registered at the global scope holds the global config (#56) - #58
Merged
Merged
Conversation
The global config had two holders: two per-thread stores (live, saved) and the parameters of the state registered at scope "" for a global config type, kept in step by five write sites in the state and by every application that called a setter. Now there is one. While such a state is registered, the live global config is the object its parameters describe and the saved one is the object it last saved; get_live_global_config, get_saved_global_config, config_context and every state's resolution ask the registered state. While none is registered, the object given to set_global_config is both. - set_global_config is the one outside write route. With a state registered it is state.update_object_instance; without one it stores the object and tells registered states at once. - Registering a global state makes its object the global config with no setter call; unregistering, or time travel to before it was registered, ends that. - One global config per process: the per-thread stores and the thread field of ResolutionSources are gone, so a state's resolved values no longer depend on the thread that reads them. - Removed: ensure_global_config_context, set_global_config_for_editing, set_live_global_config, set_saved_global_config, get_current_global_config, clear_current_temp_global (and its four calls, which dropped the caller's entered context on a global edit), GlobalContextValues, ObjectState.prepare_history_global_context, rebuild_lazy_config_with_new_global_reference (no caller). CHANGELOG.md lists each removed name with what to call instead. Fixes #56 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
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 #56
What was wrong
The global config had two holders that callers had to keep in step: two per-thread stores (
_live_global_config_contexts,_saved_global_config_contexts) and the parameters of the state registered at scope""for a global config type. The state wrote the stores from five places (update_parameteronly if a live value was already stored,update_object_instance,restore_saved, time travel,mark_saved); registering wrote nothing; four setters under four names wrote the stores without telling any state. Two copies kept equal by convention are not a single source (P10 Proposition 2.2: agreement cannot orient a source edge).Decision: the registered global state is the holder
A state's value is its flat
parameters/_saved_parameters; undo, dirty fields, checkpoints and notifications are all defined on them, and the object is already derived from them for every other ancestor (object_for_resolution). Making the store the holder would mean a state with no parameters of its own, unlike every other state. So the direction is: parameters -> object -> global config.get_live_global_config,get_saved_global_config,get_base_global_config(soconfig_contextandbuild_context_stack) and_resolution_sourcesaskObjectStateRegistry.global_config_state(config_type)at read time. Nothing is copied, so nothing is written: the five write sites are deleted, not merged. A process without states uses the same three functions; with no state registered they answer from the objectset_global_configstored.One write route
set_global_config(config_type, config). With a global state registered it isstate.update_object_instance(config); without one it stores the object and tells registered states at once. A stored object that disagrees with a registered state cannot be made.Registration
Registering a global state makes its object the global config with no setter call (also when nothing was stored). Unregistering it, or time travel to a point where it was not registered, ends that at once: the global config is again what was stored while no state held it, or the declared defaults. Because the lookup is made at read time, every way the registry changes (
register,unregister,unregister_scope_and_descendants,clear, time travel, time-travel rollback) is covered without a hook.Threads: one rule
The global config is one value per process, like the registry. The per-thread stores and
ResolutionSources.threadare gone, so a state's resolved values depend only on its place in the registered hierarchy (the rule from #53), not on the reading thread. Before, a thread that had not called a setter itself resolved plain lazy objects against the declared defaults. Nothing is synchronised (it never was): edits belong in one thread.spawn_thread_with_contextcopies the enteredconfig_contextlayers; the global config needs no copying.Alternative not taken: keep the stored object per thread while no state is registered. Example where they differ: two threads in a stateless process each call
set_global_configwith a different config and then compile. Per process (this PR): both read the config set last. Per thread: each reads its own, and a third thread that set nothing reads the declared defaults.Removed (see CHANGELOG.md for what to call instead)
ensure_global_config_context,set_global_config_for_editing,set_live_global_config,set_saved_global_config,get_current_global_config,clear_current_temp_globaland its four calls,GlobalContextValues,ObjectState.prepare_history_global_context,rebuild_lazy_config_with_new_global_reference(no caller anywhere). Added:set_global_config,forget_stored_global_config,ObjectStateRegistry.global_config_state._resolve_again_belowis nowresolve_again_below.Behaviour changes for callers (pyqt-reactive, OpenHCS)
set_global_configbeside a registered global state replaces the state's object and drops its unsaved edits. Calls that only repeated what the state held must be deleted.config_context; a block entered before the change keeps its layers until it is left.get_live_global_configreturns the object the state builds from its parameters, not the instance first passed in.Tests
tests/test_one_holder_of_the_global_config.py: every sequence of up to three operations (edit, save, restore, replace the object,set_global_config, register, unregister), with and without the state registered at the start, each operation made in the calling thread or (sequences up to two) in another thread. After every operation: the global config is the holder's object and holds its values; a registered and an unregistered descendant agree by every read kind (stored, stored on the built object, attribute access, live, saved, reported source, dirty fields); a plain lazy object agrees in contexts entered from the live and the saved global config; another thread reads the same; the registered descendant was told at once, the unregistered one is told the same on its read, and nobody is told when the value did not change. A three-value model predicts the values. Further tests: undo and redo across edit/save/restore/replace/set, time travel to before the state was registered, a global state never passed to a setter, a stateless process across threads, a context entered before a global edit.Mutation checks: making the setter store beside a registered state fails 11 tests; making the stored setter tell nobody fails 4.
Full suite in a fresh venv: 395 passed. Ruff fatal checks and the added-lines check pass;
sphinx -Wbuilds.Benchmark
One global state, one plate, N steps of 78 leaf fields; minimum over 3 interleaved runs, microseconds, 3.0.4 -> this PR.
A global edit costs about 0.15 ms more, once per edit and independent of N: the first read after it rebuilds the global object from the state's parameters (as for any other ancestor) where 3.0.4 patched one field into the stored copy. Everything else is within run-to-run noise (the machine was shared; single runs varied by more than these differences).
🤖 Generated with Claude Code