Repository navigation
🐛 needflow: fix legend crash, id collisions, border_color handling and highlight/warning divergences - #1768
Merged
chrisjsewell merged 22 commits intoAug 15, 2026
Conversation
ubmarco
approved these changes
Aug 15, 2026
Codecov Report✅ All modified and coverable lines are covered by tests. Additional details and impacted files@@ Coverage Diff @@
## master #1768 +/- ##
==========================================
+ Coverage 86.87% 90.14% +3.26%
==========================================
Files 56 74 +18
Lines 6532 11128 +4596
==========================================
+ Hits 5675 10031 +4356
- Misses 857 1097 +240
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:
|
chrisjsewell
added a commit
that referenced
this pull request
Aug 15, 2026
#### What moved where Both needflow engines independently resolved the allowed link types, walked out from `root_id`, filtered, built the nesting tree, resolved every node's presentation and collected the edges — in code that had drifted apart (#1768 fixed the divergences that had already become bugs). All of that now happens once, in a new `sphinx_needs/directives/needflow/_model.py`, which hands the engines a graph of dumb dataclasses (`NeedflowGraph`, `GraphNode`, `GraphEdge`, `NodePresentation`) holding resolved values and no syntax: a colour is a colour, not `#FF0000` or `line:FF0000`, and nesting is a tree, not braces or a subgraph. The emitters keep only what is genuinely theirs: - `_plantuml.py` (553 → 420 lines): source text, the injective entity naming, brace nesting, the `' Config` block, legend/debug/caption/align/scale, DOM wiring. - `_graphviz.py` (739 → 632 lines): source text, `subgraph cluster_*` + the invisible ghost node + `ltail`/`lhead`, the plantuml→dot shape and arrow translation tables, `graphviz_style`, its own legend/debug, the HTML visitor. - `_shared.py` (115 → 44 lines): just the "Used filter:" paragraph, the one piece of docutils content neither engine writes in diagram syntax. `resolve_link_types` is deliberately a separate entry point from `build_graph`, so that plantuml keeps warning about link types BEFORE its `sphinxcontrib.plantuml` import guard. #### The zero-delta proof Behaviour is frozen, and the gate is the generated diagram source itself. A harness captures every `@startuml…@enduml` and every `digraph needflow {…}` at the processor level, for EVERY `tests/doc_test` project containing a needflow, the whole `docs/` build, and two purpose-built synthetic projects covering the option paths the fixtures miss — each built with BOTH engines: 21 projects × 2 engines = 42 builds, 206 diagram sources. Baseline captured at master, re-run after every commit: zero byte deltas, no exceptions. The full Sphinx warning stream is compared as a second channel and is also byte-identical, which is what fences "no new warnings on any default path". The gate itself was review-verified: deliberate mutations (a one-character emission change; a deleted emitter call) turn it red in both the content and the missing-diagram direction, and its capture seam was independently checked against the real renderer boundaries at both the base and the tip. #### The one normalisation The directive defaulted `root_direction` to `"all"`, a value outside the `Literal["both", "incoming", "outgoing"]` its own `NeedsFlowType` declares; `filter_by_tree` has always treated it identically to `"both"` via the else-branch. The default is now `"both"` — the runtime now matches the declared type, and the byte gate proves the graphs are unchanged. No TypedDict shape change, so no `ENV_DATA_VERSION` implication. #### What stayed frozen, and why This slice exists so the NEXT one (a portable option vocabulary) can be implemented once instead of twice. Fixing behaviour here would have destroyed the only proof available that the extraction is faithful. So these are reproduced exactly, and now documented where they happen rather than being folklore: - `filter_by_tree` walks depth-first (`dict.popitem`) with first-visit-wins, so `root_depth` is traversal-order dependent. - `parent_needs` is stripped from the allowed link types BEFORE the root walk, so `root_id` cannot follow the need hierarchy. - The root walk runs before the filter. - `show_link_names` is OR-ed with `needs_flow_show_links`, so config can only turn labels on. - A plantuml edge is emitted even when an end is not drawn (plantuml then invents a bare node); graphviz drops it. The model records `source_drawn`/`target_drawn` and lets each engine keep its rule. - The graphviz engine used to test for an empty result before applying the `max_items` cap, and the model now applies the cap first. The verdict is unchanged either way: `apply_max_items` returns the needs untouched or truncates them to a limit of at least one, so it can never turn a non-empty result into an empty one. - A need whose parts and children were all filtered out still gets its (empty) plantuml nesting block and its `' parts:` comment: those are decided from the need, not from what is drawn. - A need drawn as a graphviz subgraph emits its `type_style` untranslated and unwarned, unlike a plain node. - The per-engine legend content, debug node type, caption/align/scale handling and `:config:` namespaces are untouched, in their own emitters. #### Tests `tests/test_needflow_model.py` — 47 new unit tests, 0.2s, no documentation project built: the walk (direction, depth, cycles, links out of the view), allowed-link-type resolution, presentation (highlight beating border colour, part→rectangle, colour normalisation), the nesting tree, and the edge membership rule. Purely additive; no existing assertion was touched. The full suite passes (1337 passed, 11 skipped). **Changelog**: one bullet under `Unreleased` → `Internal changes`, the section this repository keeps for changes that do not affect user-facing behaviour: ♻️ needflow's two engines now share one graph-model pass, with no change to the generated diagram source. --- _Generated by [Claude Code](https://claude.ai/code/session_01BVNJ8SV3EVcoc6TdLVmnVn)_ --------- Co-authored-by: Claude <noreply@anthropic.com>
This was referenced Aug 15, 2026
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.
Seven independently evidenced
needflowdefects, each with a regression test. No newoptions, no deprecations, no refactor — the two engines keep their current behaviour
everywhere else. (First slice of the needflow portable-options work; the option
abstraction itself comes separately.)
Fixes
:show_legend:crashed the build for a need type without acolor(needflow nodes extremely dark under dark mode (Sphinx 9.1.0, Sphinx‑Needs 7.0.0) #1664).coloris optional inneeds_types, butdiagrams_common.create_legend(plantuml) and
_graphviz._create_legendboth subscripted it, so the build ended withKeyError: 'color'on either engine. The type now keeps its legend row with a blankswatch instead of being dropped, and a type whose
coloris an empty string no longeremits an empty swatch value. This is the other half of needflow nodes extremely dark under dark mode (Sphinx 9.1.0, Sphinx‑Needs 7.0.0) #1664, whose fix covered the node
colour only — its fixture had no
:show_legend:, so the legend path was never covered.PlantUML entity names cannot contain punctuation, so
R-1andR=1both becameR_1,and the second need plus its edges silently disappeared (the edge became the self-loop
R_1 --> R_1). Entity names are now assigned per diagram from a stable, injectivemapping. The shared
make_entity_nameis deliberately unchanged, becauseneedumlimports it and its entity names are user-visible references there.
:border_color:mishandled a leading#, and a variant matching nothing.#00FF00reached graphviz as##00FF00and PlantUML asline:#00FF00, which PlantUMLrejects outright (the diagram failed to render). Separately, graphviz wrapped
match_variantsinstr(...), so an unmatched variant expression became the literalcolour
#None. Both engines now normalise the value through one helper and add their ownprefix, and an unset result means "no border colour" on both. The two graphviz code paths
— plain node and subgraph — are covered by the same test and must agree.
:highlight:filters that consultneedsfailed for a need with parts or children.Graphviz draws such a need as a subgraph, and that path called
filter_single_needwithout the needs list, while the plain-node path passed it. An expression like
len(needs) > 1therefore raisedNeedsInvalidFilter: name 'needs' is not definedandfailed the whole build, purely because a need happened to have a child.
alt, so the intendedalt="needflow graphviz diagram"default was unreachable andevery image was published with
alt="". The placeholder now applies when:alt:isabsent; writing
:alt:with no value still givesalt=""for a diagram that is purelydecorative. This is the one user-visible rendering change in the PR.
:config:warning namedneed_flows_configs,which does not exist (it is
needs_flow_configs), and the graphviz unknown-:link_types:warning was emitted without a location, so it arrived with no
file:line— unlike theplantuml one.
Environment version
ENV_DATA_VERSIONgoes 5 → 6, because thealtfix changes how the option is recordedwhen it is not given (
""→None) and that value is persisted in doctrees. Without thebump the two halves can disagree within one version: an older reader over a newer doctree
publishes the literal
alt="None", and a newer reader over an older doctree leaves the fixsilently inert. The first build after upgrading re-reads every document.
Tests
tests/test_github_issues.py::test_doc_github_1664_legend(both engines), on theexisting
doc_test/doc_github_issue_1664project extended with a:show_legend:page.tests/test_needflow.py, parameterised over both engines where thebehaviour is shared, asserting on the
:debug:diagram source (and, foralt, on thebuilt
<img>): node-id injectivity,border_colornormalisation,highlightwithneeds, both:config:warning texts, the link-type warning location, the threealtcases, and the entity-name fallback contract (value + warning) covered directly.fix landed; the commits are ordered test-then-fix per defect.
today — the two
assert warnings == ""tests intests/test_needflow.pyare what keepsit that way, and they stay green.
Documentation
docs/changelog.rstgains aBug fixessection underUnreleased, and thealtsectionof
docs/directives/needflow.rstnow documents the graphviz default and theexplicitly-empty case. The
border_colorreference needs no change: accepting a leading#is a tolerance, not a contradiction of what it documents.