Repository navigation
🐛 sphinx-needs: an empty need.FIELD selector fails the call, as an unset one does - #2136
Merged
Merged
Conversation
An untyped extra option that a need does not set holds "", and a
need.<field> selector holding "" passed it on: copy("title", need.src)
then ran copy("title", ""), which copies the need's own title, and
calc_sum(..., links_only=need.src) summed every need instead of the
links. In a selector slot (copy's id, field and filter; calc_sum's field,
filter and links_only; check_linked_values' field and filter;
links_from_content's id and filter) "" is now unset, as None already was:
the call fails as needs.dynamic_function naming the field, in a field, an
ndf role and a :style_row: alike, and the order reads nothing for it. A
value argument still passes "" on. Found by ubCode's phase-1 review; both
tools follow the same rule.
… "" (sphinx-needs)
need.<field> selector fails the call, as an unset one does
need.<field> selector fails the call, as an unset one does
Codecov Report✅ All modified and coverable lines are covered by tests. Additional details and impacted files@@ Coverage Diff @@
## master #2136 +/- ##
=======================================
Coverage 92.78% 92.78%
=======================================
Files 137 137
Lines 20158 20159 +1
=======================================
+ Hits 18704 18705 +1
Misses 1454 1454
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:
|
5 tasks done
chrisjsewell
added a commit
that referenced
this pull request
Oct 8, 2026
## What
Package: `packages/sphinx-needs`.
This fixes `check_linked_values` and `calc_sum(links_only=True)` in a
link field whose need's own `links` are computed in
the same step. Until the links are computed, nobody knows which needs
they name, including the ones written in them.
So the call now reads its field, and the fields its filter names, as a
**column**, on every need. That is the same
column node a whole-project `calc_sum` reads:
```rst
.. req:: Reads a link type through links that are computed too
🆔 LL_WLINK
:links: T_THREE, [[copy("links", "OTHER_L")]]
:blocks: [[check_linked_values("T_THREE", "refs", "T_ONE", one_hit=True)]]
```
What the call does now depends on where its field is computed:
- **Computed in step 2, the link fields, by some need** (`refs` above):
the call runs after every need that computes
it, so it reads the computed value (above, `blocks` is `['T_THREE']`
once a linked need's computed `refs` names
`T_ONE`). It also still waits for its own `links`.
- **Computed after step 2, on any need, or a back link or dead-link
flag:** the read is out of scope. The call reports
`needs.derive_scope` and is not run, and the field keeps its written
links. For example: `dynamic function
'check_linked_values' for option 'blocks' reads 'links_back' on every
need its links name, which is final only after
the link fields are computed: the call is not run and the field is left
empty`.
- **Computed by no need:** the call reads the written value.
Two more cases:
- A call in the `links` themselves still reads itself, which is a cycle.
- A need that reads its own field through such a column is on a cycle
with it. The message then names the links:
`…, through its links, which are computed in the same step, so every
need is a candidate`.
Code changes:
- `functions/order.py`:
- `_CallReads._linked()` replaces the two walks of the stored list.
- `OutOfScope.linked` records a back link or flag read on every need the
links name.
- A column's `Reason` can now be the reader's own `links` node.
- `functions/functions.py`: the `derive_scope` and `derive_cycle`
messages for these reads.
- Docs: a paragraph in `dynamic_functions.rst`'s processing order, a
clause in the list of reads that cannot be
ordered, and a sentence on the cycle message. The changelog's
*Unreleased* derived-values entry gets a clause and
cites this pull request.
## Why
#2064 (derived values) and #2134 (phase 1, merged) order a call after
every value it reads. #2136 was a follow-up.
ubCode's review of its own phase 1 found a gap in these reads. The order
walked `need.get("links")`, which is `[]`
for a computed link list, so a written target got no edge. That had
three effects:
- Its back links were read as the pass had reset them, so
`check_linked_values(…, "links_back", …)` passed silently.
- A link type it computes was read before it was computed. Only the
run-time check caught this, as `…the order of the
pass did not account for this read; please report this…`.
- A filter on such a field silently gave the wrong value, because the
run-time check does not see a filter's reads.
A target added by the computed list was read in order or not, depending
on its id. That means the value depended on the
schedule, which the order exists to rule out.
The ruling holds for both tools: such a reader reads through a column.
ubCode's phase 1 takes the same rule, with the
same wording for the back-link case.
## Tests
-
`tests/test_dynamic_functions_strata.py::test_a_read_through_computed_links_is_a_column_read`
uses reviewer A's
shapes (`LL_WBACK`, `LL_ABACK`, `LL_WLINK`, `LL_WLINK1`, `LL_ALINK`, and
`Z_ALINK` three links down). It adds:
- a filter on a computed link type (`LL_WFLT`);
- a field computed in step 4 on a need the links do not name
(`LL_LATER`);
- a field no need computes (`LL_FINAL`, `['T_ONE']`, no finding);
- a cycle through the column (`LL_SELF`).
It runs serially and with `-j 2`, and asserts every value and every
warning. The run-time check is silent on every
shape. Red before the change:
- `{'LL_WBACK': ['T_ONE']} != {'LL_WBACK': []}` and `{'LL_ABACK':
['T_ONE']} != {'LL_ABACK': []}` (the call ran on
the reset back links);
- `{'LL_WFLT': []} != {'LL_WFLT': ['T_ONE']}` (the filter read `refs`
before it was computed, silently).
Before the change, its build also gave the "please report this" warning
for `LL_WLINK`, `LL_WLINK1` and `LL_ALINK`.
- `tests/test_functions_order.py`:
- The three outcomes as unit rows: a column `(Column('refs'), ('RD',
'links'))` for the option, a `links_only` sum and
a filter; out of scope for a later field, a back link and a dead-link
flag; final for a field no need computes.
- A link field reading through itself stays a self-cycle.
- A cycle through the column names the links.
- Red before the change: 9 of the 12 new items (the other 3 are
regression guards that already pass).
The full suite (`uv run poe test-needs -n 4 tests/`) passes, except the
50 graphviz renders that need `dot` (the same 50
fail on master on a machine without it).
## Checklist
- [x] I wrote this change myself and have read every line of it; it was
not generated automatically from an issue.
Built in an orchestrated session from ubCode's review finding and the
code. The tests were written first and shown
red, and every line was read.
- [x] I ran the package's tests (`uv run poe test-needs`) and they pass
(the graphviz renders excepted on a machine
without `dot`, identically to master).
- [x] Documentation is updated where behaviour or options change
(`docs/dynamic_functions.rst`).
- [x] The package's `docs/changelog.rst` has the change in the existing
*Unreleased* derived-values entry (its `:pr:`
cite now names this number).
- [x] `uv run poe lint` and `uv run poe typecheck` pass.
Refs #2064.
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.
What
Package:
packages/sphinx-needs.A
need.<field>argument that selects what a built-in reads (copy's id, field and filter;calc_sum's field,filter and
links_only;check_linked_values' field and filter;links_from_content's id and filter) now fails thecall when its field holds
"", exactly as it already did when the field holdsNone:With
srcan untyped extra option (needs_extra_options = ["src"]) that the need does not set,srcis"", and thecall ran as
copy("title", ""), which copies the need's own title, silently. It is now oneneeds.dynamic_functionwarning,Error while applying need to function 'copy': need.src selects what the call reads, and is not set, and the field keeps its placeholder (empty here). The same holds for the call in an:ndf:role or aneedtable
:style_row:(they share the check), and the dependency order reads nothing for such a call. Aneed.<field>in a value argument (
check_linked_values'resultorsearch_value,copy'supper) still passes""on.functions/order.py: one_UNSET = (None, "")used byunset_selector()(the run-time check of the field, the roleand
:style_row:) and by the order's own reading of a selector.dynamic_functions.rst'sneed.<field>paragraph says "unset or empty" (and that a value argument isNone,or
""for an untyped extra option, when unset); the 9.0.0 entry of the changelog likewise, in the Unreleasedentry "Dynamic functions and variants are computed in dependency order", whose cite list gains this pull request.
Why
#2064 (derived values), #2134 (phase 1, merged), which made an unset (
None) selector fail the call instead ofletting
copyfall back to the need's own field. ubCode's review of its own phase 1 found that an untyped extra optionleft unset holds
"", notNone, so the same silent fallback remained for the most common kind of extra option, andlinks_only=need.srcsummed every need instead of the need's links. Both tools now treat""in a selector as unset,with the same finding.
Tests
test_an_empty_need_attribute_selector_fails_the_call(tests/test_dynamic_functions_strata.py): an untyped extraoption left unset in
copy's id,calc_sum's field,check_linked_values' field andlinks_only, eachNoneandone
needs.dynamic_functionnamingneed.src; the:ndf:role renders??;upper=need.srcis a value and copiesnormally. Red before the change:
{'E_COPY': "copy's need"} != {'E_COPY': None}(the own-field copy) and{'E_ONLY': 2.0} != {'E_ONLY': None}(every need summed).tests/test_functions_order.py: two rows of the edge table, an empty selector ofcopyand an emptylinks_only,read nothing. Red before:
{'deps': [('RD', 'summary')]} != {'deps': []}and{'columns': [(Column(field='hours', candidates=None), None)]} != {'columns': []}.The full suite (
uv run poe test-needs -n 4 tests/) passes but for the 50 graphviz renders that needdot(the same 50on master on a machine without it).
Checklist
Built in an orchestrated session from ubCode's review finding and the code; the tests were written first and shown
red, and every line was read.
uv run poe test-needs) and they pass (the graphviz renders excepted on a machinewithout
dot, identically to master).docs/dynamic_functions.rst).docs/changelog.rsthas the change, in the existing Unreleased derived-values entry (its:pr:cite now names this number).uv run poe lintanduv run poe typecheckpass.Refs #2064.
When squashing, strip the
Co-authored-bytrailer GitHub proposes.