Skip to content

Test that field resolution is one precedence order; characterise the crossed case - #52

Merged
trissim merged 2 commits into
mainfrom
test/resolution-precedence-order
Oct 12, 2026
Merged

trissim merged 2 commits into
mainfrom
test/resolution-precedence-order

Conversation

@trissim

@trissim trissim commented Oct 12, 2026

Copy link
Copy Markdown
Contributor

Tests only, no production change. Checks the field resolver (dual_axis_resolver._resolve_in_layers) against two results on two-axis resolution.

What the resolver does

_resolve_in_layers is the only place the order is decided. The class axis (MRO of the type being read) is the outer loop and the scope axis (context layers, innermost first) is the inner loop. Attribute access on a lazy config, resolve_with_provenance and both ObjectState resolution paths all go through it.

New tests (tests/test_resolution_precedence_order.py)

Three classes (Leaf(Middle(Base))) x three scopes (step, plate, global) give nine definition sites; all 512 subsets are resolved for Leaf.value.

Hold under either loop order:

  • a site most specific on both axes wins;
  • removing losing sites changes neither the winner nor its reported source;
  • the two-site results form a strict total order, and its first member wins every set;
  • stored value, attribute read, resolver answer, and ObjectState's resolved value and source agree in every subset, and are asserted separately.

Characterisation of today's behaviour (labelled as such in the file):

  • all 9 crossed pairs are won by the more specific class;
  • the full order is class first, then scope;
  • a registered ObjectState keeps None stored and reports the resolver's value and source on a crossed pair.

Checked by mutation: a scope-first resolver fails only the characterisation tests; a resolver that switches order when three or more values are present fails the removal and total-order tests.

Owner question: which axis is meant to be outer?

Before this PR no test asserted a winner for a crossed pair. _ancestor_supplies_base_type_only in test_one_field_resolution.py builds one, but only compares two reads that both use the same resolver, so it passes under either order.

The documentation states both orders:

  • class first: the _resolve_in_layers docstring ("MRO order dominates layer order") and src/objectstate/docs/context_system.rst ("For each type in the container type's MRO, most specific first, it asks every layer from innermost to outermost");
  • scope first: src/objectstate/docs/configuration_framework.rst ("check AppConfig context first, then GlobalAppConfig context, walking MRO at each level").

Concrete example, reading Leaf.value on a step:

site value
Leaf in the global config "Leaf@global"
Base on the plate "Base@plate"

Today the step reads "Leaf@global" with source ("", Leaf): a value the user set on the plate for the base class loses to the global value on the subclass. Scope first would give "Base@plate" with source ("plate", Base).

If class first is intended, drop the "characterisation" labels and correct the sentence in configuration_framework.rst. If scope first is intended, the change is the loop nesting in _resolve_in_layers and the expected winner in the two characterisation tests.

Test run

Fresh uv venv, Python 3.12: 312 passed, 22 warnings (297 before, 15 new).

🤖 Generated with Claude Code

…) sites

Enumerates every subset of 3 config classes x 3 scopes for one field and checks:
- a site most specific on both axes wins (true under either loop order);
- removing losing sites changes neither the winner nor its reported source;
- the two-site results form a strict total order whose first member wins every set;
- stored value, attribute read, resolver answer and ObjectState's resolved value
  and source agree in every case and stay distinct.

Crossed pairs (no site most specific on both axes) are the only cases that show
which axis is the outer loop. Those tests are labelled as a characterisation of
today's class-first behaviour, because the documentation states both orders.

No production change.

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

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.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@trissim
trissim merged commit 2ac0c1a 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.

2 participants