Skip to content

Context stacks: with-only build_context_stack, scoped ancestors only, no layer skipped or substituted (#31, #34 items 1-2) - #41

Merged
trissim merged 2 commits into
mainfrom
fix/34-31-context-stack-loud
Oct 12, 2026
Merged

trissim merged 2 commits into
mainfrom
fix/34-31-context-stack-loud

Conversation

@trissim

@trissim trissim commented Oct 12, 2026

Copy link
Copy Markdown
Contributor

Fixes #31. Refs #34 (items 1 and 2; item 3, TimeTravelChangeSet.legacy_entries, is handled separately, so #34 stays open).

What changed

  • build_context_stack is a @contextmanager. It sets nothing until the with block is entered and leaves every layer on exit, including when a layer fails halfway or the block raises.
  • The ancestor_objects parameter and its unscoped branch are deleted. Every ancestor layer now carries a scope id.
  • Every site listed in Building a context continues after failures with a layer missing or substituted #31 raises instead of continuing.
  • New NoActiveContextError (in context_manager.py) names the "no context active" state.

Acceptance: #34 items 1 and 2

Criterion How it is met
build_context_stack is decorated as a context manager @contextmanager in context_manager.py; test_build_context_stack_sets_nothing_until_entered_and_resets_on_exit, test_build_context_stack_resets_when_the_block_raises
every call site uses with both sites in object_state.py are with build_context_stack(...):; the example in src/objectstate/docs/context_system.rst is rewritten to the real signature
no ancestor_objects= in src/objectstate parameter deleted; test_build_context_stack_rejects_ancestors_without_scopes
no DEPRECATED in src/objectstate none left
no legacy in src/objectstate not met by this PR. Left: legacy_entries in object_state_registry.py:139, :1889 (item 3) and one stale comment at lazy_factory.py:1082, outside the sites this PR may edit

Acceptance: #31

Site Now Test
config_context, replace_raw fails the constructor's exception propagates test_layer_that_cannot_be_merged_raises
build_context_stack, obj_type() fails propagates test_global_type_that_cannot_be_constructed_raises
resolve_with_provenance, extract_all_configs(layer) fails propagates test_layer_whose_configs_cannot_be_extracted_raises
lazy _resolve_field_value, no context raises NoActiveContextError test_resolver_method_without_active_context_raises
_inject_context_layer, _get_global_context_layer, _find_live_global deleted, see below n/a

No except Exception remains in context_manager.py or dual_axis_resolver.py. Seven of the eight new tests fail on main 1fcdf12; the eighth (reset when the block raises) pins behaviour that already held.

Two more except Exception blocks in context_manager.py were not in the issue's table but fall under its acceptance line, so they are handled too: merge_configs (returned the base unchanged on failure; now raises, test_merge_configs_that_cannot_be_applied_raises) and extract_from_object_attributes (deleted, no caller).

Deviation: no layer type for the partial-form case

The issue asks for the SimpleNamespace substitute in _inject_context_layer to become its own layer type. I did not add one, because nothing reaches that case: _inject_context_layer has had no caller since build_context_stack was reduced to three parameters (06abcca). I searched src/, tests/, docs/, and the OpenHCS, pyqt-reactive and formbridge checkouts for _inject_context_layer, _get_global_context_layer, _find_live_global, _find_live_values_for_type, extract_from_object_attributes and extract_from_dataclass_fields; all six are uncalled. A type with no producer and no consumer would be a second dead path, so the six functions are deleted. If a form that lacks constructor arguments needs to enter a context again, it should arrive as a declared layer type passed to config_context, not through a caught constructor failure.

Behaviour changes for callers (pyqt-reactive, OpenHCS)

Situations that used to continue and now raise:

  1. build_context_stack(..., ancestor_objects=[...]) raises TypeError (unknown keyword). pyqt-reactive services/widget_service.py:313 does this and must switch to ObjectStateRegistry.get_ancestor_objects_with_scopes(...) / ancestor_objects_with_scopes=.
  2. Calling build_context_stack(...) without with no longer sets any context. Code that relied on the returned ExitStack being already entered (or called .close() on it) now resolves outside the stack or fails on .close(). stack = build_context_stack(...); with stack: still works.
  3. config_context(obj) whose overrides the base config's constructor rejects (for example a __post_init__ validation, or an unexpected field): used to log a warning and run the block on the base config without obj's values; now the constructor's exception propagates.
  4. build_context_stack(global_config_instance) when the global config type cannot be built with no arguments: used to run with no global layer; now raises the constructor's TypeError. Global config types need defaults for every field.
  5. resolve_with_provenance with a layer that extract_all_configs rejects (a field declared as one config type holding another, or two instances of one config type in a layer): the layer used to be skipped and the value came from another layer; now the TypeError propagates. This reaches ObjectState live recomputation and ObjectState.resolve_saved_object.
  6. lazy_instance._resolve_field_value(name) with no active context: used to return None; now raises NoActiveContextError. Plain attribute access on lazy instances is unchanged. OpenHCS and formbridge only test for the method's presence.
  7. merge_configs(base, overrides) with overrides the constructor rejects: used to log and return base; now raises.
  8. Removed private names: the six functions listed above.

Paper results

  • [P10] Theorem 2.3: a missing distinction is not repaired by logging. The warning in config_context and the debug line in _resolve_field_value are replaced by raising.
  • [P9] Theorem 5.3: a skipped or substituted layer was recorded only in private state or a log, so a caller of resolve_with_provenance could not tell a value from the right layer from one found after a skip. The failure now leaves through the declared channel (an exception).
  • [P9] Definition 4.1: provenance must be recoverable, not only the value. Unscoped ancestor layers were absent from context_layer_stack, so their values were attributed to another scope or to none; every layer now has a scope id.
  • [P1] Theorem 4.13: each catch-and-continue was an independently omittable layer, so incomplete stacks were reachable. Each omission is now rejected.

Tests

pytest -n 2: 248 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

trissim and others added 2 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>
Copilot AI balanced review requested due to automatic review settings October 12, 2026 04:26

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.

@trissim

trissim commented Oct 12, 2026

Copy link
Copy Markdown
Contributor Author

Superseded by #50 (release/3.0.0-context-resolution), which merges this branch unchanged together with #41, #49, #40 and #42 for the 3.0.0 release. Left open; review and merge decisions happen on #50.

@trissim
trissim merged commit 3e0a337 into main Oct 12, 2026
11 checks passed
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.

Building a context continues after failures with a layer missing or substituted

2 participants