Skip to content

Release 3.0.0: one field resolution, loud context stacks, per-context blocks, registry-owned lazy counterparts - #50

Merged
trissim merged 12 commits into
mainfrom
release/3.0.0-context-resolution
Oct 12, 2026
Merged

trissim merged 12 commits into
mainfrom
release/3.0.0-context-resolution

Conversation

@trissim

@trissim trissim commented Oct 12, 2026

Copy link
Copy Markdown
Contributor

Fixes #29, #30, #31, #33, #34, #35

Release branch for 3.0.0. Public names are removed, so the major version changes (__version__ in src/objectstate/__init__.py, the one place it is declared).

Supersedes

Merged in that order with merge commits. Git reported no textual conflict. #49 and #40 both edit object_state_registry.py in different places (#49 removes three calls to the deleted cache-clear functions; #40 moves the block state into ContextVars) and #41, #49 and #42 all edit lazy_factory.py; each merged cleanly and every PR's tests pass together.

Added on this branch

get_provenance for a state without a scope id. With #49, an ObjectState without a scope id has its own object as a layer, so an unset field inherits from another config of the same object, as it already did for a state with a scope id and for an attribute read on the object. The resolved value was right, but get_provenance still returned None for it (its scope_id is None branch predates #49), which callers read as "set on this field". It now returns (None, source_type): the state's own scope id, and the type that supplied the value. tests/test_scopeless_state_resolution.py runs each case with and without a scope id:

Read Value Source
nothing set on the object global value ("", WellFilterConfig)
sibling config of the same object sets it sibling's value (state.scope_id, WellFilterConfig); (None, WellFilterConfig) without a scope id
the field itself is set its own value None (not inherited)

Two of these cases fail without the fix, both for the state without a scope id.

The third id()-keyed cache. LiveContextResolver keyed answers by (id(config_obj), attr_name, ids of the context objects, token). Each answer is now kept together with the config object and context objects it was computed from and found again with is; holding the objects means an address cannot be reused while its answer is kept. Answers belong to one cache token and are dropped when a new token arrives (before, every token's answers were kept for the life of the resolver). tests/test_live_context_resolver_answers.py; the reused-address test fails on the old code.

Not changed: object_state_registry.py:1054 puts id(state) in an atomic block's entry signature. That is a comparison within one block while the states are held, not a cache key.

Checks run locally (CPython 3.12)

  • pytest -n 2: 297 passed.
  • ruff check src/ --select E9,F63,F7,F82: clean.
  • scripts/check_ruff_added_lines.py --base origin/main: no diagnostics on added lines.
  • black --check src/ (non-blocking in CI): 23 files would be reformatted.
  • Docs build and mypy: not run (Sphinx is not installed in the environment used).

What callers must change

Full ledger with file:line for every site: OS-3.0-caller-ledger.md (kept with the OpenHCS integration notes). Summary:

pyqt-reactive (10 sites)

  • services/widget_service.py:310-316: build_context_stack(..., ancestor_objects=...) raises TypeError. Use ObjectStateRegistry.get_ancestor_objects_with_scopes(...) and ancestor_objects_with_scopes=.
  • pyproject.toml:32: objectstate>=2.0,<3 excludes this release. requirements-ci.txt:2 names 2.1.1.
  • tests/test_preview_formatters.py:10,229,230,279: GROUP_ABBREVIATIONS_REGISTRY is removed; declare with @abbreviation.
  • tests/test_function_list_editor_tokens.py:32-34: assigns removed _atomic_* attributes.
  • Review: clickable_help_components.py:534,707,735 can now receive (None, source_type) from get_provenance; the same-window branch already handles it.

formbridge (3 sites)

  • strategies/preview_formatting.py:28,31: imports GROUP_ABBREVIATIONS_REGISTRY; call objectstate.lazy_factory.get_group_abbreviation.
  • pyproject.toml:29: version pin.

pycodify: none.

OpenHCS (10 sites, none in openhcs/, benchmark/ or scripts/)

  • pyproject.toml:104: version pin.
  • tests/unit/pyqt_gui/test_source_bindings_editor.py:3180-3181: asserts that only the edited cell flashes. It pinned the old disagreement between the two resolvers: with a scope id the same edit already flashed two cells on main, because step_source_bindings_config.source_filters inherits from source_bindings_config of the same object. Assert both paths.
  • tests/unit/test_time_travel_navigation.py:53-55, tests/unit/pyqt_gui/test_ui_agent_bridge.py:3738-3740, tests/unit/pyqt_gui/test_parameter_form_manager_cross_window.py:178: assign removed block-state attributes.

Removed names: ancestor_objects=, resolve_field_inheritance, clear_mro_resolution_cache, invalidate_mro_cache_for_field, clear_extract_all_configs_cache, set_current_temp_global, _has_concrete_field_override, GROUP_ABBREVIATIONS_REGISTRY, LAZY_CONFIG_PREFIX, TimeTravelChangeSet.legacy_entries (now states_needing_navigation), ObjectStateRegistry._atomic_depth / _atomic_label / _atomic_triggering_scope / _atomic_snapshot_requested / _atomic_entry_signature / _deferred_invalidation_depth (read current_atomic_block()), and six uncalled private functions listed in #41.

Changed behaviour: failures that were swallowed now raise (NoActiveContextError, constructor errors in config_context and merge_configs, get_base_global_config); get_context_layer_stack() returns a ContextLayerStack, not a tuple of pairs; build_context_stack sets nothing until entered; atomic blocks belong to the thread or task that opened them. The four PR bodies list every item.

Open point

tests/unit/test_compilation_session.py::test_compiler_streaming_config_snapshot_preserves_inherited_port (OpenHCS) failed once in a full -n 4 unit run against this branch and not in the matching run against main. It passes alone and with its file. Not reproduced and not explained; first thing to check is that build_context_stack(use_live=False) now reads only the saved global config.

Not merged, tagged or released.

🤖 Generated with Claude Code

trissim and others added 12 commits October 12, 2026 00:24
…skipped or substituted

build_context_stack is a context manager and takes scoped ancestors only.
config_context, merge_configs, build_context_stack, resolve_with_provenance
and the lazy resolver method raise where they used to continue with a layer
missing. Uncalled helpers that substituted layers are deleted.

Refs #34 (items 1, 2), #31.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… 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>
The lazy class of a config class was found by building its name from a
prefix and reading that name off the module. The registry that already
maps lazy to base now answers both directions, and nothing looks a lazy
class up by name.

A class's group abbreviation is declared once, on the class, and read
on demand; a lazy class answers with its base's declaration. The
@abbreviation decorator and the abbreviation= parameter write the same
declaration, so decorator order no longer matters and two different
declarations for one class raise.

GROUP_ABBREVIATIONS_REGISTRY and LAZY_CONFIG_PREFIX are removed. The
factory alone names a lazy class.

Fixes #35

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ayer stack

Attribute access and ObjectState both read resolve_with_provenance. The merged-config
walk (resolve_field_inheritance) and the process-wide caches are deleted.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ld by their objects

- get_provenance reports (None, source_type) when a state without a scope id
  inherits a field from another config of its own object. It returned None,
  which reads as 'set on this field', while the resolved value was inherited.
- LiveContextResolver keeps each answer with the config object and context
  objects it was computed from and finds it again by identity. No key is built
  from id(...). Answers of an earlier cache token are dropped.
- Version 3.0.0: public names are removed by the merged changes.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Copilot AI balanced review requested due to automatic review settings October 12, 2026 06:56

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.

Attribute reads and displayed provenance can resolve the same field to different values

2 participants