✨ Add <{...}> variant data syntax for field values#1721
Conversation
Adds a new `<{ ... }>` "variant data" syntax that can be used in need field values to inject values from the configured `needs_variant_data`. This loosely mirrors how variant functions (`<<...>>`) work, and is only activated when a field has `parse_variants = True`.
### Behaviour
- Syntax: `<{ expression }>`, e.g. `:myfield: <{ var.platform }>`.
- Only the `var.*` variant-data namespace is available inside the expression (no need fields, `filter_data`, or `build_tags` — unlike variant functions).
- The inner expression is **evaluated** and its result is **substituted** into the field value.
- The resolved value is type-validated against the field's type (and item type for arrays); a warning is emitted on mismatch.
- Works for scalar (`string`/`integer`/`number`/`boolean`), embedded-string, and `array` fields, as well as link fields.
Example:
```rst
.. req:: Example
:id: REQ_001
:mystring: <{ var.platform }>
:myarray: a, <{ var.platform }>, c
```
### Implementation
- **variant_data.py** — new `VariantDataParsed` dataclass (parsed `<{...}>` reference).
- **needs_schema.py** — tokenize `<{...}>` in `_split_list` / `_split_string` / `_split_link_list` (gated by `parse_variants`); new `ListItemType.VD` / `VD_U`; wire conversion through `FieldSchema`/`LinkSchema` `convert_directive_option` and `convert_or_type_check`; widen container value type unions.
- **functions.py** — resolve `VariantDataParsed` items in `resolve_functions`, via a new `_get_variant_data` helper that evaluates the expression with only `var` in scope (builtins disabled) and type-validates the result.
- **need.py** — widen `_copy_links` types to handle links carrying `VariantDataParsed`.
### Tests
- test_needs_schema.py — `test_convert_directive_option_vd` covering scalar/embedded/integer/array conversion.
- test_variant_data_integration.py — `test_variant_data_fields_html` end-to-end build, backed by new doc_variant_data_fields project.
### Checks
- `tox -e mypy` ✅ `tox -e ruff-check` ✅ — 465 relevant tests pass.
> Note: for `string`-type fields, literal text and resolved values are joined with spaces (consistent with existing variant-function behaviour), e.g. `platform is <{ var.platform }>!` → `platform is arm !`.
Codecov Report❌ Patch coverage is
Additional details and impacted files@@ Coverage Diff @@
## master #1721 +/- ##
==========================================
+ Coverage 86.87% 89.41% +2.53%
==========================================
Files 56 74 +18
Lines 6532 10675 +4143
==========================================
+ Hits 5675 9545 +3870
- Misses 857 1130 +273
Flags with carried forward coverage won't be shown. Click here to find out more. ☔ View full report in Codecov by Sentry. 🚀 New features to boost your workflow:
|
Replace the `eval()`-based resolution of `<{ ... }>` variant data
references with a constrained `var.*` dotted-path lookup, mirroring the
ubcode implementation. References are now resolved by walking the variant
data mapping; arbitrary expressions, operators, function calls, and item
access are no longer supported.
- Add `lookup_variant_data()` in `variant_data.py` for the constrained,
eval-free lookup, with clear errors for invalid paths, missing keys, and
non-leaf/mapping results.
- Resolve references against `needs_config.variant_data` instead of binding
a proxy into `eval()` (the proxy is still used for variant-function and
`needif` filter expressions).
- Update docs to describe the dotted-path semantics.
- Update error-case tests/messages and add a `parse_variants: False` case
asserting the syntax is left literal.
VerdictLooks good to me, just 3 findings. 1. Bug — string interpolation inserts an extra spaceSeverity: medium (functional, user-visible) · Where: When a .. req:: Embedded string from variant data
:id: REQ_002
:mystring: platform is <{ var.platform }>!Produces (snapshot Note the double space before Root causeTwo behaviors stack:
So Suggested directionThe literal segments already carry the author's intended spacing, so the join should sphinx-needs/sphinx_needs/functions/functions.py Lines 373 to 386 in 675f33c Blast radius (for the author)
2. Implement
|
| Step | Result |
|---|---|
_split_list / _split_string |
captures the whole thing as one variant-function item; <{ var.opt }> is literal text inside it |
match_variants / _get_variant (condition true) |
returns the string '<{ var.opt }>' — not 2 |
| string field outcome | field becomes the literal "<{ var.opt }>", silently, no warning |
| typed scalar field (e.g. integer) | worse — value coercion runs at parse time, so int("<{ var.opt }>") raises a parse warning |
standalone <{ var.opt }> (for comparison) |
resolves correctly to 2 |
Note the asymmetry: var.* already works in the variant-function condition
(<<[var.cpu == "arm"]:fast, slow>> → "fast"), because _get_variant evaluates the condition
with the var proxy in context. Only the value side is missing.
The silent literal passthrough for string fields is the worst part — no warning, wrong output.
sphinx-needs/sphinx_needs/functions/functions.py
Lines 398 to 405 in 675f33c
Why it's a moderate, additive change (~3 files)
The two consumers of VariantFunctionParsed are cleanly separable: field resolution
(_get_variant) vs. filters/needflow colors (match_variants, which parses its own string
fresh). The constraint is that typed fields coerce the value at parse time, so a <{...}> value
must be parsed as a deferred reference rather than coerced. A clean shape:
variants.py— addVariantDataParsedto the value/final_valueunions and a keyword-only
parse_variant_data: bool = Falsetofrom_string; when set, a<{...}>value is stored as
VariantDataParsedinstead of being coerced. (No circular import:variant_data.pyimports
nothing fromvariants.py.)needs_schema.py— passparse_variant_data=Trueat the field/linkfrom_stringcall sites
(already guarded byparse_variants).match_variantskeeps the default, so filters/colors
are unchanged.functions.py— inresolve_functions, if_get_variantreturns aVariantDataParsed,
resolve it via the existing_get_variant_data(...)before the type-check, reusing the
validation/warning machinery this PR already added.
Result: var.* works symmetrically in both the condition and the value, for string and typed
fields, with proper type validation.
3. Test gaps (medium)
lookup_variant_dataerror branches uncovered. "is not a mapping"
(variant_data.py:74-78, e.g.var.platform.xwhereplatformis a scalar) and "resolves to
a mapping" (:86-90, e.g.var.buildas a leaf) are not exercised. Codecov flags both. One
fixture line each.- Link fields are documented but untested.
docs/dynamic_functions.rst:320lists link
fields as supported, andLinkSchemais wired for<{...}>, but no test exercises a link
field withparse_variants: Trueand a<{...}>value.
sphinx-needs/sphinx_needs/variant_data.py
Lines 64 to 91 in 675f33c
This is an existing bug that effects existing functionality, and so out of scope of this PR
This is out of the scope if this PR, and requires more thought because I am not convinced that such nested syntaxes are ideal
legitmiate, will add (acb1bb7) |
This PR adds a new
<{ ... }>syntax for injecting values fromneeds_variant_datadirectly into need field values.What it does
Within a field value,
<{ var.<path> }>is replaced by the corresponding value looked up from the configuredneeds_variant_data, e.g.:Here
archresolves to the string"arm"andoptto the integer2.Semantics
parse_variants: Trueinneeds_fields/needs_links. Whenparse_variantsisFalse, the syntax is left untouched as a literal.var.*path lookup — not arbitraryeval(). Operators, function calls, item access (var["cpu"]), need fields,build_tags, and Python builtins are not available. This keeps the semantics simple, safe, and replicable by other tooling (it mirrors the ubcode implementation).Changes
lookup_variant_data()in variant_data.py implementing the eval-free dotted-path resolution.resolve_functions()resolves<{...}>references againstneeds_config.variant_data(theVariantDataProxyremains for variant-function<<...>>andneediffilter expressions).parse_variants: Falsecase asserting the syntax stays literal.