Skip to content

A state resolves from its registered place only; saving the global state stores the saved global config (3.0.4) - #57

Merged
trissim merged 2 commits into
mainfrom
fix/53-state-resolution-ignores-entered-context
Oct 12, 2026
Merged

trissim merged 2 commits into
mainfrom
fix/53-state-resolution-ignores-entered-context

Conversation

@trissim

@trissim trissim commented Oct 12, 2026

Copy link
Copy Markdown
Contributor

Closes #53. Releases 3.0.4.

A state's resolved values depend only on its place in the registered hierarchy

build_context_stack is the one place a state builds its resolution layers (both callers are in ObjectState). It now starts from the global config whatever config_context the caller has entered: the caller's context is set aside for the block and active again after it. Before, the state's layers were stacked on the caller's, so the caller's layers beat the global config, were taken whenever a field happened to be resolved inside a block, were kept afterwards and were reported under scopes where nothing is registered.

Plain attribute access on a lazy object that no state owns is unchanged and reads the entered context.

Saving the state of the global config stores the saved global config

mark_saved on the state of a global config stores its saved object as the saved global config before any descendant resolves again. The state already wrote the store on edit, restore, instance replacement and time travel; save was the one operation left to the application, so the result depended on whether the application called set_saved_global_config before or after mark_saved. All three sequences (save only, save then store, store then save) now end the same and tell registered descendants the same things in the same order.

dirty_fields compares the sources first, like the other resolved reads. On 3.0.3 it answered from before a source change until another read happened.

The single-owner design for the rest (first store at registration, the setters' remaining purpose, the clear_current_temp_global() calls) is #56.

Tests

tests/test_resolved_values_follow_registered_ancestors.py:

  • the strict xfail is a passing test;
  • one history (build, global config replaced, ancestor edited / saved / reset / unregistered / registered again / restored, every field resolved again, own value set and saved, an edit one block further in, the global state edited and saved) is run outside any context and inside nine kinds of entered context (with and without a scope id, under a registered scope, under the state's own scope, under the global scope, a global config object, masking, started from the saved global config, nested). Every read and every notification of three states (built before, built inside and never registered, first read after the block) must be equal, and every reported source must be a registered scope or the global config;
  • the layers a state resolves against are the global config, the ancestors and the state's object, and the caller's stack is the same object after the block, also when the block raises;
  • a lazy object no state owns reads the entered context before and after states are built, read and resolved again in it;
  • a context held by another thread is not a source; a thread started with the caller's context copied reads states as outside;
  • saving the global state in three orders, descendants registered or not.

19 of these fail on 3.0.3. Full suite: 388 passed on 3.12 and 3.11 in fresh venvs. pyqt-reactive main (593 passed, 1 skipped) and formbridge main (108 passed) give the same results on 3.0.3 and on this branch.

🤖 Generated with Claude Code

trissim and others added 2 commits October 12, 2026 12:35
…ate stores the saved global config

A state's resolved values are computed from its own object, the states
registered above it and the global config. build_context_stack, the one
place a state builds its layers, now starts from the global config whatever
config_context the caller has entered: the caller's context is set aside for
the block and active again after it. Before, the state's layers were stacked
on the caller's, so the caller's layers beat the global config, were taken
whenever a field happened to be resolved inside a block, were kept
afterwards, and were reported under scopes where nothing is registered.
Plain attribute access on a lazy object that no state owns is unchanged and
reads the entered context. Closes #53.

mark_saved on the state of a global config stores its saved object as the
saved global config before any descendant resolves again. The state already
wrote the store on edit, restore, instance replacement and time travel; save
was the one operation left to the application, which made the result depend
on whether the application called set_saved_global_config before or after.
Both orders, and mark_saved alone, now end the same: descendants' saved
values follow the saved global and nothing stays dirty.

dirty_fields is a read like the other resolved reads: it compares the
sources first, so it no longer answers from before a source change.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Copilot AI balanced review requested due to automatic review settings October 12, 2026 16:35
@trissim
trissim merged commit 01f3a5d into main Oct 12, 2026
11 checks passed

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

A state resolved inside an entered config_context keeps that context's values (cases 1 and 2 fixed in 3.0.3)

2 participants