You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
## Summary
Introduces `needs_variant_data` (inline nested dict) and
`needs_variant_data_file` (path to JSON file) as new configuration
options, providing a namespaced, structured replacement for the flat
`needs_filter_data`. In filter expressions, the data is accessible under
the `var` namespace:
```rst
.. needtable::
:filter: var.cpu == "arm" and var.build.debug == True
.. needtable::
:filter: "feature_a" in var.build.features
```
`needs_filter_data` is deprecated with a warning pointing users to the
new option.
## Motivation
- `needs_filter_data` is flat (`dict[str, str]`) and injects keys at
root level, risking collisions with need field names.
- Users working with kconfig-like systems need nested structured data
(e.g. `{"build": {"arch": "arm", "debug": true}}`).
- A dedicated `var` namespace avoids collisions and has been developed
for compatibility with the ubcode query engine which already parses
`var.a.b` attribute chains natively.
## Design Decisions
| Decision | Choice |
|----------|--------|
| Namespace | `var` (e.g. `var.cpu == "arm"`) |
| Config style | Single slot + file override (no named profiles) | |
Switching | `sphinx-build -D needs_variant_data_file=other.json` | |
`filter_data` | Deprecated with warning, keeps working | | Leaf types |
`str`, `bool`, `int`, `float` |
| Arrays | Uniform scalar type only |
| Precedence | File loaded first, inline deep-merged on top (inline
wins) | | Attribute access | Dot-only (`var.a.b`), no item access | |
Missing key | Strict `AttributeError` (catches typos in filters) |
## Alternatives Considered
### Named profiles (rejected)
We considered letting users define multiple named variant configurations
upfront and selecting between them:
```toml
[needs]
selected_variant = "arm_debug"
[needs.variant_profiles.arm_debug]
file = "variants/arm_debug.json"
```
**Why rejected:**
- Adds config complexity without clear benefit over just switching the
file path directly.
- Users with multiple profiles already manage them as separate files;
they don't need an indirection layer in sphinx config.
- The simpler "just point at a file" approach composes better with
external tooling (CI matrices, kconfig generators, etc.) that already
produce a single output file.
- Can always be added later as a convenience layer on top if demand
materializes.
### Namespace name
Also considered:
- `env` — too easily confused with environment variables or Sphinx
`BuildEnvironment`
- `cfg` — Rust-flavored, less obvious to Python/RST users
- No namespace (keep flat like `filter_data`) — loses the
collision-safety motivation entirely
`var` was chosen as short, unambiguous, and evocative of "variant" /
"variable".
### `SimpleNamespace` via `json.loads` object_hook (rejected)
Quick and elegant for prototyping, but rejected for production because:
- No validation of leaf types or uniform arrays
- No controlled error messages for missing keys (gives generic
`AttributeError`)
- Exposes internal `SimpleNamespace` attributes (`__dict__`, etc.) to
eval context
- No way to add `__contains__` for `"x" in var.tags` support without
subclassing
### `__getitem__` fallback (rejected)
Considered allowing both `var.a.b` and `var["a"]["b"]`. Rejected
because:
- Dot-access is significantly more readable in filter expressions:
`var.build.debug` vs `var["build"]["debug"]`
- Consistent with Jinja2 template syntax where `foo.bar` and
`foo["bar"]` are equivalent — users of Sphinx/Jinja are already familiar
with dot-access for nested data
- Keeps the API surface minimal and forces a single idiomatic style
- Can be added later if needed
## User-facing Configuration
```python
# conf.py — inline (simple cases)
needs_variant_data = {
"cpu": "arm",
"debug": True,
"build": {"optimization": 2, "features": ["feature_a", "feature_b"]},
}
# Or point to a file (kconfig-generated JSON, etc.)
needs_variant_data_file = "variants/arm_debug.json"
# Both can be combined: file loaded first, inline overrides on top
```
Switch at build time: `sphinx-build -D
needs_variant_data_file=variants/x86_release.json docs/ _build/`
### Allowed Data Shape
```json
{
"cpu": "arm",
"debug": true,
"build": {
"optimization": 2,
"features": ["feature_a", "feature_b"]
}
}
```
Constraints:
- Top-level must be a dict
- Intermediate nodes: dicts only
- Leaf values: `str | bool | int | float | list[str] | list[bool] |
list[int] | list[float]`
- Arrays must be uniform (all elements same scalar type)
- No `null` values, no dicts inside arrays
## Implementation
- **`sphinx_needs/variant_data.py`** (new) — `VariantDataProxy`,
validation, JSON file loading, deep-merge
- **`sphinx_needs/config.py`** — Two new fields: `variant_data` and
`variant_data_file`
- **`sphinx_needs/needs.py`** — Resolution in `prepare_env` (load file,
merge, validate, store); deprecation warning in `check_configuration`
- **`sphinx_needs/filter_common.py`** — Injects `var` proxy at all
filter eval sites (including `filter_single_need`, `filter_import_item`,
`apply_default_predicate`)
- **`sphinx_needs/functions/functions.py`** — Injects `var` in variant
expression context
- **`sphinx_needs/ubquery.py`** — AST fast-path support for `var.*`
attribute chains (comparisons, `in`/`not in`), avoiding `eval()`
overhead
### Fast-path details (`ubquery.py`)
The AST-based fast-path (`try_build_simple_predicate`) compiles simple
filter expressions to native Python lambdas without `eval()`. This PR
extends it to handle `var.*` attribute chains:
1. `_unpack_attribute_chain()` — unpacks `var.build.debug` AST into
`("var", "build", "debug")`
2. `_resolve_chain()` — traverses the fallback context to resolve the
chain at eval time
3. Comparison handlers recognize `ast.Attribute` chains rooted in
`_FALLBACK_ROOTS` (`{"var"}`)
4. `"value" in var.field` membership tests are also compiled natively
5. Bare `var` as an `ast.Name` bails to the slow path (added to
`_CONTEXT_ONLY_NAMES`)
## Documentation
- **`docs/configuration.rst`** — New `needs_variant_data` and
`needs_variant_data_file` sections with full usage examples;
`needs_filter_data` moved to "Deprecated Options" with a `..
deprecated:: 8.2.0` directive pointing to the new options
- **`docs/directives/need.rst`** — Updated `jinja_content` context
reference
- **`docs/dynamic_functions.rst`** — Updated variant evaluation context
reference
- All predicates context lists (under `needs_fields`, `needs_links`,
`needs_global_options`) now reference `needs_variant_data`
## Tests
- **`tests/test_variant_data.py`** — Unit tests for `VariantDataProxy`,
validation, deep-merge, file loading, fast-path compilation (28 tests)
- **`tests/test_variant_data_integration.py`** — Integration tests with
Sphinx builds: inline config with `needs_warnings`, file loading with
inline override (2 tests)
- **`tests/test_ubquery.py`** — `TestVariantDataFastPath` class:
comparisons, nested access, membership, compound expressions, error
cases (23 new tests)
- Updated existing tests (`test_needs_filter_data`, `test_variants`,
`test_parallel_execution`) to expect the new deprecation warning
## Notes & Edge Cases
- **`-D` override granularity**: Sphinx `-D` cannot set nested dict keys
individually. Switching is done by pointing at a different JSON file.
- **Array uniformity**: Validated at config-load time (fail fast with
clear error).
- **`eval()` safety**: `VariantDataProxy` has no `__getitem__`,
`__setattr__`, or `__delattr__` — only `__getattr__` for reads. Cannot
mutate data from a filter.
- **Relative paths**: `needs_variant_data_file` is resolved relative to
the Sphinx `confdir`.
## Future Improvements
- **Include variant data in `needs.json`**: The resolved variant data
could be emitted in the `needs.json` metadata section, enabling
downstream tools (e.g. ubcode) to know which variant configuration was
active during the build. This would support diffing outputs across
variants and reproducing builds.
- ``status in ['value1', 'value2', ...]`` / ``status in ("value1", "value2", ...)``
250
289
- ``'value' in tags`` / ``"value" in tags``
290
+
- ``var.key == 'value'`` / ``var.nested.key == 'value'`` (see :ref:`filter_variant_data`)
291
+
- ``'value' in var.key`` / ``'value' not in var.key``
251
292
252
293
Also filters containing ``and`` will be split into multiple filters and evaluated separately for the above patterns.
253
294
For example, ``type == 'spec' and other == 'value'`` will first be filtered performantly by ``type == 'spec'`` and then the remaining needs will be filtered by ``other == 'value'``.
0 commit comments