✨ Add variant role for inline variant data references#1737
Conversation
Introduce a `variant` role that resolves `:variant:`a.b`` immediately during parsing into a text node holding the value looked up from `needs_variant_data`. The dotted path is rooted at `var` (the prefix may be given explicitly or omitted) and reuses the existing constrained `lookup_variant_data` resolution, so no operators, calls, or item access are allowed. Missing/invalid references emit a `needs.variant` warning and produce empty text. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FCw22eieCzkuAw6DmXLPpE
The `var` root is now always implicit: `:variant:`a.b`` resolves `var.a.b` and an explicit `var.` prefix is no longer special-cased. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FCw22eieCzkuAw6DmXLPpE
The `:variant:`a.b`` examples were written as inline literals ending in a
backtick, which docutils rejects ("Inline literal start-string without
end-string"), failing the docs build (Docs-Linkcheck and Read the Docs, both
`-W`). Show the examples in an rst code-block instead, and reword the
comma-join note to avoid a literal ending in whitespace.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FCw22eieCzkuAw6DmXLPpE
chrisjsewell
left a comment
There was a problem hiding this comment.
Review: variant role for inline variant data references
Nice, focused addition. The implementation is clean, reuses the existing constrained lookup_variant_data() resolver, matches the ndf/need_func stringification conventions (lists comma-joined, scalars via str), and the docs + changelog entry are consistent (the :ref: targets role_variant and needs_variant_data both resolve, and versionadded:: 8.2.0 lines up with the next unreleased minor). Below are a few things worth considering before merge.
1. Parse-time resolution vs. incremental-build caching (main concern)
VariantRole.run() resolves the value and bakes a nodes.Text into the doctree at read/parse time. This is the deliberate difference from ndf that the PR description calls out — but it interacts badly with Sphinx's caching:
needs_variant_dataandneeds_variant_data_fileare registered withrebuild="html"(sphinx_needs/config.py:740,:758), not"env".- With
rebuild="html", changing the variant data on an incremental build re-writes output but does not re-read unchanged source docs. Their pickled doctrees keep the previously-baked value, so the output goes stale until a full-Erebuild.
The field-value <{ var.a.b }> references don't have this problem because they resolve later, during dynamic-value resolution (resolve_dynamic_values → _get_variant_data, sphinx_needs/functions/functions.py:352-353), which runs on every build after reads. So the two mechanisms the PR positions as siblings actually behave differently on incremental builds.
Two options:
- Resolve in a post-processing pass like
NeedFunc(leave a lightweight placeholder node, replace it during aneeds-resolve callback) so re-writes always pick up current data; or - If parse-time resolution is intended to stay, at minimum document the incremental-build caveat, and consider whether
needs_variant_datashould trigger an env rebuild for this role to be reliable.
(Note the file-contents case is a pre-existing gap for both mechanisms: a changed needs_variant_data_file whose path is unchanged isn't tracked as a build dependency, so neither mechanism detects it. Worth a docs note, out of scope to fix here.)
2. "not configured" warning wording (minor)
sphinx_needs/roles/variant.py — the if not variant_data: branch reports "needs_variant_data is not configured", but this also fires when the user has configured needs_variant_data/needs_variant_data_file and it merely resolves to an empty mapping. Consider softening to e.g. "no variant data is available" so the message isn't misleading in the empty-but-configured case.
3. Test coverage gaps (minor)
tests/test_variant_role.py covers scalar/nested/list and the unknown-key warning — good. Not covered:
- The
variant_data_filepath (relevant given #1 — a fixture using the file would exercise the resolved/merged dict the role reads back fromconfig.variant_data). - The "path resolves to a mapping" warning branch (
:variant:build``) and the "not configured"/empty branch.
Cheap to add and they lock in the two remaining warning paths.
4. Naming (subjective, non-blocking)
variant sits next to the existing "variant options" / needs_variants support (the [value] field syntax) and the needs_variant_data filter namespace. A reader could reasonably expect a role called variant to relate to variant options rather than pure data lookup. The choice is defensible as the inline sibling of needs_variant_data; just flagging that the vocabulary is getting crowded.
Nothing here is a hard blocker for a first cut — #1 is the one I'd want a decision on, since it affects correctness on incremental builds.
Generated by Claude Code
Review feedback on the `variant` role (#1737): - Parse-time resolution baked stale values into cached doctrees on incremental builds, since `needs_variant_data`/`needs_variant_data_file` used `rebuild="html"`. Switch both to `rebuild="env"` so a variant-data change forces a re-read and keeps the resolved role values current. Document the remaining file-contents caveat (unchanged path is not a build dep). - Soften the empty-data warning to "no variant data is available", which is accurate when the data is configured but resolves to an empty mapping. - Add tests for the mapping-reference warning, the empty/not-configured branch, and the `variant_data_file` (file + inline override) path. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FCw22eieCzkuAw6DmXLPpE
|
Thanks for the thorough review! Addressed in 341467f: #1 (incremental-build caching) — kept parse-time resolution but switched #2 (warning wording) — softened to #3 (test coverage) — added tests for the mapping-reference warning ( #4 (naming) — left as Generated by Claude Code |
Codecov Report✅ All modified and coverable lines are covered by tests. Additional details and impacted files@@ Coverage Diff @@
## master #1737 +/- ##
==========================================
+ Coverage 86.87% 89.41% +2.53%
==========================================
Files 56 73 +17
Lines 6532 10580 +4048
==========================================
+ Hits 5675 9460 +3785
- Misses 857 1120 +263
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:
|
ubmarco
left a comment
There was a problem hiding this comment.
I wonder how the various field types are serialised to text. I could not find anything in the docs about this, also not for the <{ wheel.speed }> syntax in fields.
That should be documented and tested. Users could pass a whole dictionary, or array, or float.
Address review feedback: the `variant` role always renders a single text node, but how each variant-data type serialises was undocumented. Add a type-handling table to the role docs (string/int/float → str, bool → True/False, array → comma-joined, mapping → warning + empty) and a float test case. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FCw22eieCzkuAw6DmXLPpE
|
Good point @ubmarco — the Added a Type handling table to the role docs spelling out exactly how each value renders (the role always emits a single text node):
Also added a float test case alongside the existing string/int/bool/array/mapping ones in For the
Note the key semantic difference: the field syntax preserves native types (an int stays an int in Generated by Claude Code |
Prepares the **8.2.0** release. ## Version bumps - `sphinx_needs/__init__.py` → `8.2.0` - `.github/workflows/docker.yaml` (`NEEDS_VERSION`) → `8.2.0` - `docs/_static/tutorial_needs.json` regenerated with the new version ## Changelog Rewrote the `Unreleased` section into a user-friendly `8.2.0` entry. Reviewing every PR since v8.1.1 surfaced several that were missing from the draft (notably the full variant-data feature set), so those are now included and categorised. The entry leads with the two headline features: - **Variant data** — a dedicated narrative section tying together the new variant-data tooling (#1715, #1716, #1721, #1737): `needs_variant_data` / `needs_variant_data_file`, the inline `<{ ... }>` field syntax, the `variant` role, and the `if` directive — with runnable examples showing the shared `var` namespace across filters, fields, prose, and whole blocks. - **`network_back` schema validation** (#1731) — incoming-link validation, letting coverage rules ("every requirement must be covered by a test") be expressed once on the target. Remaining changes are grouped into **Improvements** (#1632, #1717, #1736, #1730, context7), **Deprecations** (`needs_filter_data`), **Breaking changes** (Open-Needs removal, #1732), and **Bug fixes** (#1727, #1371, #1564). Internal/test-only PRs were intentionally omitted. Docs build cleanly — all cross-references in the new section resolve without warnings. 🤖 Generated with [Claude Code](https://claude.com/claude-code) --- _Generated by [Claude Code](https://claude.ai/code/session_01HQwraRU189oRFK5j4yRiYo)_ --------- Co-authored-by: Claude <noreply@anthropic.com>
Summary
Adds a new
variantrole that resolves a reference intoneeds_variant_dataimmediately during parsing, replacing:variant:`a.b`with a plain text node holding the looked-up value.This complements the existing
<{ var.a.b }>field-value references (from #3ba0da0) by exposing the same lookup as an inline role usable anywhere in body text. Unlike thendf/need_funcroles — which leave a placeholder node in the doctree for a later post-processing pass — this role resolves inrun()and returns anodes.Textdirectly.Behaviour
Given:
:variant:`platform`→arm:variant:`build.opt_level`→2:variant:`archs`→arm, x86(lists are comma-joined)The role content is a dotted path rooted at
var(thevarroot is implicit). It reuses the existing constrainedlookup_variant_data()resolution — no operators, function calls, or item access are permitted. A missing config, unknown key, or a path that resolves to a mapping emits aneeds.variantwarning and produces empty text.Changes
sphinx_needs/roles/variant.py— newVariantRole(SphinxRole).sphinx_needs/needs.py— registerapp.add_role("variant", VariantRole()).docs/roles.rst— document the new role; changelog entry added.tests/test_variant_role.py+tests/doc_test/doc_variant_role/— coverage for scalars, nested values, lists, and the warning path.