Repository navigation
✨ sphinx-needs: read variant data from [variants] through ub-project, keeping [needs] variant_data* - #1996
Merged
Conversation
The runtime edge for reading the [variants] table of needs_from_toml. The lock carries the minimal two-line edge only; a plain `uv lock` re-derives fork markers without moving any version.
…s_unknown_key warning subtypes Named after ub-project's diagnostic codes, so that ubCode and sphinx-needs report the same findings in one vocabulary. Each is suppressible on its own, rather than only together with every other needs.config check.
load_config_from_toml now reads the variant data of the needs_from_toml file with ub_project.read_variants: from [variants] (or [<prefix>.variants] under needs_from_toml_table), or from its legacy location, [needs] variant_data / variant_data_file. - both locations declared: [variants] wins whole, and each ignored [needs] key is a needs.variant_data_location warning; - data read from [needs]: one line per key with -v, nothing otherwise; - an unknown key in [variants]: a needs.variants_unknown_key warning; - a file with [variants] and no [needs] table is read; - a refusal (variants not a table, an empty or non-string data_file, a missing or invalid TOML-declared data file) is a NeedsConfigException. What stays as it was: a -D override replaces one key and removes it from both locations before the read, conf.py values still lose to the TOML per key and fill the gaps it leaves, and the data file confval is still written through _abs_path, so its string is unchanged. The merge with conf.py values stays in resolve_variant_data_config. The quickstart test project is a verbatim copy of ubCode's rust/ubc_quickstart/src/templates/variants/ (ubcode 8732fbc065).
A subsection under needs_from_toml: the two keys, the legacy [needs] location and its -v line, whole-location precedence and the needs.variant_data_location warning, the per-key conf.py / -D rules, the prefix rule that makes pyproject.toml work, the refusals, and that ubCode and sphinx-mounts do not read [variants] yet. Cross-links from needs_variant_data and needs_variant_data_file. Also corrects the needs_from_toml caution, which said relative paths in the toml are resolved against the conf.py directory: they are, and were, resolved against the toml file's own directory. The package AGENTS.md says what is now ub-project's and what stays in load_config_from_toml.
Its data_file names a file that does not exist, so a reader that decided the location from the top-level table would fail the build.
…ain, whatever the error The parse was narrowed to catching ProjectConfigError, but load_toml does not wrap every failure: a deeply nested array raises RecursionError, which master turned into the needs.config "Error loading" warning and the branch let escape as an ExtensionError. Catch Exception around the parse, as before. An empty needs_from_toml_table entry is refused by ub_project with a ValueError; re-raise it as a NeedsConfigException, like its other refusals, instead of a traceback.
…unpinned
- both locations with disjoint keys: nothing is read from [needs];
- [tool.variants] alone under needs_from_toml_table is read, and a
top-level [variants] does not stand in for a missing [tool.needs];
- even an empty [variants] (or variants = {}) with no [needs] is silent;
- a broken [needs] (needs = 1, schema = 1) beside [variants] still ends
the read with today's warning: only a missing [needs] goes on;
- a deeply nested TOML only warns;
- an empty needs_from_toml_table entry is a NeedsConfigException;
- invalid inline variant data in the TOML is refused in ub-project's
words, and every refusal keeps the ProjectConfigError as its cause;
- a diagnostic code this version does not know is a -v line only.
…nces - relative toml paths: name the options whose toml value is resolved against the toml file's directory, instead of every path in the file, and drop the unscoped conf.py / -D claim; - a toml file with [variants] and no [needs] is read, and even an empty [variants] stops the missing-[needs] warning; - a conf.py value fills a key the LOCATION READ does not set; - invalid inline variant data is among the refusals; - a data file named in the toml is read twice during configuration initialisation, not once.
… and the empty-prefix cause The validation round's two nits: a catch narrowed to (ProjectConfigError, RecursionError) passed every test while a 5000-digit integer literal crashed the build where master warns -- the unparseable-TOML test now holds a case per exception class; and the empty-prefix configuration error asserts its ValueError cause. The boundary's comment says what its ValueError catch also converts until ub-project wraps it itself (#1995).
Codecov Report✅ All modified and coverable lines are covered by tests. Additional details and impacted files@@ Coverage Diff @@
## master #1996 +/- ##
==========================================
+ Coverage 91.69% 91.74% +0.04%
==========================================
Files 129 129
Lines 18155 18174 +19
==========================================
+ Hits 16648 16674 +26
+ Misses 1507 1500 -7
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:
|
…error text CI's py3.11 cells failed the deeply-nested case: the RecursionError there read "maximum recursion depth exceeded in comparison", the suffix Python adds when the limit trips inside a comparison rather than a call, which depends on the stack depth at that moment. Assert the message's start and the subtype tag; the same for the integer-limit case.
chrisjsewell
added a commit
that referenced
this pull request
Sep 30, 2026
…#1998) sphinx-needs carried its own `validate_variant_data`, `load_variant_data_file`, `deep_merge` and `resolve_variant_data`; ub-project (a runtime dependency since #1996) carries the one copy of the contract. This deletes sphinx-needs' bodies and uses ub-project's. **Deleted:** the four function bodies and `_SCALAR_TYPES` in `sphinx_needs/variant_data.py`. The four names stay importable there as plain re-exports of `ub_project`'s objects (a test pins `is`). **Stays sphinx-needs':** `VariantDataError`, `VariantDataParsed`, `lookup_variant_data`, `VariantDataProxy` — the `var` proxy for filters and `.. if::`, the `<{ var.* }>` lookup and its error — and the boundary: `resolve_variant_data_config` (priority 11) maps `""` to no file (ub-project's reading contract §10.1) and re-raises `ProjectConfigError` — and the one `ValueError` ub-project lets through, an integer past Python's conversion limit (#1995) — as `NeedsConfigException`, exactly the pair the TOML route catches at priority 10. **The five observable changes** (each pinned by a test): 1. Error texts on the `conf.py` / `-D` route are ub-project's — the TOML route's words: | before | after | |---|---| | `Variant data file not found: <p>` | `variant data file not found: <p>` | | `Invalid JSON in <p>: …` | `variant data file <p> is not valid JSON: …` | | `Variant data file must contain a JSON object, got list` | `variant data file <p> must hold a JSON object, got list` | | `var.x: expected str/bool/int/float/list/dict, got NoneType` | `variant data file <p>: var.x: a value must be a str, bool, int or float, an array or a table, got NoneType` (inline: without the file prefix) | | `var.x[1]: expected int, got str (arrays must be uniform type)` | `var.x[1]: an array must hold one type, expected int but got str` | | `var: expected a dict, got str` | `var: variant data must be a table, got str` | | `var: all keys must be strings, got int` | `var: keys must be strings, got int 1` | 2. A directory or non-UTF-8 data file: raw `ExtensionError` → `NeedsConfigException` naming the file (exit code 2 both). 3. The four functions raise `ub_project.ProjectConfigError`, not `VariantDataError` (six unit tests moved; their regexes already matched). 4. The resolved `needs_variant_data` is a new top-level dict, even with no file (nested tables are still the `conf.py` objects) — observable only by identity. 5. A data file that cannot be read (permission denied): raw `ExtensionError` → `NeedsConfigException … cannot be read` (exit code 2 both). Everything else is identical: the old and new quartets were run over the same 44 inputs (validate / load / merge / resolve): same accept/reject set and same merge results on every row, the only outcome difference being `resolve(inline, "")`, which the resolver's `""` → `None` mapping keeps as "no file". **Route parity:** the same bad data file declared in the TOML and in `conf.py` now produces the same message, the TOML one only prefixed `<toml>: [variants]: ` — pinned over eight bad-file shapes. The oversized-integer case reads the same on both routes with no prefix at all (ub-project raises it bare) and is pinned as exact equality. No manifest or lock change (`ub-project>=1.1.0` already declared; `import-check-needs` against PyPI's 1.1.0 is green). Nothing under `packages/ub-project`. ### Review One adversarial reviewer (110 inputs and 25 whole builds through master's code and this branch's: identical accept/reject sets and merge results; only the enumerated class and wording differences), one fix round (the fifth change, the `ValueError` catch, three pins) and a validation round. Found on the way and filed: #1995 (ub-project's bare `ValueError`, and its `/dev/null` message) and #1997 (a `UV_PYTHON` cell run re-syncs the root `.venv` through the `fetch-plantuml` deps task).
chrisjsewell
added a commit
that referenced
this pull request
Sep 30, 2026
…ct, keeping `[needs] variant_data*` (#2000) ### What sphinx-mounts now reads its variant map from `[variants]` — the table of `ubproject.toml` that sphinx-needs 9.0.0 reads (#1996) and every tool reading the file is to share — through `ub-project`, the shared reader, and keeps reading the legacy `[needs] variant_data` / `variant_data_file` keys. Its private copy of the variant-data functions (validate / load / deep-merge / resolve) and `VariantDataError` are deleted: the workspace now holds one implementation, ub-project's. `ub-project>=1.1.0,<2` becomes a runtime dependency. The map still comes from sphinx-needs whenever it is loaded, decided from the config alone (no import, no version gate): - **sphinx-needs not loaded** (not installed, or not in `extensions`): `ub_project.read_variants` reads the top-level `[variants]` / `[needs]` of the file sphinx-mounts already reads, and its findings are reported here. - **loaded, non-empty map**: unchanged — the confvals, re-merged with ub-project's idempotent `resolve_variant_data`; the TOML's variant data is not read. A `needs_variant_data_file` that is not a path (8.3.1 copies the TOML's value unvalidated) is refused. - **loaded, EMPTY map**: the file is read (findings unreported) only for the mispointing guard, which now covers `[variants]` too. When sphinx-needs is pointed at the file, it is read as sphinx-needs read it: a key overridden with `-D` is removed from both locations first, exactly as sphinx-needs removes it. ### Behaviour (enumerated) Well-formed `[needs]` projects are unchanged in every installation (not loaded, sphinx-needs 8.3.1, 8.5.0 and this repository's; pointed, unpointed, elsewhere; every `-D` spelling — measured on a 30-project × 4-installation × 7-`-D`-form grid, and every legacy cell of a 432-build `-D` grid on this branch has master's exit code). What changes: 1. A `[variants]`-only project with sphinx-needs not loaded builds its gated content; before, the table was not read and every gated file/mount was silently excluded. 2. A `[variants]`-only project with sphinx-needs loaded but unpointed, or pointed at another file, is refused by the guard — as a `[needs]` project already was. 3. **New refusal** — see below. 4. Both locations set, sphinx-needs not loaded: `[variants]` wins, whole, with one `mounts.variant_data_location` warning per ignored `[needs]` key (new subtype; a `-W` build that passed now fails — `suppress_warnings = ["mounts.variant_data_location"]`). 5. Unknown key in `[variants]`, sphinx-needs not loaded: `mounts.unknown_key`. Newly red under `-W` only when the data is in `[needs]` and `[variants]` holds only unknown keys (a project with data in `[variants]` already failed `-W` on `mounts.variant_rule_unevaluable`). 6. Data read from the legacy location, sphinx-needs not loaded: one `-v` line per key; no warning (the deprecation warning is a later step). 7. Newly refused where sphinx-mounts reads the data: `variants = "x"`; a malformed `[variants]` (missing file, `data_file = ""`, invalid JSON/shape); `needs = "x"` with a gating key declared (was silently no data). 8. **Less strict:** a malformed `[needs] variant_data*` shape is no longer refused by a build that does not use it (no gating key; or sphinx-needs supplies a non-empty map, e.g. from `conf.py`). sphinx-needs still refuses it when pointed at the file. 9. Messages for unreadable data are ub-project's and name the TOML and table: a directory is reported as a directory; `variant_data_file = ""` is refused as empty rather than read as the TOML's directory; a JSON integer beyond Python's conversion limit (#1995) is the clean `mounts.variant_data_unreadable` refusal instead of a raw "Handler … threw an exception"; with sphinx-needs not loaded, a `[needs] variant_data*` of the wrong type is `mounts.variant_data_unreadable` instead of `TomlConfigError`; with 8.3.1 pointed, `variant_data_file = 1` is `mounts.variant_data_unreadable` naming the value. 10. A TOML data file that is missing or malformed with sphinx-needs loaded but not reading that file: still refused, now as the read error, and the message still names the one-line `needs_from_toml` fix. ### The new refusal sphinx-needs pointed at this file (`needs_from_toml`), its map EMPTY, while the file's `[variants]` — read as sphinx-needs read it, with any `-D`-overridden key removed — yields a NON-empty map. A sphinx-needs pointed at the file resolves the file's own map unless it did not READ the table: because it predates 9.0.0 (measured on 8.3.1 and 8.5.0), because it abandoned the file on a `[needs]` error before reaching `[variants]` (measured: `[needs] schema = "x"` on this repository's sphinx-needs), or because `needs_from_toml_table` scopes it to `[<prefix>.variants]`. Before this PR the gated content was silently lost in all three. The message names the file, the gating keys, and the cause — "a sphinx-needs before 9.0.0 does not read it, and a later one does not when it could not load this file's `[needs]` table (its `needs.config` warning above says why)", with the remedy "upgrade sphinx-needs to 9.0.0 or later and fix any `[needs]` error it reports, or keep the two keys in `[needs]`"; or, under a prefix (rendered as TOML, `[tool."a.b".variants]`), "move the table under the prefix (merging it into the one there, if any), or stop setting `needs_from_toml_table`" — and ends `[mounts.variant_data_unreadable]` (hard, not suppressible). Legitimately empty data (a data file of `{}`, an empty `[variants.data]`) is not refused, and a `-D` cannot make the check fire: sphinx-needs removed the overridden key from this file, and so does the check. Because the key is removed as sphinx-needs 9.0 removes it, on an older sphinx-needs an override of `variant_data` / `needs_variant_data` also drops a `[variants.data]` that release never read (rc 0, as on master). ### What is deliberately not fenced - **Both locations set + sphinx-needs < 9.0.0 pointed.** Both tools use `[needs]` and `[variants]` is lost with per-rule warnings only — unless a `-D` empties the `[needs]` map, when the check reads `[variants]` and refuses. Catching the rest needs "confval map ≠ TOML map", which false-positives on a correct 9.0 project whose `conf.py` map merges with the TOML's file. Documented as a mid-migration shape ("finish the move, or require sphinx-needs 9.0.0"). - **A `-D` over a file sphinx-needs is not reading** is not applied to it: that file keeps every declaration, so an unpointed project is still refused with the pointing fix, whatever `-D` says. - **Diagnostics when sphinx-needs is loaded**: never reported by sphinx-mounts (no double report). - A leafless `[variants.data] a = {}` counts as non-empty (an old sphinx-needs is refused where a 9.0 one warn-and-excludes to the same document set); harmless, named in the guard's docstring. - The example project (`tests/example/`) stays on `[needs]`: ubCode, which reads that file, has no `[variants]` reader yet. ### Docs / contract - `docs/configuration.rst`: "Where the variant data comes from" rewritten for `[variants]` (legacy still read, whole-location precedence, top-level only, the report-when-not-loaded rule, the `-v` line), linking sphinx-needs' section by URL; the refusal note gains the new refusal (both causes, the prefix, the `-D` rule, the empty `conf.py` map) and the residual; the examples use `[variants.data]`; the warning table gains `mounts.variant_data_location` and widens `mounts.unknown_key`; the hard-codes paragraph widens `mounts.variant_data_unreadable`. - `design/mapping-contract.md` §7.1, §11 (a `[variants]`-only file is read here and not by ubCode until its reader ships), §12.4, §12.6 (ub-project computes the map; both locations; the three routes; the `-D` rule; the joined anchor). - `packages/sphinx-mounts/AGENTS.md` lists `variants.py`. - `packages/sphinx-needs/docs/configuration.rst`: the `[variants]` note now says sphinx-mounts reads it from its next release, 0.3.0. - Changelog: the `Unreleased` bullets in `packages/sphinx-mounts/docs/changelog.rst`. ### Tests - New `tests/test_variants_table.py` (50 items, Sphinx-level; every sphinx-needs-present case simulates it with `_stub_conf`, because the release compat cell runs against the PUBLISHED sphinx-needs, which does not read `[variants]`): the not-loaded route (map built, location warnings one per key + suppression, unknown key + suppression, legacy not-a-warning / `-v` line, `variants`/`needs` not a table, malformed `[variants]`, the oversized integer), the guard (unpointed, elsewhere, the new refusal naming both causes, three prefix renderings, the route-C refusal in four sub-cases with and without the pointing fix), the new cell's false positives (legitimately empty ×2, `-D` of every declared key in both spellings ×4, `-D` over a missing file in both locations and spellings ×4), what `-D` must not excuse (an old sphinx-needs over `[variants.data]`; a file sphinx-needs is not reading), the new cell staying off `[needs]`, a non-path confval data file, the dotted `-D` on route B, no double report ×2, and an AST fence that `sphinx_mounts` never imports `sphinx_needs`. Warnings asserted by code and required phrases, never by ub-project's wording. - `_stub_conf` can register `needs_from_toml_table`; the route-B stand-down test also asserts its status line. - `tests/test_variant_data.py` deleted (33 items, all on the deleted copy): ub-project's suite pins a superset of its shapes and both idempotency properties; the Sphinx-level three-cell tests stay. - Red first, then mutation: every fence of a behaviour change failed against the unfixed reader; the fences of a false positive are proven by mutation — 12 mutations of the new code (reinstating the old exemption, stripping on every route, never stripping, each conjunct of the new cell, `declares`, route-C reporting, the `ValueError` catch, the `-v` line, the absent-route map, the prefix branch, a guarded `sphinx_needs` import), each red, plus the wrong versions review found surviving (the strip's scope and spellings, in-place mutation, a `PathLike` confval, `""` → `None` on the confval route, the prefix renderer's escaping), now red. - `poe test-mounts`: 959 passed, 2 deselected, 0 skipped; `test-mounts-sphinx7` (Python 3.11): 950 passed, 2 xfailed (pre-existing). End to end: 112 builds across sphinx-needs not loaded / 8.3.1 / 8.5.0 / this repository's, as designed; a 432-build grid of `-D` forms in which every legacy cell's exit code equals master's; the Bazel example builds identical logs before and after, both editions. ### Toolchain (dependency, lock, compat cell) - `ub-project>=1.1.0,<2` in `dependencies` (the tight floor `check-workspace` demands). sphinx-mounts joins the set re-floored by every ub-project release (`propagate_floors.py`). - `uv.lock`: master's lock plus the two lines of the new edge in the `sphinx-mounts` block — not a re-lock (a fresh `uv lock` rewrites ~224 lines of fork markers). `uv lock --check` passes on 0.12.15 and on the hook's 0.12.9. - `compat-requirements.txt` unchanged: `ub-project` is a runtime dependency, so the compat cell resolves it from PyPI through the wheel's metadata; `poe import-check-mounts` shows the built wheel pulling `ub-project==1.1.0` from the index and importing all 8 modules. The suite passes in release.yaml's compat recipe against the published sphinx-needs 8.5.0 (measured; the tests that simulate a present sphinx-needs are stub-only, so that stays true until sphinx-needs 9.0.0 is published). - No `env_version` change: the document set still changes only through `exclude_patterns` / `mounts`, both `rebuild="env"`. **Follow-ups, not this PR:** ub-project should export `render_path`; sphinx-needs: `-D needs_variant_data=x` is rejected by Sphinx yet stays in `config.overrides`, and its `-D` removal then drops the inline map — an issue follows.
chrisjsewell
added a commit
that referenced
this pull request
Oct 14, 2026
Six members of the workspace released together, in one pull request and three tag waves: sphinx-needs 9.0.0 (the Sphinx half of the derived-values lockstep with ubCode's next release) and the five packages around it. Everything mechanical is `uv run poe bump`'s, one commit per member; the prose, the gap bullets and the version-marker sweep are by hand, each claim measured against the tree. ### What this releases | member | from → to | level | why | |---|---|---|---| | sphinx-ai-index | 0.1.0 → **1.0.0** | major | the `ai_docs_index.json` it writes has been the format's `"1.0"` since the first release, and ubCode reads it in production — that is the stability 1.0 declares (#2034); this is also its first release from this repository, on the workspace's Python 3.11 / Sphinx 7.4–9 / docutils 0.21 floors | | sphinx-mounts | 0.2.0 → **0.3.0** | minor (0.x) | variant data from `[variants]` through ub-project, with new refusals and two new `-W`-failing warnings; Python 3.11 again; docutils 0.21; first release from this repository | | sphinx-needs | 8.5.0 → **9.0.0** | major | computed values (derived fields, dependency order), `needextend` filters and the needs.json readers can change need data in certain corner cases; `[[…]]` in content and `need_func` removed; Python 3.11; `[variants]`; `needstable.js` | | ub-test-reports | 1.0.0 → **1.1.0** | minor | parser and converter fixes that add `ReportReadError` and change which exceptions escape and what a nested-suite report converts to | | sphinx-codelinks | 1.4.0 → **2.0.0** | major | requires Sphinx-Needs 9; `get_rst` / `[analyse.marked_rst]` refused, the `@rst` blocks replaced by multi-line needs; first release from this repository | | sphinx-test-reports | 2.0.0 → **3.0.0** | major | the Sphinx-free half is ub-test-reports; the import name moves to `sphinx_test_reports`; requires Sphinx-Needs 9 and ub-test-reports 1.1 | ub-project stays at 1.1.0: nothing to release. The six versions, the `__version__` literals, the floors (`sphinx-ai-index>=1.0.0,<2` in four `docs` extras; `sphinx-needs>=9.0.0,<10` in codelinks and test-reports; `ub-test-reports>=1.1.0,<2` in test-reports), the docker fallback (`sphinx-needs-v9.0.0`), the lock and the six changelog headings are `uv run poe bump`'s, one commit per member in the planner's order. ### What was added by hand - **sphinx-needs 9.0.0**: bullets for the changes the entry had not recorded —‼️ `[[…]]` in need content is plain text and the `need_func` role is removed (#1919); ✨ the needtable script (`needstable.js`, `needs_table_page_size` / `needs_table_page_sizes`, Excel / PDF export and column reordering removed; #1920); ✨ `[variants]` read through ub-project, a new dependency, with two new warning types (#1996, #1998); ⬆️ docutils 0.21 (#1977); ⬆️ jsonschema-rs up to 0.58 (#1851, #1980); 👌 deterministic CSS order (#1914); 🔧 the uv workspace, the `#subdirectory=packages/sphinx-needs` git install and the `sphinx-needs-v<version>` tags (#1839, #1846), in a new "Internal changes" section; 🔧 the sdist without `tests/` / `performance/` (#1905); 🐛 needgantt's integer completion (#1870); and the derived-fields bullet cites #2204 and #2205. - **sphinx-mounts 0.3.0**: the move bullet (#1850). - **sphinx-codelinks 2.0.0**: 🐛 `get_current_rev` reads `packed-refs` (#1871); the‼️ floor bullet says Sphinx-Needs 9.0 / `>=9.0.0,<10`; `codelinks write rst` "will be removed in 3.0.0"; the documentation is served by Read the Docs; the `ingest_need_record` note trimmed. - **sphinx-test-reports 3.0.0**: the floors it ships (`sphinx-needs>=9.0.0,<10`, ub-test-reports 1.1.0). - **ub-test-reports 1.1.0**: the UTF-8 bullet links #2024. - **The six summary paragraphs**, one per entry (sphinx-ai-index's replaces its old lead). - **Pages outside the changelogs** (each states what this release ships): - sphinx-codelinks' `codelinks write rst` deprecation (`WRITE_RST_DEPRECATED` in `cmd.py`, and `docs/components/write.rst`) now promises removal in 3.0.0, not 2.0.0, and is dated 2.0.0, not an uncut 1.5.0: 2.0.0 ships with the command. - `docs/basics/introduction.rst` says `src-trace` renders multi-line needs. - sphinx-needs' `docs/directives/choose.rst` says `versionadded:: 9.0.0`, not an uncut 8.6.0, and `docs/configuration.rst` says sphinx-mounts reads `[variants]` "from 0.3.0", not "from its next release". - sphinx-test-reports' `docs/install.rst` names the Sphinx-Needs 9 floor (`>=9.0.0,<10`). - Comments and agent notes naming the old `>=8.5.0,<9` floor (`compat-requirements.txt`, two `AGENTS.md`, `src_trace.py`) name 9.0.0. - A sweep of all 216 version markers (`versionadded` / `versionchanged` / `deprecated`) in the six members' docs and sources, against each member's released versions, found 13 more that named a release never cut. Each now names the release that shipped it: mounts 0.2.0 ×2, sphinx-needs 3.0.0 ×7, 4.0.0 ×1 and 7.0.0 ×1, test-reports 1.1.0 ×2. ### Merge and tag order Merge RIGHT BEFORE tagging. **Re-stamp `:Released:` on the merge day first** (it is stamped 2026-10-14, the earliest tag day); one line per file, each in its own format: | file | line | format | |---|---|---| | `packages/sphinx-ai-index/docs/changelog.rst` | l.11 `:Released: 2026-10-14` | ISO `YYYY-MM-DD` | | `packages/sphinx-mounts/docs/changelog.rst` | l.11 `:Released: 2026-10-14` | ISO | | `packages/sphinx-needs/docs/changelog.rst` | l.12 `:Released: 14.10.2026` | `DD.MM.YYYY` | | `packages/ub-test-reports/docs/changelog.rst` | l.11 `:Released: 2026-10-14` | ISO | | `packages/sphinx-codelinks/docs/changelog.rst` | l.11 `:Released: 14.10.2026` | `DD.MM.YYYY` | | `packages/sphinx-test-reports/docs/changelog.rst` | l.11 `:Released: 14.10.2026` | `DD.MM.YYYY` | Then tag the squash commit `<sha>` explicitly (so nothing merged later can slip into a release), in three waves, each after the previous wave's `publish` jobs are green — the plan job's check 4 refuses a member whose workspace dependencies (extras included) are not on PyPI at the version this tree builds: ```bash # wave 1 — dependencies already on PyPI (ub-project 1.1.0) or none git tag sphinx-ai-index-v1.0.0 <sha> && git push origin sphinx-ai-index-v1.0.0 git tag ub-test-reports-v1.1.0 <sha> && git push origin ub-test-reports-v1.1.0 # wave 2 — once sphinx-ai-index 1.0.0 is on PyPI git tag sphinx-mounts-v0.3.0 <sha> && git push origin sphinx-mounts-v0.3.0 git tag sphinx-needs-v9.0.0 <sha> && git push origin sphinx-needs-v9.0.0 # wave 3 — once sphinx-needs 9.0.0 and ub-test-reports 1.1.0 are on PyPI git tag sphinx-codelinks-v2.0.0 <sha> && git push origin sphinx-codelinks-v2.0.0 git tag sphinx-test-reports-v3.0.0 <sha> && git push origin sphinx-test-reports-v3.0.0 ``` The bare `9.0.0` tag is pushed by `release.yaml`'s last step for sphinx-needs ("Push the bare companion tag"); it is what moves Read the Docs' `stable` and keeps `git+…@<version>` pins working. Only if that step fails: `git tag 9.0.0 sphinx-needs-v9.0.0 && git push origin refs/tags/9.0.0`. A tag pushed too early fails only its plan job, with nothing published: re-run it once the dependency is up. After a green `publish`, a red later job is re-run with **Re-run failed jobs**, never *Re-run all jobs*. ### Before wave 1 Five of these PyPI projects release through `release.yaml` for the first time — sphinx-ai-index, sphinx-mounts, sphinx-needs (8.5.0 went out through the old token-based workflow), sphinx-codelinks and sphinx-test-reports. Each must list the trusted publisher **owner `useblocks`, repository `sphinx-needs`, workflow `release.yaml`, environment `pypi-<dist>`**. The GitHub side is ready (the five `pypi-<dist>` environments exist, each with the tag policy `<dist>-v*`); the PyPI side can only be checked by a project owner. A wrong publisher fails `publish` with nothing uploaded — fix it and re-run from the same tag — but sphinx-ai-index's is the very first tag, so it would stall the whole train. ### Rehearsal Two rehearsals can pass before the merge, from this branch: ```bash gh workflow run release.yaml --ref release/9.0.0-train -f tag=sphinx-ai-index-v1.0.0 gh workflow run release.yaml --ref release/9.0.0-train -f tag=ub-test-reports-v1.1.0 ``` Dispatched from this branch at `9a2dd5ec`: run [38479550037](https://github.com/useblocks/sphinx-needs/actions/runs/38479550037) (`sphinx-ai-index-v1.0.0`) and run [38479553168](https://github.com/useblocks/sphinx-needs/actions/runs/38479553168) (`ub-test-reports-v1.1.0`). A dispatch runs `plan` and `build` (the resolver gate and the compat cell) and never `publish`. It must name this branch: at `master` the tag's version is not the tree's (check 2). The other four cannot pass yet, because check 4 stays fatal in a rehearsal: sphinx-mounts and sphinx-needs need sphinx-ai-index 1.0.0 on PyPI (their `docs` extras), sphinx-codelinks and sphinx-test-reports need sphinx-needs 9.0.0 (and test-reports ub-test-reports 1.1.0). They can be rehearsed from `master` once the wave before them has published. ### What goes red and why - **Docs-Linkcheck** (not a required check): the new `8.5.0...sphinx-needs-v9.0.0` compare link 404s until the `sphinx-needs-v9.0.0` tag exists. - **`uv run poe import-check-codelinks` / `import-check-reports`**: red until wave 2 publishes — "only sphinx-needs<=8.5.0 is available and sphinx-codelinks==2.0.0 depends on sphinx-needs>=9.0.0,<10". Run them on `master` between waves 2 and 3: red then would be a real missing symbol. The other five import checks are green on this branch. - **Docker**: the fallback ref becomes `sphinx-needs-v9.0.0`, so the `docker/**` pull-request and manual-dispatch path, red since the move, goes green once that tag exists. This PR does not run it (its trigger is `docker/**` only), and the tag push builds the image from the tag itself. ### For a human (outside the repository) - **PyPI trusted publishers**: the five checks under "Before wave 1". - **codelinks.useblocks.com deep links**: the old GitHub Pages site served pages at the root (`…/components/analyse.html`). Under Read the Docs they 404 and live under `/en/latest/`. A Read the Docs page redirect (old path → `/en/latest/<path>`) on the `sphinx-codelinks` project would keep bookmarks and inbound links working. - **Read the Docs `stable` for sphinx-mounts and sphinx-codelinks** serves the Sphinx-Needs 8.5.0 documentation today (both projects build from this repository, whose highest PEP 440 tag is sphinx-needs' bare `8.5.0`). After this train it follows the bare `9.0.0` tag, then every later sphinx-needs release, and never a mounts- or codelinks-only one. Consider deactivating `stable`, or making `latest` the default version, on those two projects. ### ubCode ubCode's next release names sphinx-needs 9.0.0 as the version its derived-values reading pairs with. This changelog names no ubCode version, by design: it keeps the entry's "phase-2 release" wording. The ubCode release pull request is prepared separately. Closes #2034
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.
sphinx-needs becomes a consumer of
ub-project(≥ 1.1.0) for the variant data in theneeds_from_tomlfile. A project may nowdeclare its variant data in a top-level
[variants]table — or[<prefix>.variants]underneeds_from_toml_table— and the legacy[needs] variant_data/variant_data_filekeys keep working, reported only as a-vline.What changes (each pinned by a test in
tests/test_variants_table.py)[variants] data/data_file(and[<prefix>.variants]) are read.[variants]wins whole; oneneeds.variant_data_locationwarning per ignored[needs]key.[needs]→ one-vline per key; nothing without-v;-Wstays green (ubCode's quickstartvariantstemplate,copied verbatim, is the test).
[variants]→ aneeds.variants_unknown_keywarning, silenced by exactly that subtype.variants = <not a table>→NeedsConfigException(was silently ignored).[variants]and no[needs]table is read (it used to warn'needs'and read nothing) — even an empty[variants](orvariants = {}), which declares nothing, turns that warning (rc 1 under-W) into silence.(
<toml>: [needs] variant_data must be a table, got str,<toml>: [needs]: variant data file not found: <p>); the sameNeedsConfigException, except a directory or non-UTF-8 data file, which used to escape as a rawExtensionError. Theconf.pyroute is untouched.variant_data_file = ""or a list in the TOML →NeedsConfigExceptionnaming the key (was anExtensionErrortraceback).Also: an empty segment in
needs_from_toml_table([""]) is now a configuration error (NeedsConfigException), where a["".needs]table used to be read. A TOML that cannot be parsed at all still only warns, whatever the parser raised (pinned with a deeply nested array and aninteger past Python's conversion limit — two exception classes, so the catch cannot be narrowed to one).
What stays byte-identical, and how that is shown
Every existing test passes unmodified (1805 → 1847 = 1805 + 42 new, the same 11 skips).
-Doverrides one key: an overridden key isremoved from both locations before the read, so it neither opens a file nor decides the location.
conf.pyvalues still lose tothe location read per key and fill the gaps it leaves — the reader writes the declared keys of the location read to the two confvals one by
one, and
resolve_variant_data_configmerges them exactly as before (the data file is therefore read twice). The data-file confvalis still written through
_abs_path, so its string (resolved, symlinks followed) is unchanged. Every existing warning text andsubtype is unchanged. Seven mutations of the reader (merged map written,
-Dstrip dropped, prefix ignored, missing-[needs]continuation dropped, joined path written, both-set warning on
needs.config, legacy line as a warning) each turn a named test red,and so do eight more for the edges (the
[needs]loop reading the ignored keys, the continuation checking the wrong table orswallowing any error, the parse narrowed, the empty-segment
ValueErrorescaping, the cause dropped, an unknown code warning,an empty
[variants]not counting).The dependency
ub-project>=1.1.0,<2, a runtime dependency (stdlib-only). The lock carries the two-line edge only;uv lock --checkpasses onuv 0.12.15 and 0.12.9.
import-check-needsresolvesub-project==1.1.0from PyPI.The prefix
With
needs_from_toml_table = ["tool"]the tables are[tool.needs]and[tool.variants], because apyproject.tomlreserves itstop level; a top-level
[variants]is then someone else's table and is neither read nor reported — the contract's rule(
reading-contract.md§6.2), and the one ubCode (which has no prefix) agrees with.The
-vline and what followsThe legacy location is only a verbose line in this release, so projects created by ubCode's quickstart keep building under
-W. Alater release adds a
needs.variant_data_legacy_locationwarning, after ubCode's reader release; ubCode and sphinx-mounts do notread
[variants]yet, and the docs say a project built by either keeps[needs]until they do.Docs
A
[variants]subsection underneeds_from_tomlinconfiguration.rst, cross-linked fromneeds_variant_dataandneeds_variant_data_file; the two new subtypes appear in the generated warnings list. Theneeds_from_tomlcaution that saidrelative TOML paths resolve against the
conf.pydirectory is corrected: they resolve against the TOML's own directory (and did).The changelog entry is written at release time.
Review
Two adversarial reviewers, one fix round and a validation round; the refuter's differential ran the recon's 122 constructions through
master's reader and this one side by side. Two things found on the way are filed rather than fixed here: #1991 (a dotted
-D needs_variant_data.key=…is lost under a TOML inline table — pre-existing, and inherited by[variants]identically) and #1995(ub-project lets a bare
ValueErrorthrough for an oversized integer in a data file; this reader reports it as a configuration erroruntil ub-project names the file itself).