Skip to content

emrg: a cross-reference to a module is decided by the whole path, and a member by its field list - #2542

Merged
argszero merged 1 commit into
masterfrom
feature/cross-reference-mod-and-attr
Oct 23, 2026
Merged

argszero merged 1 commit into
masterfrom
feature/cross-reference-mod-and-attr

Conversation

@argszero

Copy link
Copy Markdown
Owner

Closes #2541

What changed

tests/test_docstring_cross_references_resolve.py read four cross-reference roles. It now reads six, and each new role carries the rule its own claim needs rather than a place in one list.

mod resolves only when the whole dotted path is a module — one this checkout's files form, or one the interpreter can find. The symbol rule decides a dotted name by its head, which is the right bound for os.path.expandvars because its tail cannot be followed. A module path is different: its head is always a package bound somewhere here, so borrowing that rule would have resolved every module reference every time and reported nothing however stale the tail was. The arm test_the_symbol_rule_would_have_passed_that_module_reference pins that difference, so the module rule cannot quietly become decorative.

attr resolves through its head when dotted, and when bare against the bound names plus the field names a _fields_ list declares as strings. The exclusion it replaces said the role "names a member, not a binding" and that the tree's sites are ctypes fields — measured on 22e01111, two of the fourteen are (SubAuthority, SubAuthorityCount); the other twelve name an ordinary class attribute, which the bound-name set already answers. The field list is therefore the whole of what the role adds, and the module docstring now says that instead of carrying a count.

Measurements

  • Suite on this branch: 5804 passed, 35 skipped.
  • Suite on master 22e01111, same machine, same call: 5798 passed, 35 skipped. The difference is the six arms this PR adds; the guard file goes from 10 to 16.
  • Roles read in the tree, all resolving: func, class, meth, data, attr (14 sites), mod (21 sites).

Mutation arms

Each new rule was shown red where it must be, with the mutated line printed before the run so a no-op edit could not read as a survivor:

Mutation Reddened
module_resolves returns True test_a_module_reference_past_a_module_the_tree_has_is_reported
the bare-member branch takes every name test_a_bare_member_role_nothing_declares_is_reported
declared_fields returns an empty set test_a_member_role_naming_a_declared_field_resolves and the whole-tree arm

The third row is the useful reading: the field list is load-bearing on the real tree, not only on its fixture.

What this still does not measure

Whether the cited name is the right one, and a bare backticked name in prose — both unchanged from before, and both still stated in the module docstring.

CI is pending.

… a member by its field list

The guard read four roles and left two out. `mod` cannot borrow the symbol rule — every module
path begins with a package bound somewhere in the tree, so a module reference read by its head
resolves always and reports nothing however stale the tail is. `attr` was excluded on the stated
ground that the tree's references are ctypes fields; measured, two of the fourteen are.

So `mod` resolves only when the whole path is a module this tree's files form or one the
interpreter can find, and a bare `attr` resolves against the bound names plus the field names a
`_fields_` list declares as strings — the one member a reader can follow that is no binding at
all. Class attributes were already in the bound-name set, which is why the field list is the only
addition and the docstring says so instead of a hand-written count.

@argszero argszero left a comment

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

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

✅ LGTM — cycle cyc20261023-192447

Both legs are green on this head (test 2m23s, test-windows 4m4s), and check-merge-freshness.py reads FRESH — the merge base 22e01111 is master's tip, so the verdict is about the tree a merge would produce rather than one two commits back.

The change adds two Sphinx roles to the cross-reference guard, and each one needed its own rule rather than a place in the existing list. mod is answered by the whole dotted path — a module this checkout's files form, or one the interpreter can find — because the symbol rule decides a dotted name by its head, and a module path's head is always a package bound somewhere here: borrowed into that rule, every module reference would have resolved however stale its tail was. attr is answered by the bound names plus the field names a _fields_ list declares as strings, which is the one member a reader can follow that is no Python binding at all.

The arm I care about most is test_the_symbol_rule_would_have_passed_that_module_reference. It asserts the old rule returns no finding for the same text read as a func, which is what keeps the new module rule from being quietly reabsorbed if someone later simplifies the code. Without it the two rules are indistinguishable from a green suite.

Three mutation arms were run with the mutated line printed before each, so a no-op edit could not read as a survivor: module_resolves returning True reds the module arm; the bare-member branch taking every name reds the member arm; declared_fields returning an empty set reds both the fixture arm and the whole-tree arm — that last one is what shows the field list is load-bearing on the real tree rather than only on its own fixture.

What this still does not measure is unchanged and stated in the module docstring: whether the cited name is the right one, and a bare backticked name in prose. I am not claiming coverage the guard does not have.

I re-read the change against its merge base rather than a summary: two paths — tests/test_docstring_cross_references_resolve.py (+197 −21) and its new arms — and the guard file goes from 10 tests to 16.

@argszero argszero left a comment

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

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

✅ LGTM — cycle cyc20261023-193226

Second vote, on an unchanged head. Both legs pass (test 2m23s, test-windows 4m4s), check-merge-freshness.py reads FRESH — merge base 22e01111 is master's tip — and nothing has been pushed since the first vote, so that vote still stands and this one is added beside it rather than replacing it.

The change adds two Sphinx roles to the cross-reference guard, and the reason each needed its own rule rather than a place in the existing list is what I want on the record:

mod is decided by the whole dotted path — a module this checkout's files form, or one the interpreter can find. The symbol rule decides a dotted name by its head, correctly, because the tail of os.path.expandvars cannot be followed. A module path is the case where that bound fails: its head is always a package bound somewhere in the tree, so borrowed into the symbol rule every module reference would resolve however stale its tail was. test_the_symbol_rule_would_have_passed_that_module_reference pins that difference by asserting the old rule returns no finding for the same text read as a func — without it the two rules are indistinguishable from a green suite.

attr is decided by the bound names plus the field names a _fields_ list declares as strings — the one member a reader can follow that is no Python binding at all. The exclusion it replaces claimed the tree's member references are ctypes fields; measured, two of the fourteen are, and the other twelve name an ordinary class attribute that the bound-name set already answered.

Three mutation arms were run with the mutated line printed before each, so a no-op edit could not read as a survivor. The third is the informative one: making declared_fields return an empty set reds both the fixture arm and the whole-tree arm, which is how the field list was shown load-bearing on the real tree rather than only on its own case.

Unchanged and stated in the module docstring, so nobody reads this as more coverage than it is: the guard does not answer whether a cited name is the right one, and it does not read a bare backticked name in prose.

@argszero argszero left a comment

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

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

✅ LGTM — cycle cyc20261023-193636

Reviewed at head f3cb6453, measured on this checkout.

  • The change is confined to tests/test_docstring_cross_references_resolve.py (197 added, 21 removed) — no production module moves, so the risk surface is the guard itself.
  • The two additions answer a real gap the message states honestly: mod resolves by the whole path, because a module reference read by its head resolves against a package bound somewhere in any tree and would pass forever — an empty guard; attr was excluded on a claim ("these are ctypes fields") that measurement contradicts — two of fourteen.
  • The class-attribute half of attr was already in the bound-name set, so the only addition is the string-declared _fields_ names, and the docstring says that instead of carrying a hand-written count that would decay.
  • Freshness: check-merge-freshness.py reads FRESH at base 22e01111, and both legs pass on this head (test 2m23s, test-windows 4m4s).
  • check-merge-pairs.py finds this PR and the open bump land cleanly in either order, so no ordering constraint stands against it.

@argszero
argszero merged commit fdeb126 into master Oct 23, 2026
2 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.

emrg: the cross-reference guard omits the two roles whose rule differs from its own

1 participant