Skip to content

♻️ sphinx-needs: each field carries its needs_string_links rule - #2059

Open
chrisjsewell wants to merge 12 commits into
masterfrom
refactor/string-links-per-field
Open

chrisjsewell wants to merge 12 commits into
masterfrom
refactor/string-links-per-field

Conversation

@chrisjsewell

Copy link
Copy Markdown
Member

Part of #2048 — stage 0: a string link becomes a property of its field in the backend, before any new key is exposed or needs_string_links deprecated.

What

When the schema is built, each field that has a FieldSchema takes the rule of the first declared needs_string_links entry whose options name it and that compiles: FieldSchema.string_link, a StringLinkRule of source strings (regex, link_url, link_name) plus the entry's name. The need's meta area and needtable cells read the rule from the field instead of scanning the table. The table stays the only input. Validation at config-inited (priority 551) keeps every check except the one that needs the schema — whether each name in options is a field that can carry a rule — which now runs where the schema is built, as the needs_global_options defaults are checked. The compiled form stays in the existing _compile_string_link memo; nothing compiled is stored on the schema, which is pickled with the environment.

Why

A string link is per-field in everything but its syntax: only the first entry naming a field is ever used, so one field has at most one effective rule. Putting the rule on the field definition object — not in a side table keyed by name — is what lets it travel with the field, including into a future per-type field schema, exactly as needs_global_options moved onto FieldSchema.default. Stage 1 (a link declared on the field, which does not split its value) builds on this seam; every rule already records which entry supplied it.

The cross-engine model and the row-by-row parity with ubCode are recorded in packages/sphinx-needs/design/string-links-contract.md; the ubCode half is useblocks/ubcode#3841.

Byte identity (measured, base db7683a2 vs tip, normalised for SNCB-<hex> ids and "created")

  • tests/doc_test/doc_needtable: 9 html pages + needs.json, 0 differing lines, warnings identical.
  • Probe projects (late writers at three points, every claim kind, custom and default layouts, late entries that do not compile, fields registered late, codelinks ordering, ubproject.toml order), serial and -j 2: every differing html line is one of the disclosed changes below, each a link becoming text, none the other way.
  • Docs (sphinx-build -nW --keep-going, exit 0 at base and tip): 42 of 46 pages identical; api.html and genindex.html (the new attribute and class), configuration.html (the new clause), changelog.html (this entry) and needs.json (lineno +2 for the 6 needs below the clause) differ. searchindex.js carries the new environment version in every project.

Behaviour changes (all pinned)

  1. An entry written to needs_string_links after the schema is built — by an env-before-read-docs handler running after Sphinx-Needs' own, or by a directive while documents are read — no longer renders. The directive-time shape already did not render under -j N. An entry written during config-inited, even after validation, still renders if it compiles; if it does not, it is reported once ("passed validation but failed to compile …", as before), links nothing, and the next entry naming the field still draws.
  2. options can name only extra fields and the core fields that are part of the field schema (title status tags collapse hide layout style template pre_template post_template constraints). Any other core field (docname, section_name, type_name, id, …) now warns and renders as plain text wherever it was linked — in need cards (every default layout's heading shows type_name), in needtables and in custom layouts. A link type already warned, and is no longer linked by a custom layout's meta(). The warning is emitted when the schema is built (every build, before any need renders), once per entry and name, so a field registered after the configuration is read — the GitHub service fields every project has, or an add_field at a later priority — is no longer warned about and links as it always did.
  3. A late writer that replaces needs_string_links with something other than a dict no longer ends the build: Sphinx's own type warning remains, and nothing links.
  4. An entry written after validation whose source has the wrong type (a regex that is neither a string nor a string pattern, or a template that is not a string) is reported once and claims nothing — its rule could not be pickled with the environment, and a bytes pattern can never match — so a field only it names is no longer split. Two warning counts change: a name an entry lists twice warns once, and a late bytes pattern is refused once instead of failing on every rendered value.

Environment version

ENV_DATA_VERSION 8 → 9. An unbumped rebuild over an existing _build writes no page whose source did not change, so values that no longer link would stay linked on disk. Measured over a build of the previous code: the first build after upgrading reads "build environment version not current", re-reads every document and rewrites every page, leaving none of the old links. The docs' nitpick_ignore gains re.Pattern, which the 3.8 intersphinx inventory they are pinned to lacks.

Tests

25 new tests in tests/test_string_links.py: first-declared wins on both surfaces; a priority-700 config-inited writer renders (serial and -j 2); links render in a real -j 2 build; the schema carries the rule (and a link field cannot); a schema with rules pickles, compiled pattern included; both behaviour changes, including the default layout's heading; late-registered fields link without a warning, and a refused name warns once per entry; a late entry that does not compile neither shadows a later one nor stops its field splitting; a late entry with a callable regex, link_url or link_name is reported, not fatal; a late bytes pattern is refused once; the fold's precedence (the first usable entry wins). The test_basic_doc schema snapshot gains string_link=None on its 25 FieldSchema reprs. test-needs: 2103 passed, 11 skipped (the Windows-only and Python-3.12-only tests). Codelinks url_links 19/19, with no change to sphinx-codelinks.

Review

Two adversarial reviewers, three fix rounds, one validation round, recorded with every construction. The review moved the claimable-name check from config-inited into the schema build (a field registered later was warned about and then linked), made the fold compile two-pass so a broken late entry does not shadow a later valid one, type-checked late entries (an unpicklable template used to crash the environment pickle), and corrected the disclosure to name the default layouts' heading. Twenty-six reviewer mutations are red at the tip; the one left green by design is reverting the environment version, which no unit test can see.

PR requirements

  1. Description: this. 2. Tests: above. 3. Documentation: configuration.rst says which fields options may name; StringLinkRule is in the API docs. 4. Changelog: docs/changelog.rst, Unreleased → Improvements, one ♻️ entry marked (changed output). 5. uv run poe lint and uv run poe typecheck pass.

@codecov

codecov Bot commented Oct 5, 2026 •

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 98.78049% with 1 line in your changes missing coverage. Please review.
✅ Project coverage is 92.18%. Comparing base (6434aef) to head (2c86e5f).

Files with missing lines Patch % Lines
...ages/sphinx-needs/src/sphinx_needs/string_links.py 98.03% 1 Missing ⚠️
Additional details and impacted files
@@           Coverage Diff           @@
##           master    #2059   +/-   ##
=======================================
  Coverage   92.17%   92.18%           
=======================================
  Files         134      134           
  Lines       18832    18884   +52     
=======================================
+ Hits        17359    17408   +49     
- Misses       1473     1476    +3     
Flag Coverage Δ
ai-index 100.00% <ø> (ø)
codelinks 94.72% <ø> (ø)
mounts 94.26% <ø> (ø)
pytests 91.66% <98.78%> (+0.01%) ⬆️
reports 88.01% <ø> (ø)
ub-test-reports 90.16% <ø> (ø)

Flags with carried forward coverage won't be shown. Click here to find out more.

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

Adds design/string-links-contract.md: why a string link moves onto the
field it applies to (#2048), the per-field model both engines implement,
and the sphinx-needs / ubCode parity table.

Pins today's behaviour that the per-field move must keep: the first
declared entry wins on both surfaces (meta area and needtable), links
render in a -j 2 build, and an entry written at config-inited after
validation still renders, serially and in parallel.
At the end of create_schema, each field with a FieldSchema takes the
string link of the first declared needs_string_links entry naming it
(FieldSchema.string_link, a StringLinkRule of source strings plus the
entry's name), and the meta area and needtable cells read the field's
rule instead of scanning the table. Validation at config-inited 551 is
unchanged except that options may only name the fields that can carry
a rule: the core fields of the field schema, and the extra fields.

The compiled form stays in the _compile_string_link memo, whose key no
longer carries options. string_link_field_names had no other importer
and is removed. The test_basic_doc schema snapshot gains
string_link=None on each of its 25 FieldSchema reprs.
Pins that the field schema carries the rule (and a link field cannot),
that a schema carrying rules pickles (compiled pattern included), and
that the fold never raises. Pins the two behaviour changes: an entry
written after the schema is built no longer renders, and options naming
a field with no FieldSchema warns at config time and renders as text in
a needtable and through a custom layout's meta().

Documents which fields options may name, adds the changelog entry, and
lists string_link among FieldSchema's attributes in AGENTS.md.
StringLinkRule.regex is typed str | re.Pattern[str], and the intersphinx
python inventory the docs use is pinned to 3.8, which has no re.Pattern,
so the nitpicky docs build could not resolve it.
The entry describes changed output, and a second Unreleased section named
like 8.5.0's took its anchor and renumbered every later one.
…inks

An unbumped rebuild over an existing _build writes no page whose source
did not change, so the values that no longer link (a field outside the
field schema, an entry written after the schema was built) stayed linked
on disk until a full rebuild. Measured over a build of the previous tip.
The fold now runs every check that needs the schema. A name in options
with no FieldSchema is warned about there, once per entry, instead of at
config-inited 551, where fields registered later (the GitHub service
fields every project has, an add_field after 551) were warned about as
ignored and then linked anyway.

Each entry is compiled in the fold, through the memo validation filled:
an entry that does not compile, or whose sources are not strings (only
an entry written after validation can be either), is reported once and
holds a field only until a usable entry names it, so the field still
splits but links with the first usable entry, as before; an unpicklable
value is never stored on the schema.
…hecked

The changelog and the string-links contract now say that a value outside
the field schema renders as text wherever it was linked, need cards
first (every default layout's heading shows type_name); that names are
checked when the schema is built, so a field registered late is not
warned about; and how an entry that does not compile is folded. The
contract's split row no longer ties the strip-then-drop fix to #1718,
and the -j 2 test states what actually pickles the rules.
A late bytes pattern is refused with validation's own reason, and the
fold's docstring says what a type-refused late entry does (claims
nothing, so a field only it names is not split) and which warnings are
now reported once. Pins: a later usable entry never replaces the first,
a late callable in any of the three sources is reported rather than
fatal, a late bytes pattern is refused once, and a late option that is
not a name is warned about.
Item 5 now separates the validated table, where only the name check
moved, from entries written after validation, which are checked,
compiled and reported at the fold: a compile failure once, a source of
the wrong type refused and claiming nothing (so a field only it names
is not split), a bytes pattern once instead of per value. Item 3 notes
that a usable entry takes a field from an unusable one.
…ally (Windows)

Two string-link tests prove a `-j 2` build really shared its read between
workers by looking for the chunked status line (`index .. page3`). Sphinx
only reads in parallel with the forking start method
(`sphinx.util.parallel.parallel_available`, `os.name == 'posix'` from 7.4
through 9.1), so on Windows the read is serial whatever `-j` says, the
status names one document per line, and the proof fails although the links
render. Both proofs are now skipped on exactly that flag; the serial variant
of the late-entry test still runs everywhere, and on posix the assertion is
unchanged.
@chrisjsewell
chrisjsewell force-pushed the refactor/string-links-per-field branch from de4926c to 2c86e5f Compare October 6, 2026 10:51

This branch has not been deployed

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

Labels

pkg: sphinx-needs The sphinx-needs distribution (packages/sphinx-needs): its code, tests and docs

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant