Repository navigation
♻️ sphinx-needs: each field carries its needs_string_links rule - #2059
Open
chrisjsewell wants to merge 12 commits into
Open
chrisjsewell wants to merge 12 commits into
chrisjsewell wants to merge 12 commits into
Conversation
Codecov Report❌ Patch coverage is
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
Flags with carried forward coverage won't be shown. Click here to find out more. ☔ View full report in Codecov by Harness. 🚀 New features to boost your workflow:
|
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
force-pushed
the
refactor/string-links-per-field
branch
from
October 6, 2026 10:51
de4926c to
2c86e5f
Compare
This branch has not been deployed
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
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_linksdeprecated.What
When the schema is built, each field that has a
FieldSchematakes the rule of the first declaredneeds_string_linksentry whoseoptionsname it and that compiles:FieldSchema.string_link, aStringLinkRuleof 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 atconfig-inited(priority 551) keeps every check except the one that needs the schema — whether each name inoptionsis a field that can carry a rule — which now runs where the schema is built, as theneeds_global_optionsdefaults are checked. The compiled form stays in the existing_compile_string_linkmemo; 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_optionsmoved ontoFieldSchema.default. Stage 1 (alinkdeclared 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
db7683a2vs tip, normalised forSNCB-<hex>ids and"created")tests/doc_test/doc_needtable: 9 html pages +needs.json, 0 differing lines, warnings identical.ubproject.tomlorder), serial and-j 2: every differing html line is one of the disclosed changes below, each a link becoming text, none the other way.sphinx-build -nW --keep-going, exit 0 at base and tip): 42 of 46 pages identical;api.htmlandgenindex.html(the new attribute and class),configuration.html(the new clause),changelog.html(this entry) andneeds.json(lineno+2 for the 6 needs below the clause) differ.searchindex.jscarries the new environment version in every project.Behaviour changes (all pinned)
needs_string_linksafter the schema is built — by anenv-before-read-docshandler 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 duringconfig-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.optionscan 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 showstype_name), in needtables and in custom layouts. A link type already warned, and is no longer linked by a custom layout'smeta(). 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 anadd_fieldat a later priority — is no longer warned about and links as it always did.needs_string_linkswith something other than a dict no longer ends the build: Sphinx's own type warning remains, and nothing links.regexthat 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_VERSION8 → 9. An unbumped rebuild over an existing_buildwrites 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_ignoregainsre.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-700config-initedwriter renders (serial and-j 2); links render in a real-j 2build; 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 callableregex,link_urlorlink_nameis reported, not fatal; a late bytes pattern is refused once; the fold's precedence (the first usable entry wins). Thetest_basic_docschema snapshot gainsstring_link=Noneon its 25FieldSchemareprs.test-needs: 2103 passed, 11 skipped (the Windows-only and Python-3.12-only tests). Codelinksurl_links19/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-initedinto 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
configuration.rstsays which fieldsoptionsmay name;StringLinkRuleis in the API docs. 4. Changelog:docs/changelog.rst, Unreleased → Improvements, one ♻️ entry marked (changed output). 5.uv run poe lintanduv run poe typecheckpass.