Needs¶
The [needs] section is the heart of your ubproject.toml.
It tells ubCode how to identify, validate, and relate the need objects
in your reStructuredText files —
types, link types, custom fields, statuses, tags, and external needs.
See also
Minimal example¶
[needs]
id_required = true
id_regex = "^[A-Z]{3}_[0-9]{3}$"
[[needs.types]]
directive = "requirement"
title = "Requirement"
prefix = "REQ_"
[needs.links.implements]
outgoing = "implements"
incoming = "implemented by"
[needs.fields.priority]
description = "Priority level"
Core options¶
- id_required
Type:
boolean(default:false)If
true, every need must have an explicit ID. Iffalse, IDs will be auto-generated when not provided.- id_regex
Type:
string(default:"^[A-Z0-9_]{5,}")Regular expression pattern to validate need IDs against. Only applies when
id_requiredistrue.- id_length
Type:
integer(default:5)Length of auto-generated IDs when not explicitly provided.
- id_from_title
Type:
boolean(default:false)When
true, auto-generated IDs will be based on the need title instead of being random.- title_optional
Type:
boolean(default:false)When
true, allows needs to be created without a title.- title_from_content
Type:
boolean(default:false)When
true, auto-generates titles from the first line of need content when no title is provided.
- default_layout
Type:
string(default:"")The card design every need with no
:layout:of its own is drawn with.The value is any design this project knows — see the list under [needs.fields.layout] for what that means, and Card designs with [needs.card_layouts] for what each design shows. A
:layout:written on the need itself wins over this, and so does a value a [needs.fields.layout]defaultorpredicatesrow puts on the need: this key only catches needs that end up with no value at all. An empty string is the same as unset, which is the same asclean— the design that draws ubCode’s ordinary card. A value naming no known design is reported and treated as unset.[needs] default_layout = "complete"
Moving the default changes what writing nothing means, so
:layout: cleanstops being a no-op and becomes a deliberate override: under the example above, a need with no:layout:drawscompleteand a need with:layout: cleandrawsclean.This is the ubCode spelling of Sphinx-Needs’
needs_default_layout, and it is the same key in the same table:needs_from_tomlmaps[needs] default_layoutontoneeds_default_layout, so aubproject.tomlshared betweensphinx-buildandubc build htmlnow means one thing to both. Two behaviours differ deliberately — an empty string is unset here and a build error there, and an unknown name is reported here rather than aborting the build; see Differences from a Sphinx build.- views_max_items
Type:
integer(default:100)The default
:max_items:cap for the view directives (needlist,needtable,needflowandneedsequence).A view that sets no
:max_items:of its own renders at most this many items and appends a notice saying how many of how many are shown; a view that sets one uses its own value instead. Set0for no cap — from either source. The cap applies after the filter and the sort, in both the editor’s rendered preview andubc build html. A build also emits abuild.need_view_truncatedwarning for each truncated view, so a capped view is visible in the build log without opening the built page.- table_style
Type:
string(default:"datatables")The style a
needtablerenders in when it sets no:style:of its own.datatablesis the interactive table: the reader sorts by column, filters, pages, hides columns, and copies or downloads the rows as CSV, all in the browser.tableis the static one — a scroll frame with a sticky header row and no client-side behaviour. Aneedtablethat sets its own:style:uses that instead; a:style:naming neither style falls back to this key, as it does in a Sphinx-Needs build.This is the ubCode spelling of Sphinx-Needs’
needs_table_style, with the same default and the same two values. Nothing called DataTables is involved: the interactive table is ubCode’s and Sphinx-Needs’ own shared asset, and the value keeps its name because it is the one both tools already write.- table_page_size
Type:
integer(default:10)How many rows an interactive
needtableshows per page.A
needtablethat sets its own:page_size:uses that instead, and a reader can choose another size from the table’s own control. It must be a positive integer —0means “All” in the offered sizes below, not a page size — and an invalid value is reported and this default used. A:style: tabletable ignores it and shows every row it rendered.- table_page_sizes
Type:
list[integer](default:[10, 25, 50, 0])The page sizes an interactive
needtable’s control offers, where0means All.Must be a non-empty list of non-negative integers. The page size actually in use is added to the list when it is not already there, so the control can always show the size the table is on.
- part_prefix
Type:
string(default:"→ "— a right arrow and a non-breaking space)The text put in front of a need part’s ID and content in a
needtablethat sets:show_parts:.It marks a part’s row as belonging to the need above it. The default’s space is non-breaking on purpose, so the marker cannot wrap away from what it labels. Set
""for no prefix at all. It applies in the editor’s rendered preview and inubc build htmlalike, and nothing but a:show_parts:table reads it.This is the ubCode spelling of Sphinx-Needs’
needs_part_prefix, with the same default.
- report_template
Type:
string(default:"")A Jinja template file that every
.. needreport::in the project renders from, instead of ubCode’s built-in report. A relative path is taken from the directory holdingubproject.toml; an absolute one is used as written. Unset — or set to""— renders the built-in report.A directive’s own
:template:option wins over this key, and resolves differently: relative to the document that writes it, with a leading/meaning relative to the project source root — which it may not leave. A:template:that resolves outside the source root is refused (needs.report_template_outside_project) and the built-in report renders instead; this key is the supported way to use a template that lives outside it, because you write it in the project’s own configuration file rather than in a document. (With noubproject.tomlat all there is no configured source root, and the boundary is the host document’s own directory — the diagnostic says which of the two it used.)[needs] report_template = "templates/report.need"
The template must produce reStructuredText, and is given the same five names a Sphinx-Needs build supplies:
Name
Value
typesone entry per configured need type, in declaration order, with the keys
directive,title,prefix,colorandstylelinksone entry per configured link type, with the keys
option,incoming,outgoing,copyandallow_dead_linksoptionsthe project’s extra field names
usageneeds_amountandneeds_types(a count per configured type)report_directivethe string
dropdownA section the directive did not ask for arrives as the empty value rather than as a missing name, so a template can always reference all five and guard with
{% if types|length != 0 %}.{% if types|length != 0 %} .. dropdown:: Need Types {% for type in types %} * {{ type.title }} (``{{ type.directive }}``) {%- endfor %} {% endif %} Requirements: :need_count:`type == "req"`.
Two practical notes:
Give the file an extension your project does not build —
.needor.txt, not.rstor.md— or it is also indexed as a document of its own. (The Sphinx-Needs documentation gives the same advice.)Python method calls work in every ubCode Jinja surface, this one included:
link.get('copy', None),d.items()ands.upper()all resolve, so a template written for a Sphinx-Needs build runs unchanged.
If the template cannot be used, the directive renders the built-in report and says why —
needs.report_template_not_found,needs.report_template_outside_projectorneeds.report_template_failed.- flow_direction
Type:
string(default:"down")The default layout direction for
needflowdiagrams — where the graph grows.One of
down,up,rightorleft, or the equivalent engine tokensTB/TD,BT,LRandRL. Aneedflowthat sets its own:direction:uses that instead. An empty string is the same as unset.- flow_show_links
Type:
stringorboolean(default:"none")The default edge labelling for
needflowdiagrams.outgoingandincominglabel each edge with the link type’s corresponding display name,typelabels it with the link type’s own name, andnoneleaves edges unlabelled.truemeansoutgoingandfalsemeansnone, so a value carried over from Sphinx-Needs’needs_flow_show_linksmeans the same thing here. Aneedflowthat sets its own:show_link_names:uses that instead, including:show_link_names: noneto leave one diagram unlabelled in a project that labels edges by default. An empty string is the same as unset.- flow_show_legend
Type:
string(default:"")Which named legend a
needflowgets when its:show_legend:names none.The value is a key of
flow_legendsbelow. This selects which legend a diagram gets, never whether it gets one: only a:show_legend:on the directive itself draws a legend, so setting this cannot add a legend to a diagram that did not ask for one. Aneedflowwhose:show_legend:names a key uses that key instead. An empty string is the same as unset, which leaves the choice to the diagram engine — for the Mermaid flowcharts ubCode draws, a table beside the diagram listing the drawn need types. A value naming no configured legend is reported and treated as unset.- flow_legends
Type: table of tables (default: none)
The project’s named legend configurations, each selected by name from a
needflow’s:show_legend:or byflow_show_legendabove.[needs.flow_legends.wide] parts = ["types", "links"] placement = "external"
partsThe sections the legend lists, as a list, in the order they are drawn:
types(each drawn need type with its colour) andlinks(each drawn link type with its display name). Unset is the same as["types"].Order is significant.
parts = ["links", "types"]draws the link table first. That is why this key is a list and not a single name: a name per combination could not express order without inventing one per permutation.parts = ["types"] # just the need types parts = ["types", "links"] # both tables, types first parts = ["links", "types"] # both tables, links first
A list is the only accepted spelling. A bare string is reported, with the list form to write instead.
placementWhere the legend is drawn:
internal(inside the diagram itself) orexternal(a table beside it). Unset takes the diagram engine’s default placement, which for the Mermaid flowcharts ubCode draws isexternal.internalis a preference, not a guarantee. An engine that cannot draw a legend inside the diagram draws the external one instead, with the same rows. Mermaid is such an engine — its flowchart syntax has no legend construct — so ubCode always draws the external table and says nothing about it. The key is still worth setting for a project whoseubproject.tomlis shared with a Sphinx-Needs build, whereinternalis honoured and is that engine’s default.
Every mistake in a legend costs only what it names, and the legend still renders: an unknown section is reported and skipped, a repeated one is reported and kept once, and an empty or non-list
partsis reported and takes the default. A legend name that could never be selected — one that is empty or has surrounding whitespace — is reported and the entry is ignored.
- diagram_template
Type:
string(default:"")The per-node Jinja template that a
.. needuml::or.. needarch::body’sflow()renders each need with, and that each node of a.. needflow::drawn by a PlantUML executable is drawn from ([build.plantuml] needflow). Unset — or set to""— uses ubCode’s built-in template, which is byte-identical to the Sphinx-Needs default.[needs] diagram_template = """{{ need.type_name }} {{ need.id }}"""
The template produces PlantUML, is given the need as
need, and sees every name [needs.render_context] adds.This is the ubCode spelling of Sphinx-Needs’
needs_diagram_template, with the same default. For aneedumlubCode transpiles the produced diagram to Mermaid rather than rendering it with PlantUML, so a template that emits a PlantUML construct Mermaid has no equivalent for is reported (needs.uml_unconvertible) rather than drawn — or, when only part of the diagram is affected,needs.uml_partially_converted, which names the lines that are missing. Aneedflowdrawn by a PlantUML executable has no such limit — the executable draws whatever the template produced, with one narrowing: a%or an&in the produced label is escaped, so a template that deliberately writes%version()or%date("yyyy")draws that text literally here where a Sphinx build lets PlantUML’s preprocessor expand it. (The label is a need’s data; a [needs.flow_configs] block is not, and its content is spliced verbatim — that is the one place a PlantUML built-in is honoured.) Aneedflowdrawn as a Mermaid flowchart does not read this key at all: it draws the same three parts (the type title, the wrapped title in bold, the id) in Mermaid’s own label syntax.
- [needs.render_context]
Type:
tableofstring→ value (default: empty)Extra names every template ubCode executes can read — a
needumlorneedarchbody, diagram_template, and theneedreportreport_template.[needs.render_context] product = "ubCode" release = "2026.1"
Values may be strings, numbers, booleans, arrays of those, or nested tables. A TOML date-time is reported as a
configdiagnostic atneeds.render_context— quote it as a string (release = "2026-01-01"); one inside an array is a configuration error, like an array of tables or of arrays.Names here do not reach two template surfaces, deliberately: [needs.string_links] templates and
needs_role_need_template. Both see their pattern’s named capture groups and nothing else, which is also what keeps a project-wide name from silently shadowing a capture group calledvalue.This is the ubCode spelling of Sphinx-Needs’
needs_render_context.
- flow_engine
Type:
string(default:"")Added in version 0.36.0.
Which engine draws the project’s
needflowdiagrams.[needs] flow_engine = "plantuml"
plantumlasks for them to be drawn by the PlantUML executable [build.plantuml] names, andgraphvizasks for them not to be — ubCode has no Graphviz pathway, sographvizmeans “not PlantUML” rather than naming a second engine. Any other value is reported once for the project (config, atneeds.flow_engine) and ignored.This is only the default:
[build.plantuml] needflow— or a builder’s own[build.html.plantuml]/[build.pdf.plantuml]— wins over it, and a diagram’s own:engine:wins over both. Drawing with PlantUML also needs[build.plantuml] enabled, since that is what names the executable. Unset (or"") leaves the choice to[build.plantuml] needflow, which is off by default.This is the ubCode spelling of Sphinx-Needs’
needs_flow_engine, and it is read here so that oneubproject.tomlshared by both tools need not say the same thing twice. Where the two halves of that shared file disagree — this key sayinggraphvizwhile[build.plantuml] needflowistrue, or the reverse — ubCode follows the build key and a Sphinx build of the same file follows this one, so the same project draws its diagrams with different engines in the two tools. Neither tool reports it: each is doing what its own precedence rules say. If you want one answer from both, set only this key and leave[build.plantuml] needflowunset.
- [needs.flow_configs]
Type:
tableofstring→string(default: empty)Named blocks of PlantUML configuration that a
.. needuml::,.. needarch::or.. needflow::pulls in with its:config:option. Each value is a block of PlantUML lines; the option names one or more blocks, comma-separated, and their lines are prepended to the produced diagram.[needs.flow_configs] monochrome = """ skinparam monochrome true skinparam shadowing false"""
.. needuml:: :config: monochrome {{ flow('R_LOGIN') }}
A
:config:naming a block this table does not define is reported (needs.uml_config_unknown) and contributes nothing; a Sphinx build is silent about it, which leaves a diagram that renders perfectly and merely looks wrong. The twoneedflowdirection names below are the exception: they are read as a direction rather than looked up here, so they are never reported.Because ubCode transpiles the produced diagram to Mermaid rather than rendering it with PlantUML, a block’s lines reach the diagram but only the ones Mermaid has an equivalent for change what is drawn —
skinparamin particular is dropped. Adding:debug:to the directive shows the produced PlantUML with the block’s lines in place, which is the way to check that a name resolved.A
needflowinserts a named block’s lines only when it is drawn by a PlantUML executable ([build.plantuml] needflow), and then exactly as they are inserted for aneeduml. Its:config:is read on both arms for the layout DIRECTION, though: a block containingleft to right directionortop to bottom directionsets that direction as the low-precedence source for the diagram’s:direction:, which is how Sphinx-Needs reads it, and a block setting both resolves to top-to-bottom as it does there. Two names also work with no entry behind them:lefttorightandtoptobottomare read as the direction itself and are never reported as unknown — Sphinx-Needs ships them as entries of this table and ubCode ships none, so the two spellings are built in instead. A block defined under either name is read like any other block: its content decides the direction, and the name does not override it. Because a block’s content is spliced verbatim, it is also the one place a PlantUML built-in such as%version()is honoured — a%in a need’s title or in adiagram_template’s output is escaped and drawn literally, but configuration you wrote is passed through as you wrote it, exactly as Sphinx-Needs passes it.This is the ubCode spelling of Sphinx-Needs’
needs_flow_configs.
Need types¶
The types array defines the available need directives in your project:
[[needs.types]]
directive = "requirement" # The RST directive name
title = "Requirement" # Human-readable title
prefix = "REQ_" # Prefix for auto-generated IDs
color = "#BFD8D2" # Color for diagrams and the need-card accent
style = "node" # Style for diagrams
Each type supports:
directive(required): The reStructuredText directive nametitle(required): Human-readable title for the need typeprefix(required): Prefix used for auto-generated need IDscolor(optional, default:"#000000"): Color used in need diagrams and as the accent on this type’s need cards, where it outlines the type badge and paints the card’s leading edge. An explicit*_borderstyle frames all four edges and takes that one; on cards that show a type badge, the badge outline still carries the type colour (thefocuslayout family shows no badge, and a hidden card’s badge outline is zero-width). The card accent applies only to a colour you have chosen — see When a type colour accents a card below. Cards use only#RGB,#RRGGBBand#RRGGBBAAvalues; any other value is ignored for the card. Diagrams are unaffected and always use the color.style(optional, default:"node"): Style used in need diagrams
When a type colour accents a card¶
A type’s color accents its need cards only where you have chosen it.
Two cases are deliberately not treated as a choice:
a type declared without a
color— the#000000default means no accent rather than black. So do#000,#000000FFand any fully transparent value (an00alpha, e.g.#00000000), since an invisible accent is not one. A partly transparent colour such as#00000080is a choice, and paints. For a near-black accent write#010101.a built-in type still carrying its shipped colour. Every project gets the four built-ins whether or not it asks, and their colours were picked to fill diagram nodes, so accenting them would colour every card of every project:
Directive
Shipped
colorAccents a card?
req#BFD8D2no — this exact pair is the shipped default
spec#FEDCD2no
impl#DF744Ano
test#DCB239no
The rule matches the pair, so these colours accent normally on any other directive, and any other colour accents normally on these four.
Note
Migrating from sphinx-needs? Those four values are sphinx-needs’ own
defaults, so a [[needs.types]] block copied across lands on them exactly
— and your cards will show no accent. If a colour of yours is being
ignored, change it by one digit (#BFD8D2 → #BFD8D3) and it will
paint. Spelling does not help: the comparison ignores case and a fully
opaque alpha, so #bfd8d2 and #BFD8D2FF on req are the same pair.
Link types¶
Links connect needs to one another and express relationships like implements, tests, or depends on. ubCode supports two configuration formats: the dict-based format (sphinx-needs ≥ 6.3, recommended) and the list-based format (sphinx-needs < 6.3).
Note
Three names cannot be shadowed by a link type:
id and type are refused outright,
and content — the need’s own body — is reported as
config.field_shadowed_by_identity.
A content link is still created and its targets still validated,
but every filter, table, variant condition, report template
and needs.json key answers the body for that name,
so the link is invisible to every one of them.
See Variants for the shadowing rule in full.
Dict-based format (recommended)¶
Note
The dict-based [needs.links] format requires sphinx-needs ≥ 6.3.
It became the recommended format in sphinx-needs 7.0,
where the older list-based needs_extra_links was formally deprecated.
Use [needs.links.<name>] to define each link type as a named TOML table:
[needs.links.implements]
outgoing = "implements"
incoming = "implemented by"
copy = false
color = "#0000FF"
allow_dead_links = false
[needs.links.tests]
outgoing = "tests"
incoming = "tested by"
copy = true
Each link supports:
outgoing(optional): Description for outgoing linksincoming(optional): Description for incoming linkscopy(optional, default:false): Whether to copy to common linksallow_dead_links(optional, default:false): Allow links to non-existent needs: such a link is not reported asneeds.dead_link. It still sets the need’shas_dead_links, never itshas_forbidden_dead_links.color(optional, default:"#000000"): Color for diagramsstyle(optional, default:""): Line style for diagramsstyle_part(optional, default:"dotted"): Style for partial linksstyle_start(optional, default:"-"): Start marker for diagramsstyle_end(optional, default:"->"): End marker for diagramsparse_variants(optional): Whether to parse variant functions (<<...>>) and variant data references (<{ ... }>, see Variant data) in this link’s values. sphinx-needs ≥ 7.0.parse_dynamic_functions(optional): Whether to parse dynamic functions in this link’s values. sphinx-needs ≥ 6.3.parse_conditions(optional, default:true): Whether bracket syntax in link values is interpreted as a condition expression. sphinx-needs ≥ 8.0.description(optional): Human-readable description of the link type. sphinx-needs ≥ 6.3.schema(optional): JSON Schema validation for link valuesdefault(optional): The targets of every need that leaves this link type unset and matches none of itspredicates: a list of need IDs (default = ["REQ_1"]), or a dynamic function such asdefault = "[[copy('links')]]". sphinx-needs ≥ 7.0.predicates(optional): A list of[filter, targets]pairs: a need that leaves this link type unset takes the targets of the first filter it matches (predicates = [["status == 'open'", ["REQ_1"]]]). sphinx-needs ≥ 7.0.
A default or predicates under [needs.global_options.<name>]
replaces the same key here; a key it does not declare leaves this one.
A link type with a derive rule takes neither (see Derived fields).
The four diagram keys follow Sphinx-Needs’ defaults.
Changed in version 0.36.0: style and style_end default to "" and "->", Sphinx-Needs’ own values;
they were "solid" and ">".
style_start and style_end are raw PlantUML arrow syntax:
Sphinx-Needs’ PlantUML engine writes each edge as style_start,
then [style] when style is set,
or [style_part] always for an edge to or from a need part
(an empty style_part draws -[]->, which PlantUML rejects),
then style_end,
so the defaults draw --> for a plain edge and -[dotted]-> for a part edge.
ubCode’s own needflow ignores style_start and style_end
(see Needs flow diagram (needflow)).
List-based format (sphinx-needs < 6.3)¶
Deprecated since version 0.25.0: The list-based [[needs.extra_links]] format
(needs_extra_links in sphinx-needs)
was deprecated in sphinx-needs 6.3 in favour of the dict-based format,
and formally migrated in sphinx-needs 7.0.
It remains supported for backward compatibility.
For new projects, prefer the dict-based [needs.links] format.
[[needs.extra_links]]
option = "implements" # The option name used in need directives
incoming = "implemented by" # Description for incoming links
outgoing = "implements" # Description for outgoing links
copy = false # Whether to copy to common links
color = "#0000FF" # Color for diagrams
style = "" # Line style for diagrams
style_part = "dotted" # Style for partial links
style_start = "-" # Start marker for diagrams
style_end = "->" # End marker for diagrams
allow_dead_links = false # Allow links to non-existent needs
An entry takes default and predicates as a [needs.links.<name>] table does.
sphinx-needs ≥ 7.0.
Fields¶
Every need carries fields — named pieces of metadata
such as status, tags, or any custom attribute you define.
Some fields are built in (id, title, status, tags);
you can add your own through the configuration.
ubCode supports two formats:
the dict-based [needs.fields] format (aligned with sphinx-needs ≥ 7)
and the list-based extra_options format (for sphinx-needs < 7).
Note
Three names cannot be shadowed by a custom field:
id and type are refused outright,
and content — the need’s own body — is reported as
config.field_shadowed_by_identity.
A content field is still created and can still be set on a directive,
but every filter, table, variant condition, report template
and needs.json key answers the body for that name,
so the declared value is invisible to every one of them.
See Variants for the shadowing rule in full.
Dict-based format (recommended)¶
Note
The dict-based [needs.fields] format requires sphinx-needs ≥ 7.0.
It replaces the older list-based needs_extra_options
and also allows specializing core fields (e.g. status, tags)
by narrowing their type constraints.
Use [needs.fields.<name>] to define each custom field:
[needs.fields.priority]
description = "Priority level"
default = "medium"
[needs.fields.assignee]
description = "Assigned team member"
[needs.fields.effort]
description = "Story points"
schema = {type = "integer", minimum = 1, maximum = 13}
Each field supports:
description(optional): Human-readable descriptiondefault(optional): Default value if not specified on a needschema(optional): JSON Schema to validate values (see Field schemas below). Schema support for fields was introduced in sphinx-needs 6.0 (via ``needs_extra_options``) and carried forward to ``needs_fields`` in sphinx-needs 7.0.nullable(optional): Whether the field accepts null values. sphinx-needs ≥ 7.0.predicates(optional): Conditional defaults (see Global options). sphinx-needs ≥ 7.0.parse_variants(optional): Whether to parse variant functions (<<...>>) and variant data references (<{ ... }>, see Variant data) in this field’s values. sphinx-needs ≥ 7.0.parse_dynamic_functions(optional): Whether to parse dynamic functions in this field’s values. sphinx-needs ≥ 6.3.
Once declared, these fields can be used on any need:
.. req:: Encrypt data at rest
:id: REQ_002
:status: open
:priority: high
:assignee: alice
:effort: 5
Styling cards with [needs.fields.style]¶
style is an ordinary core field, so the same default and predicates
keys give a project-wide card style and per-condition ones —
this is ubCode’s replacement for sphinx-needs’ needs_default_style,
and no separate configuration key is needed:
[needs.fields.style]
# Every card starts discreet …
default = "discreet"
# … except that closed needs go green and blocked ones get a red frame.
predicates = [
['status == "closed"', 'green'],
['status == "blocked"', 'red_border'],
]
A :style: written on the need itself wins over both.
The value is a comma-separated list of tokens;
each becomes a ub-need-style-<token> class on the card,
in the order written and with duplicates collapsed.
ubCode ships styling for the twenty sphinx-needs palette names —
the green / red / yellow / blue / gray / discreet fills
(with implemented / open / in_progress as aliases),
the green / red / yellow / blue / discreet _border frames,
the green / red / orange / yellow / blue _bar stripes,
and clean.
A token that is not a valid CSS identifier is ignored and reported as
build.need_style_invalid;
see Differences from a Sphinx build for how this differs from a Sphinx-Needs build.
Choosing a card design with [needs.fields.layout]¶
layout is an ordinary core field too, so the same two keys give a
project-wide card design and per-condition ones —
which is more than Sphinx-Needs can express,
where the card design is one global value:
[needs.fields.layout]
# Every card is the default design …
default = "clean"
# … except that specifications show everything, and needs marked
# `:hide_meta:` show only their content.
predicates = [
['type == "spec"', 'complete'],
['hide_meta == True', 'focus'],
]
Note
Earlier versions of this page said that this made a separate configuration
key unnecessary. That was wrong in the one case that matters.
A ubproject.toml is read by both engines,
and Sphinx-Needs’ needs_from_toml already maps [needs] default_layout
onto needs_default_layout while silently skipping [needs.fields]
entirely — so a project that wrote its card design the field way got it from
ubc build html and not from sphinx-build, with no warning from either.
ubCode therefore has [needs] default_layout
as well, and it is the key to reach for when one file feeds both engines.
The two mechanisms are not competing tiers of one chain, and the precedence follows from what each one does:
a
:layout:written on the need itself wins over everything;then a
[needs.fields.layout]predicatesrow, then itsdefault— both write a value onto the need, which is why they beat the key below;then [needs] default_layout, which catches only the needs that end up with no value at all;
then
clean.
Use default_layout for the project-wide design — it is the spelling both
engines read — and [needs.fields.layout] predicates for the
per-condition exceptions, which have no Sphinx-Needs equivalent.
The value is any design this project knows: one of the ten built-in ones —
clean (the default), complete, focus, debug, and the six
side-column names clean_l, clean_lp, clean_r, clean_rp,
focus_l and focus_r — one of the two sphinx-needs identities
(test, focus_f), or a design the project defines for itself with
[needs.card_layouts].
An unknown name is reported as
build.need_layout_unknown
and the card renders with the default design;
see Card designs with [needs.card_layouts] for what each design shows and how to define your own,
and Differences from a Sphinx build for how this differs from a Sphinx-Needs
build.
Hiding cards conditionally with [needs.fields.hide]¶
hide is an ordinary core field as well, and a boolean rather than a string —
so default and predicates take true / false, and give you
conditional hiding: hide every draft need, or every need of a type, without
writing :hide: on any of them.
This is parity rather than divergence — a Sphinx-Needs build reaches the same
result through needs_fields’ default / predicates on the hide
core field, which upstream flags allow_default for exactly this purpose:
[needs.fields.hide]
# Nothing is hidden by default …
default = false
# … except needs still being drafted, and the ones an internal
# process owns rather than the document.
predicates = [
['status == "draft"', true],
['type == "internal"', true],
]
A :hide: written on the need itself wins over both.
A hidden need renders its whole card inside a closed disclosure — one
compact summary line the reader can expand in place — so nothing is withheld,
and references, link rows, needlist entries and the built site’s search
index all still reach it;
see Differences from a Sphinx build for the full behaviour and for how this
differs from a Sphinx-Needs build.
Hiding a need does not remove it from the project, nor its content from the
page: it stays in the index, in needs.json, in every filter result and in
the published HTML, merely collapsed. :hide: is not a confidentiality
boundary; use :delete: for a need that should produce no output at all.
List-based format (sphinx-needs < 7)¶
Deprecated since version 0.25.0: The list-based extra_options format
(needs_extra_options in sphinx-needs)
was deprecated in sphinx-needs 7.0 in favour of the dict-based format.
It remains supported for backward compatibility.
For new projects, prefer the dict-based [needs.fields] format.
Simple format (string list):
extra_options = ["priority", "effort", "assignee", "component"]
Detailed format (with descriptions and schemas):
extra_options = [
{name = "priority", description = "Priority level"},
{name = "effort", description = "Story points", schema = {type = "integer", minimum = 1, maximum = 13}},
{name = "assignee", description = "Assigned team member"},
]
Field schemas¶
The schema key on a field applies
JSON Schema validation
to every value entered for that field.
Invalid data is caught at index time, before the docs are built.
String schemas:
[needs.fields.contact]
schema = {type = "string", format = "email"}
[needs.fields.id_pattern]
schema = {type = "string", pattern = "^[A-Z]{3}-[0-9]{4}$"}
[needs.fields.severity]
schema = {type = "string", enum = ["low", "medium", "high", "critical"]}
String format options:
The format property supports various predefined formats:
"date": RFC 3339 date format (e.g.,2023-10-22)"date-time": RFC 3339 date-time format (e.g.,2023-10-22T14:30:00Z)"time": RFC 3339 time format (e.g.,14:30:00Z)"email": RFC 5322 email address (e.g.,user@example.com)"hostname": RFC 1123 hostname (e.g.,example.com)"uri": RFC 3986 URI (e.g.,https://example.com/path)"uuid": RFC 4122 UUID (e.g.,550e8400-e29b-41d4-a716-446655440000)"regex": ECMAScript regular expression
Integer and number schemas:
[needs.fields.priority]
schema = {type = "integer", minimum = 1, maximum = 10}
[needs.fields.rating]
schema = {type = "number", minimum = 0.0, maximum = 5.0}
[needs.fields.fibonacci]
schema = {type = "integer", enum = [1, 2, 3, 5, 8, 13, 21]}
Boolean schemas:
[needs.fields.approved]
schema = {type = "boolean"}
Array schemas:
[needs.fields.reviewers]
schema = {type = "array", items = {type = "string", format = "email"}, minItems = 1, maxItems = 3}
[needs.fields.categories]
schema = {type = "array", items = {type = "string"}, uniqueItems = true}
Usage in need directives:
Once defined, these fields can be used in your need directives:
.. requirement:: User Authentication
:priority: 5
:effort: 8
:assignee: developer@company.com
:due_date: 2024-12-31
:tags: security, authentication
:component: auth, backend
:active: true
Users must be able to authenticate using their corporate credentials.
Derived fields¶
A derived field is a field, or a link type, whose value a rule computes:
the rule is written once in ubproject.toml,
applies to every need (or, declared per need type, to the need types it names: see Rules per need type),
and no author can set the field.
Declare it with a derive table on its [needs.fields.<name>] or [needs.links.<name>] entry
(see Fields and Link types),
or on a table entry of the older extra_options list or a [[needs.extra_links]] entry:
a kind, which says what the rule computes,
and that kind’s roles, each under its fixed name.
The example declares the fields and link types its rules read first:
[needs.fields.hours]
schema = { type = "number" }
[needs.fields.asil_own]
schema = { type = "string", enum = ["QM", "A", "B", "C", "D"] }
[needs.links.parent]
outgoing = "parent"
[needs.links.tests]
outgoing = "tests"
[needs.links.satisfies]
outgoing = "satisfies"
[needs.fields.owner]
schema = { type = "string" }
derive = { kind = "copy", field = "owner", over = "parent" } # a lookup, one hop out
[needs.fields.total_hours]
schema = { type = "number" }
derive = { kind = "sum", field = "hours", over = "links" } # a roll-up over outgoing links
[needs.fields.verified]
schema = { type = "boolean" }
derive = { kind = "all", test = "status == 'passed'", over = "tests_back", where = "type == 'test'" }
[needs.fields.asil]
schema = { type = "string", enum = ["QM", "A", "B", "C", "D"] }
derive = { kind = "max", field = "asil_own", over = "satisfies", transitive = true, include_self = true }
[needs.fields.hash]
schema = { type = "string" }
derive = { kind = "hash", fields = ["title", "content", "status", "links"] }
[needs.fields.fp]
schema = { type = "string" }
derive = { kind = "fingerprint" } # a digest of the whole need
[needs.links.related_open]
derive = { kind = "links", where = "type == 'req' and status == 'open'" }
[needs.links.mentions]
derive = { kind = "content_links" }
ubCode computes every kind as sphinx-needs 9.0.0 does.
The kinds¶
|
declared on |
required roles |
optional roles |
the value |
|---|---|---|---|---|
|
a field |
|
one of |
the value of |
|
a field |
|
|
the sum of |
|
a field |
|
|
the number of candidates (with |
|
a field |
|
|
the least or greatest value of |
|
a field |
|
|
whether some or every candidate holds: |
|
a field |
|
|
the distinct values of |
|
a field |
|
— |
a SHA-256 of the listed fields of the need, after every other derived field: |
|
a field |
— |
|
a SHA-256 of the whole need (see The fingerprint), after every other derived field:
|
|
a link type |
|
|
the needs passing |
|
a link type |
— |
|
the needs the need’s content references with |
Every value kind but fingerprint also takes after = "derived",
which runs the rule in the late stage, after every other derived field of every need
(hash and fingerprint always do).
A late rule reads other needs as any value kind does — over a link type, through from —
and their late fields too:
the late rules run in the order their reads give,
so late_total = { kind = "sum", field = "total", over = "links", after = "derived" }
sums the targets’ total once it is final,
and a late copy of a hash field reads the hash.
A dynamic function or a rule without after reading a late field
— [[copy("h")]], [[calc_sum("h")]], a filter naming it,
{ kind = "collect", field = "h", over = "links" } —
cannot run before it: the call or the rule is needs.derive_scope and its field keeps its empty
value, as in sphinx-needs.
How a value is computed¶
The rules of the link kinds run first, then the value kinds, then the late stage:
a value rule reads every link list final, the backlinks (<link>_back) included.
Within a stage a rule runs after every value it reads,
and a rule on a cycle of reads is needs.derive_cycle and keeps its empty value.
The candidates.
over = "<link>"reads the need’s targets in the order they are written, a target written twice read twice, a link to a part read as its need, and a link to no need skipped;over = "<link>_back"reads the needs that link to this one, each once, in id order compared character by character (SPC_10beforeSPC_9).whereis evaluated on each candidate, thentest.Unset values. A candidate whose
fieldis unset (null) is skipped;""and[]are set.sumadds numbers only (a boolean or a text is skipped);minandmaxskip a value outside the field’senum, and keep the first of equal values;collectkeeps each distinct value once, a list’s items each.No candidate.
sumandcountgive0,anyfalse,alltrue,collect[];min,maxand acopywith nothing to copy compute no value, and the field keeps its empty value.A copy over several targets.
"first"and"unique"take the value of the id that sorts first character by character among the targets that set the field;"unique"with more than one such target also reportsneeds.derive_unique(a warning)."list"takes every value, in candidate order, duplicates kept.The type. The value takes the derived field’s type: a
countinto anumberfield is2.0, asumof integers into anintegerfield is7. A computed value the field’sschemarefuses is reported asneeds.dynamic_function, and the field keeps its empty value.Transitive.
minormaxover every need reachable throughover; a need on a cycle of links reaches itself, so every need of a cycle holds the same value.wherefilters the values read, not the needs reached. A rule over its OWN field (the multi-level roll-up of Rules per need type) reads the value each reached need computes: a cycle of links among those needs is then a cycle of reads (needs.derive_cycleon each), and so isinclude_self = trueon every need, which reads its own value.The hash. The SHA-256 of the listed fields as one compact JSON array: a link list as its target ids, conditions dropped, each once, in natural order; a number as Python writes it (
1e+16,1.5e-05). ubCode and sphinx-needs write the same hash for the same need.The fingerprint. See The fingerprint.
The roles¶
fieldThe field read on each candidate: a field, a link type,
<link>_back, or a context field such asid,type,docnameorlineno. Oncopywith neitherfromnorover, the need’s own field.fieldsThe need’s own fields hashed by
hash, in this order: fields and link types, never<link>_back.excludefingerprintonly: names to leave out of the fingerprint, each one it would include, listed once, such asexclude = ["id"]. An empty list leaves nothing out.overA declared link type: its targets are the candidates, in the order they are written, a target written twice counted twice.
<link>_backreads the needs that link to this one instead, in id order compared character by character (SPC_10beforeSPC_9).fromThe id of one need, read instead of this one.
copyreads an external need’s fields as its source states them;content_linkscannot read an external need, which has no content: naming one fails the rule, which is reported.whereA filter narrowing the candidates, evaluated on each candidate. On
links, the filter over every need; it must not be empty. Oncontent_links, the filter each referenced need must pass (a part reference through its need). A candidate the filter cannot be evaluated on — one lacking a field it compares, say — does not pass, and nothing is reported.testA filter asserted of each candidate, instead of a boolean
field(anyandall).selectWhich value a
copyover several targets takes:"first"(the default) takes the target that sets the field whose id sorts first character by character,"unique"takes the same and reports when several do,"list"takes every value, into an array field.transitiveminandmaxonly: every need reachable throughover, not only its direct targets (a need on a cycle reaches itself).include_selfminandmaxwithtransitive: count the need’s ownfieldtoo.links: let a need be linked to itself when it passes the filter. Defaultfalse.include_partslinksonly: need parts are candidates too, a passing part linked as<need>.<part>. Defaultfalse. Towhere, a part is its need with a few names of its own:idis the part id,id_completeis<need>.<part>,id_parentis the need’s id,is_partis true (is_needfalse), andcontentis the part’s own text; it links to nothing (every link type is empty) and has noparent_need. Every other name —type,status,title, your own fields — is its need’s. A need’s parts come right after it, in part-id order, and the need carrying the rule leaves out its own parts with itself, unlessinclude_self. A broadwherewithinclude_partson every need grows every stored list with every part that passes, so keep the filter narrow on a large project.after"derived": run in the late stage, after every other derived field of every need.
where and test are written in the filter language
sphinx-needs and ubCode both evaluate,
on the candidate alone:
they may not be blank, name needs or current_need,
or read a c. attribute such as c.this_doc(),
since a rule applies to every need wherever it is written.
The join role, for a correlated lookup
(“every need whose case_name is one of my test_case values”),
and the expr kind, for an expression over the need’s fields,
are reserved and not available yet.
A rule that uses either is refused.
types is not a role:
a rule for some need types is declared under derive_by_type (Rules per need type).
Rules per need type¶
A derive_by_type table beside derive (or instead of it) gives a rule per need type:
its keys are need types, by the directive name type holds (task, not Task),
and each value is a rule as derive takes.
A need uses its type’s rule, else derive, else none.
It is read wherever derive is:
on [needs.fields.<name>] and [needs.links.<name>],
and on the older extra_options and [[needs.extra_links]] entries.
The pattern it is made for is a value written on the leaves and rolled up through every level above.
Effort is authored on tasks;
total_effort copies it on tasks and sums it over the links on stories and epics,
through any depth
(the three types are declared, since every key must name a need type of the project):
[[needs.types]]
directive = "task"
title = "Task"
prefix = "T_"
[[needs.types]]
directive = "story"
title = "Story"
prefix = "S_"
[[needs.types]]
directive = "epic"
title = "Epic"
prefix = "E_"
[needs.fields.effort]
schema = { type = "number" }
[needs.fields.total_effort]
schema = { type = "number" }
nullable = false
derive_by_type.task = { kind = "copy", field = "effort" }
derive_by_type.story = { kind = "sum", field = "total_effort", over = "links" }
derive_by_type.epic = { kind = "sum", field = "total_effort", over = "links" }
.. epic:: Checkout
:id: EPIC_1
:links: STORY_1, TASK_3
.. story:: Pay by card
:id: STORY_1
:links: TASK_1, TASK_2
.. task:: Card form
:id: TASK_1
:effort: 3
.. task:: Card checks
:id: TASK_2
:effort: 5
.. task:: Receipt
:id: TASK_3
:effort: 2
The tasks hold 3.0, 5.0 and 2.0, STORY_1 holds 8.0 and EPIC_1 10.0.
The field is two fields, effort and total_effort, because a derived field is closed on every need type:
the tasks’ own value is written in effort, and total_effort is computed everywhere.
A need of a type without a rule holds the field’s off-type value, for good:
the field declares |
a need of a type without a rule holds |
|---|---|
a literal |
the default, in the field’s type ( |
no default, and is nullable |
|
no default, and is not nullable |
its empty value ( |
a link type with a literal |
those links ( |
a link type without one |
no links |
An imported or external need lacking the field holds the off-type value of its type when its type has no rule,
and the empty value (null when nullable) otherwise; a value it states is kept.
In needs.json, the field’s needs_schema entry is "readOnly": true,
and its default is the off-type default (a link type’s as its sorted link list).
What may sit beside it.
A literal default beside derive_by_type alone is the off-type value.
Beside derive and derive_by_type together a default is refused, as beside derive:
every type has a rule.
predicates, and a default that is a [[…]] or a variant, beside derive_by_type are refused
(config.derive_invalid) and ignored: a value that differs per need is what a rule is for.
[needs.global_options] refuses a default for the field, as for any derived field.
When it cannot be read, each entry is reported on its own, at its key
(needs.fields.<name>.derive_by_type.<type>):
a key naming no need type: the entry is ignored; a key that is a type’s title gets a hint (
Requirementis not a need type, did you meanreq?);a
derive_by_typethat is not a table, or an empty one, atneeds.fields.<name>.derive_by_type: it adds no rule, and a readablederivebeside it still applies on every need type; the field stays derived;an entry that cannot be read, as
derive: the needs of that type hold the empty value (not the default, which belongs to the types without a rule); every other type is unaffected;either key on a core field: refused, the field left as it is.
Within a field the findings come in the order derive, derive_by_type, its entries by need type,
then a key beside derive_by_type.
Rules that read each other’s own values are looked for per need type:
a type’s entry on such a cycle cannot be read there,
and a derive closing one only through some types’ entries cannot be read on those types, and applies on the others
(its finding names them).
Stages are per need.
A type’s rule may run late (after = "derived") while another type’s rule of the same field does not:
a reader of the field reads each need’s own value,
and only a need whose rule runs later than the reader is needs.derive_scope.
Who may set a derived field¶
Nobody but the rule, on every need type, also the types without a rule (Rules per need type).
A directive option naming a derived field or link type,
a dynamic function, variant or variant-data reference written in one,
and a needextend option naming one
are each reported as needs.derive_authored (a warning),
and ignored.
A derived field or link type takes no default or predicates:
each is reported as config.derive_invalid and ignored,
and [needs.global_options] refuses a default for one with its own finding.
A link type declared copy = true copies its targets into links;
when links is itself derived, the copy is not performed,
and the links rule reports it as config.derive_invalid.
A derived link type cannot be copied in this release:
copy = true on one is reported as config.derive_invalid at its rule,
and its targets are not copied into links.
A needextend whose filter names a derived field is matched before any rule runs,
against the value from before: it is reported as needs.derive_scope (a warning) at the needextend,
naming the needs that compute the field (a need whose type has no rule holds its off-type value already, and is not named).
Write the filter over the fields the rule reads instead.
When its rule computes no value — fails, cannot be read, or finds nothing to compute from —
a derived field holds its empty value, its placeholder,
which the field’s own schema does not judge:
a non-nullable enum field holding "", or a derived link type with minItems holding [],
is not reported.
A value a rule computes is validated like any other,
0, "" and [] included.
A rule does not run on an imported or external need
(needimport, [[needs.external_needs]]):
the field keeps the value its source states.
In needs.json, the needs_schema block marks every derived field and link type
"readOnly": true,
so a project importing it takes the value as data.
When a rule cannot be read¶
A rule that cannot be read is reported once,
as config.derive_invalid at the rule’s key
(needs.fields.<name>.derive, needs.fields.<name>.derive_by_type.<type>,
or needs.extra_options[<index>].derive on the older list),
naming the kind and the offending role:
a derive that is not a table, a missing or unknown kind,
an unknown role, a role the kind does not take or a required one it lacks,
a role whose value has the wrong type, or a blank field, over or from, or an empty fields,
an exclude that is not a list of names, names one twice, or names nothing a need has
or a name a fingerprint never includes (such as docname),
a value kind on a link type or a link kind on a field,
a value the field’s schema cannot hold,
sum over a field that is not a number, min or max over a field with no order,
a where or test that is blank, does not parse or is not portable,
an over naming no declared link type,
select = "list" on a field that is not an array,
and two rules that read each other’s values on the same need
(copy of an own field, hash).
The field stays derived, closed to authors, and holds its empty value;
the configuration still loads,
so a project written for a newer kind or role still builds with an older ubCode.
A derive on a core field (id, type, status, tags, title, docname, …,
and parent_needs, as a link type or a field)
is refused the same way, and ignored: the field stays an ordinary field.
The fingerprint¶
A fingerprint is the lowercase hex SHA-256 of the UTF-8 text of one JSON object holding the whole need,
its keys sorted, its values written as for hash
(no whitespace, non-ASCII characters not escaped, numbers as Python writes them,
and a number field’s value as a float).
ubCode and sphinx-needs write the same fingerprint for the same need.
What it includes.
id,type,title,status,tagsandcontent(asneeds.jsonholds it: a need nested in another’s body is part of the parent’scontent, so editing or moving the nested need changes its parent’s fingerprint too), every field the project declares under[needs.fields]or[needs] extra_options, and every link type,linksamong them; none of them derived.What it never includes. A derived field or link type (one with
deriveorderive_by_type, on any need type; so nohashand no other fingerprint), a back link list (<link type>_back),parent_needs,parts(a part’s text is incontent),constraints, where the need is (docname,lineno,sections), how it is shown (layout,style,collapse,hide), what is recorded about it (is_external,has_dead_linksand the like), and a field ubCode adds for you, such as those of[test_reports]or a[codelinks]URL field, unless the project declares that name itself.When a name enters. Only where the need’s value differs from the field’s default, compared as JSON text: the field’s
default, elsenullfor a nullable field (a default that is a[[…]]or a variant is no literal default, and so counts as none), and for a field that is neither, nothing: it always enters.statusandtagsare compared against theirs (nulland[]unless configured),contentagainst"", andid,typeandtitlealways enter. A link type, andtags, without a literal default are compared against[]. Anumberfield’s value and default are compared, and written, as floats:default = 1is1.0. So a value written equal to its default is the same as leaving it unset, while"",false,0or[]differ from anulldefault, and-0.0from0.0.Declaring a field. A new field or link type moves no fingerprint while the needs leave it unset, if its default is a literal or there is none. A default that is a
[[…]]or a variant, andpredicates, give the needs a value, which enters as a written one would: addingownerwithdefault = "[[copy('title')]]"moves the fingerprint of every need, and addingseveritywithpredicates = [["type == 'spec'", "major"]]that of everyspec. Changing a field’sdefaultmoves the fingerprints of the needs that wrote the old or the new default out, and not of those that rely on it, although their value changes.Links. A link list is written as for
hash, and compared with its default the same way: the ids it links to, without their conditions, sorted and without duplicates. So a condition pinning another need’s fingerprint never moves the fingerprint of the need holding it, while linking another need does.Leaving names out.
excludeleaves names out, such asexclude = ["id"], under which two needs with the same contents have the same fingerprint.When it is computed. After every other rule, from the need’s final values (after every
needextend, variant and[[…]]); it reads no derived field, so two fingerprints with the sameexcludehold the same value. It does not run on an imported or external need.Frozen. No later release changes which names a fingerprint includes, when a name enters, or how the text is written: a fingerprint written down stays current until the need, or the configuration of the fields it reads, changes.
For a need REQ_1 of type req, titled Größe, with status open and no other value set,
the text is {"id":"REQ_1","status":"open","title":"Größe","type":"req"}.
This is not the fingerprint ubc agent records with a review verdict
(Reviews and quality analysis), which also covers the needs a need is trace-linked to,
and ignores whitespace.
Detecting a changed need. A fingerprint changes whenever its need does, so a link condition naming the fingerprint a need had when it was reviewed reports the need once it changes:
.. spec:: Reviewed against REQ_1
:id: SPEC_1
:links: REQ_1[fp == "3f9a1c2b7d4e0a8c6f1e5d2b9a7c4e3f8d6b1a0c9e7f5d3b2a4c6e8f0a1b3c5d"]
While REQ_1 is unchanged, the condition holds.
Once its title, content, status or any other value it includes changes, its fingerprint does,
and the build reports needs.link_condition_failed at SPEC_1;
writing the new fingerprint into the condition accepts the change.
Writing only the start of the fingerprint is enough to tell a change apart:
REQ_1[fp.startswith('3f9a1c2b7d4e')].
Derived fields and sphinx-needs¶
sphinx-needs reads derive and derive_by_type from its 9.0.0 release, with the same kinds, roles and findings
(the configuration finding is needs.derive_invalid there).
An earlier sphinx-needs ignores the key without a word,
so a Sphinx build leaves the field authorable and unset.
ubCode cannot see which sphinx-needs a project builds with,
so a project that declares derive or derive_by_type gets one config.derive_sphinx_minimum notice,
graded info, naming the release,
at the first key of the first derived field by name (or link type, when no field has one);
ignore = ["config.derive_sphinx_minimum"] in [lint] silences it.
Several kinds have an inline spelling, a dynamic function written in one need’s option, with the same meaning:
dynamic function |
the kind it spells |
|---|---|
|
|
|
|
|
|
|
|
|
|
One difference between the inline calls and the kinds is kept, as sphinx-needs keeps it:
a need the filter of calc_sum, check_linked_values or copy cannot be evaluated on
(one lacking a field the filter compares) is still counted by calc_sum, still tested by
check_linked_values and dropped by copy, each reported once per call as needs.filter,
and it fails links_from_content;
a kind’s where, and links_from_filter, let such a need not pass, silently.
External needs¶
Load needs from external sources (JSON files or URLs):
# From local file
[[needs.external_needs]]
json_path = "external/project_needs.json"
id_prefix = "EXT"
base_url = "https://external-project.com"
target_url = "docs/{{ id }}.html"
# From remote URL
[[needs.external_needs]]
json_url = "https://api.example.com/needs.json"
id_prefix = "REMOTE"
Each external needs source supports:
json_path: Path to a local JSON filejson_url: URL to a remote JSON fileid_prefix: Prefix prepended to external need IDs, as written, and to the source’s list-valued links that name one of its own needs (a link written as a string, or to a part, keeps its target as written)base_url: Base URL for linking back to the source projecttarget_url: URL template for individual need links (supports{{ id }}placeholder)css_class: CSS class applied to external needsversion: Version string for the external needs sourceskip_in_ubcode(default:false): Whentrue, skip loading these external needs in ubCode analysis
Each need is named by the key it is filed under in the source’s needs.json.
A need whose own "id" differs from that key is still read under the key,
and reported once (needs.external), naming both.
Remote sources¶
The [needs.remote] section controls how ubCode fetches needs.json over
HTTP(S).
It applies to every remote source: [[needs.external_needs]] entries with a
json_url, and needimport directives whose argument is a URL.
Local sources — a json_path, or a file:// URL, which is read straight
from disk — are unaffected by every key here.
[needs.remote]
cache_days = 1
timeout = 10.0
offline = false
cache_days(default:1): How long, in days, an already-loaded remote source’s needs are reused before ubCode re-checks the source.0re-checks on every pass, and a negative value never re-checks. A re-check sends theETag/Last-Modifiedof the last response, so an unchanged source costs one small request and its needs are kept as they are.timeout(default:10.0): Timeout, in seconds, for each request.offline(default:false): Whentrue, never fetch remote sources. Each one keeps the needs it was last loaded with and is reported as skipped — useful in network-restricted CI, or for working offline against an index built while online.
A remote source whose fetch fails keeps the needs it loaded last time, rather
than losing them until the next success, and the failure is reported.
Within one long-running process — the language server, or a watch loop — an
unreachable host is not re-attempted for five minutes, so an editing session
against a source that is down does not pay a connection timeout on every pass.
That backoff is per-process and is not persisted, so a one-shot ubc build or
ubc check against an unreachable host waits for timeout on every run;
lower timeout, or set offline = true, if that is the situation you are
in.
Variants¶
The [needs.variants] section defines named filter expressions
that can be referenced from need fields using the <<name: value, fallback>> syntax.
This implements the Sphinx-Needs
variant functions
feature.
[needs.variants]
is_open = "status == 'open'"
is_html = "'html' in build_tags"
is_production = "environment == 'production'"
With these variants defined, need fields can use them:
.. req:: My requirement
:id: REQ_001
:status: open
:phase: <<is_open: active, inactive>>
:extra: <<is_html: web_only, general>>
You can also use inline bracketed expressions without pre-defining a variant:
.. req:: Another requirement
:id: REQ_002
:status: open
:phase: <<[status == 'open']: active, inactive>>
The last comma-separated value is always the fallback when no expression matches.
A variant cannot read the field it fills:
:status: <<[status == 'open']: active, inactive>> asks for status to decide status,
which is a cycle (needs.derive_cycle), and the field keeps its empty value.
Put the variant on another field, as above;
see When a call reads a value another call computes.
A variant expression resolves the need’s own fields and links,
the project’s filter_data keys, the build_tags of the current build,
and the context fields —
content, docname, lineno, sections, section_name,
is_external, is_directive, is_import, is_src_trace,
is_modified, modifications, is_need, is_part, id_parent, id_complete —
the same names a :filter: expression resolves:
.. req:: Section-aware requirement
:id: REQ_003
:status: <<[section_name == 'Interfaces']: interface, internal>>
A project-declared field or link of the same name wins over a context field,
except for id, type and content, which cannot be shadowed.
Inside a variant expression is_modified and modifications count
needextend matches only —
a need’s own variant resolution cannot be an input to itself.
A name may also carry the n. prefix (<<[n.status == 'open']: a, b>>),
matching what a :filter: expression accepts;
any other namespace is reported as an unknown name.
Note
Variant syntax is only parsed for fields that opt in.
In sphinx-needs ≥ 7.0, set parse_variants = true
on individual fields in [needs.fields].
For older versions, use the deprecated variant_options list
(see Deprecated needs options).
Parse and evaluation errors are surfaced as needs.variant diagnostics,
including a name the expression cannot resolve —
which is reported with a “did you mean” suggestion,
while the expression still resolves to its fallback value.
See also
Variant data for the related var.* namespace
and the <{ ... }> reference syntax, which inject build-wide data
rather than selecting between values conditionally.
Variant data¶
sphinx-needs ≥ 8.2.
Variant data is a nested, read-only key-value store that ubCode exposes
under the var.* namespace inside filter expressions.
It lets a single “150%” model —
one set of needs that describes every product variant —
be filtered, extended, and rendered differently for each build,
without duplicating content.
See also
Variant builds walks through the end-to-end workflow — including building and diffing each variant from the command line. This section is the configuration reference for the options it uses.
For example, a project that targets several CPU architectures and build configurations can keep one copy of its requirements and select the data relevant to the current build:
[needs.variant_data]
platform = "arm"
debug = false
opt_level = 2
archs = ["arm", "x86"]
[needs.variant_data.build]
compiler = "clang"
features = ["networking", "logging"]
With this configuration the following values are available in any filter expression:
var.platform == "arm" # scalar string
var.opt_level == 2 # scalar integer
var.debug == False # scalar boolean
"x86" in var.archs # membership in an array
var.build.compiler == "clang" # nested scalar
"networking" in var.build.features # membership in a nested array
Note
var.* resolves only against the configured variant data.
Unlike need fields or filter_data keys,
it does not depend on the need currently being evaluated,
which makes it suitable for global, build-wide switches.
Looking up an unknown key (e.g. var.missing) is a
needs.variant diagnostic, not a silent false.
Defining variant data¶
Variant data can be provided inline, from a JSON file, or both.
It lives in one of two places in ubproject.toml:
the shared [variants] table,
which every tool reading the file uses,
or the legacy variant_data and variant_data_file keys of [needs],
which ubCode still reads, without a warning.
Note
sphinx-needs reads [variants] from 9.0.0 on, and sphinx-mounts from 0.3.0 on;
every earlier release ignores the table silently,
so keep the keys in [needs] until your Sphinx build uses those.
The examples on this page and in Variant builds use [needs] for that reason;
the same values work unchanged under [variants].
- variant_data
Type:
object(default: unset)An inline, nested table of variant data, declared directly in
ubproject.tomlunder[needs.variant_data](as shown above). Its[variants]spelling is data. Settingvariant_data = {}counts as setting the table (see below).- variant_data_file
Type:
string(default: unset)Path to a JSON file containing variant data, resolved relative to the
ubproject.tomlthat declares it. This is convenient when the values are generated by a build system or shared across several projects. Its[variants]spelling is data_file, and it follows the same rules: one non-empty path, joined as written.[needs] variant_data_file = "variant_data.json"
{ "platform": "linux", "build": {"compiler": "gcc", "features": ["networking"]} }
When both are set, the file is loaded first as the base,
and the inline [needs.variant_data] values are deep-merged on top,
with the inline values winning:
At nested maps, keys are merged recursively.
At scalars and arrays, the inline value replaces the file value entirely.
[needs]
# file sets platform = "linux", build.compiler = "gcc", build.features = [...]
variant_data_file = "variant_data.json"
[needs.variant_data]
platform = "windows" # overrides the file's scalar value
[needs.variant_data.build]
compiler = "clang" # overrides build.compiler;
# build.features is preserved from the file
Because inline values take precedence, you can keep shared defaults in the file under version control and override individual switches per build.
The [variants] table¶
[variants] holds the same two values under the names the whole tool family shares,
so one ubproject.toml gives sphinx-needs, sphinx-mounts and ubCode the same variant data:
[variants]
data_file = "variant_data.json"
[variants.data]
platform = "windows"
- data
Type:
object(default: unset)An inline, nested table of variant data, under
[variants.data], merged over the file exactly as[needs.variant_data]is. Settingdata = {}counts as setting the table (see below).
- data_file
Type:
string(default: unset)Path to a JSON file holding one object of variant data. It is one non-empty path — a list of paths, and an empty string, are refused. A relative path is joined to the directory of the
ubproject.tomlthat sets it, as written: a..in it is kept, not folded away.
Any other key under [variants] is ignored and reported as config.variants_unknown_key,
so one file can serve tool versions that read more keys.
A variants that is not a table,
or a data or data_file of the wrong type,
is a configuration error.
Which table is read¶
The variant data is read from one table, never one key from each:
[variants], when it setsdataordata_file—data = {}included; an empty[variants]table, or one holding only other keys, sets neither;otherwise the legacy
[needs]keys;otherwise nothing, and
var.*is empty.
When both tables set variant data,
[variants] is read and each [needs] key it ignores is reported as config.variant_data_location,
at that key (needs.variant_data or needs.variant_data_file).
The ignored data file is never opened,
so a missing or broken one there is not reported.
Across extend and -c,
the inline table is merged key by key:
the nearer configuration’s values (a -c override is the nearest)
are deep-merged over the inherited ones, exactly as the inline table is merged over the file,
and a nearer data file replaces an inherited one.
The table is chosen once, after that merge.
[variants] wins wherever the chain or an override declares it:
an override or configuration that sets [variants] data or data_file switches a [needs] project to it,
and one that sets [needs] under a [variants] declaration is ignored — both warned.
So migrating a shared base moves every child still on [needs] to the base’s table,
and each such child’s own [needs] values are dropped (warned at its keys).
Conversely, a project on [variants] that extends a base still on [needs]
reads its own [variants] alone and reports the base’s [needs] keys:
migrate the base first, or, while a shared base is still on [needs],
add config.variant_data_location to the child’s lint.ignore
(which replaces an inherited list — repeat the base’s entries),
or pass it for one run (ubc check --extend-ignore, ubc build html --lint-extend-ignore).
An unknown [variants] key in a base is reported in every configuration that extends it.
Value types¶
Variant data values may be:
Scalars — string, integer, number (float), or boolean.
Uniform arrays of scalars — every element must share the same type.
Nested maps — arbitrarily deep tables, accessed with further dots (
var.build.sanitizers.asan).
A mixed-type array, and a TOML date-time value (which is not a variant value),
are reported as config.variant_data_invalid at the inline table’s key;
a date-time inside an array is a configuration error,
like an array of tables or of arrays in ubproject.toml.
The data file is checked on its own, before the merge,
so an inline value cannot hide an invalid file value by replacing it.
Everything wrong with the file —
missing, not JSON, not one object, a JSON null, or a value of a shape above —
is reported at the data-file key, under the same code, as is an empty file name.
Each is a warning, so ubc check and ubc build html exit non-zero,
and a bare "config" in lint.ignore does not silence it (see Code matching and .* wildcards).
Using var.* in filters¶
The var.* namespace is available anywhere ubCode evaluates a
filter expression against your needs configuration.
Variant functions — select a field value per build (see Variants):
.. req:: Bootloader
:id: REQ_001
:status: <<[var.platform == "windows"]: active, inactive>>
Conditional defaults — vary a field’s default via predicates:
[needs.fields.compiler_note]
default = "unknown-compiler"
predicates = [
['var.build.compiler == "clang"', "using-clang"],
['var.build.compiler == "gcc"', "using-gcc"],
]
needextend — extend only the needs relevant to the current build:
.. needextend:: var.platform == "windows"
:status: supported
needimport — import only the needs that match:
.. needimport:: external.json
:filter: var.platform == "windows"
External needs — filter needs loaded from another project:
[[needs.external_needs]]
json_path = "imported.json"
filter = 'var.platform == "windows"'
Referencing variant data in field values (<{ ... }>)¶
In addition to filtering, variant data can be injected directly into a
field or link value using the <{ ... }> syntax.
The text between the delimiters is a var.* path,
and ubCode substitutes the stored value at index time.
Unlike variant functions (<<...>>),
which choose between candidate values using conditional logic,
a <{ ... }> reference is a direct lookup —
there is no condition, just a substitution.
This syntax is only parsed for fields and links that opt in with
parse_variants = true (the same flag that enables <<...>>):
[needs.variant_data]
platform = "arm"
links = ["REQ_001", "REQ_002"]
[needs.variant_data.build]
opt_level = 2
[needs.fields.arch]
schema = {type = "string"}
parse_variants = true
[needs.fields.opt]
schema = {type = "integer"}
parse_variants = true
[needs.links.derived_from]
parse_variants = true
The references can then be used on any need:
.. req:: Scalar reference
:id: REQ_001
:arch: <{ var.platform }>
:opt: <{ var.build.opt_level }>
.. req:: Embedded in a string
:id: REQ_002
:arch: built for <{ var.platform }>
.. req:: Within an array and a link field
:id: REQ_003
:tags: a, <{ var.platform }>, c
:derived_from: <{ var.links }>
A few rules apply:
For string fields the reference may be embedded inside surrounding text; for non-string scalar fields (integer, number, boolean) the reference must be the entire value.
The resolved value is type-checked against the field’s schema.
If the field does not set
parse_variants = true, the<{ ... }>text is kept literally.
The following situations are reported as needs.variant diagnostics
(and leave the value unset):
Invalid expression — the content is not a dotted
var.*path, e.g.<{ platform }>(missing thevar.root).Unknown key — the path does not exist in the variant data, e.g.
<{ var.nonexistent }>or<{ var.build.missing }>.Non-leaf value — the path resolves to a nested map rather than a scalar or array, e.g.
<{ var.build }>.Type mismatch — the resolved value’s type does not match the field, e.g. injecting a string into an integer field.
See also
The corresponding sphinx-needs feature is documented under variant data.
Dynamic functions¶
ubCode resolves the built-in dynamic functions using the [[...]] syntax
from sphinx-needs.
All seven built-in functions are supported:
echo("text")— returns the given string unchangedtest(*args, **kwargs)— returns a debug string with the need ID and argumentscopy("field", need_id=None, filter=None)— copies a field value (also supportslowerandupperkwargs).fieldmay be any of the need’s own fields or links, or a context field —id,type,content,docname,lineno,sections,section_name,parent_need,is_external,is_directive,is_import,is_src_trace,is_modified,modifications,is_need,is_part,id_parent,id_complete. A project-declared field of the same name wins, except forid,typeandcontent.need_id— as a keyword or as the second positional argument — copies from that need instead of the current one. Withfilter, it copies from the need passing the filter whose id sorts first character by character, and falls back to the needneed_idnames, or to the current need, when none passeslinks_from_content(need_id=None, filter=None)— returns the needs referenced via:need:roles in a need’s own content, the current need’s unlessneed_idnames another (aneed_idnaming no need, or an external need, which has no content, fails the call, which is reported), keeping withfilteronly the needs that pass it.need_idmay be given as a keyword or as the first positional argument,filteras the secondcheck_linked_values(result, search_option, search_value, filter_string=None, one_hit=False)— compares thesearch_optionfield of every need this one:links:to (withfilter_string, of those passing it) againstsearch_value. Withoutone_hit,resultis returned only when they all match — and also when the need links to nothing at all. Withone_hit,resultis returned whatever the values are: that is a quirk of Sphinx-Needs itself, mirrored here deliberately so the two tools agree. Otherwise the call yields nothing. A link to no need is skipped (the dead link is reported on its own). It fails only on asearch_optionthat is not a field, link or context field of the linked need — a failed call renders??in a role or:style_row:, and is skipped silently in a field or link valuecalc_sum("option", filter=None, links_only=False)— sums a numeric field across the project’s needs, or across only the needs this one links to whenlinks_onlyis set (a link to no need skipped), counting withfilteronly the needs that pass itlinks_from_filter(filter, include_self=False, include_parts=False)— links every need passing the filter, in need-id order (stored character by character,SPC_10beforeSPC_9; aneeds.jsonexport sorts them naturally), leaving the current need out unlessinclude_selfis set; an empty filter is an error.include_parts=Truetests every need’s parts too, as thelinkskind’sinclude_partsdoes (also when aneed.<attr>gives it)
Several of them are the inline spelling of a derived-field kind, which writes the same rule once for every need.
A filter (filter, filter_string on check_linked_values) is written in the
filter language and evaluated on each candidate need.
An empty filter ("") is no filter;
one that is only whitespace is a filter that cannot be evaluated on any need,
and each call treats it as it treats such a need:
calc_sum still counts every need and check_linked_values still tests every linked need
(each reporting needs.filter),
copy and links_from_content fail the call,
and links_from_filter refuses it.
A filter naming needs or current_need, or reading a c. attribute such as c.this_doc(),
reads beyond the candidate need, and is not evaluated here
(the portable form of a test over other needs is a boolean field derived with kind = "any",
filtered on in a sum rule’s where = "<flag>" or with calc_sum(…, filter="<flag>")).
A call with such a filter is reported where it is written (needs.dynamic_functions, info),
and what you see in its place depends on where you wrote it:
a role keeps rendering as the role chip — its own text, unevaluated;
a [[...]] written in a needtable’s :style_row: renders ??;
and a [[...]] written in a need’s field or link value
is skipped silently, leaving the field at its other values.
(A bare [[...]] in a need’s body content is not evaluated at all —
use the ndf role there.)
Functions a project registers through needs_functions are not resolved.
For example:
.. req:: My requirement
:id: REQ_001
:status: [[copy("priority")]]
.. spec:: My specification
:id: SPEC_001
:links: [[links_from_content()]]
This specification realises :need:`REQ_001`.
Note
In a need’s own field or link value,
copy('is_modified') and copy('modifications') count needextend matches only —
the same caveat the Variants section carries,
and for the same reason: a need’s own resolution cannot be an input to itself.
Every other context field resolves as a :filter: resolves it.
In prose: the ndf and need_func roles¶
The same functions can be called from inside prose, and the result is rendered in place as plain text:
A dynamic function :ndf:`echo("in a paragraph")` reads inline.
.. req:: My requirement
:id: REQ_002
This need's own id is :ndf:`copy("id")`.
ndf takes the bare call.
need_func takes text containing a [[...]] call
and substitutes the result into that text, keeping what surrounds it:
:need_func:`before [[echo("x")]] after`
renders before x after.
Upstream deprecated need_func in favour of ndf in sphinx-needs 3.1.0;
ubCode resolves both.
Which need a call reads depends on where it is written.
Inside a need’s body, the call reads that need,
so copy("id") above renders REQ_002.
Outside any need there is no current need,
so a function that needs one — copy and links_from_content — renders ??.
A Sphinx build renders ?? there too, but it also logs a warning saying which
function failed and why; ubCode renders the ?? silently.
That difference is deliberate and is recorded under
When ubCode cannot resolve a call.
echo and test need no need and resolve anywhere.
MyST works the same way, with the role spelled {ndf} / {need_func}.
Note
A need_func whose text contains no [[...]] at all
renders that text literally and reports nothing —
which is what a Sphinx build does with it too.
When ubCode cannot resolve a call¶
A call that cannot resolve for any need is reported once,
at the place it is written,
under the needs.dynamic_functions code.
Three things trigger it:
the text does not parse as a function call,
it names a function ubCode does not implement,
or it names one that cannot be called that way.
The report covers need field and link values,
a needtable :style_row:,
and the two roles above.
These findings are informational, not warnings,
and that is deliberate:
ubCode implements every built-in,
but a Sphinx project also has everything needs_functions registers
and ubCode cannot run project Python,
so an unresolvable call is often a statement about ubCode
rather than about the document.
They are reported, they do not fail ubc check,
and they can be silenced like any other code:
[lint]
ignore = ["needs.dynamic_functions"]
A role whose call is reported this way keeps rendering
as the role chip — its own text, unevaluated —
rather than as ??.
?? would claim the call ran and failed,
which is not true of a function a Sphinx build resolves and ubCode does not.
A call that ubCode does implement but that fails when it runs —
copy("status") on a need that has no status,
or any call needing a need when there is none —
renders ?? and is not reported.
A Sphinx build reports these: it renders the same ?? and logs, for example,
Error while executing function 'copy': Option status not found in need REQ_002.
ubCode is silent, and that is a known gap rather than a considered quietness:
the value is computed while the page is being rendered,
which is after the point where ubCode attaches findings to a source location.
Reporting it is planned.
Everything ubCode can decide from the text you wrote — an unparseable call,
an unknown function name, a bad argument list — is reported, once, at the place
you wrote it.
A call in a need’s field that runs but returns a value of the wrong type for that field —
a number, an integer, a boolean or a list in a string field,
or a string or a list in an integer, number or boolean one —
is a warning on the need, as in a Sphinx build,
and the field keeps its empty value (a list keeps the items written beside the call):
needs.dynamic_function,
Dynamic function value type mismatch for field 'summary' on need 'REQ_001': expected string, got number.
An integer is accepted in a number field.
An array field takes a value of its item type or a list of such values,
and stores an empty list as [];
one item of another type fails the whole field, the values written beside the call included
(… expected array(integer), got string).
To store such a result, declare the field with its type,
for example { name = "total", schema = { type = "number" } } under extra_options.
A call that returns nothing (None) is the same warning, … got null,
in a field that may not be empty — an extra_options entry without a schema is one —
and leaves a field that may be empty (nullable) at null.
Dynamic function parsing is enabled by default and can be controlled globally or per-field/per-link:
[needs]
parse_dynamic_functions = false # disable globally (sphinx-needs ≥ 7.0)
[needs.fields.my_field]
parse_dynamic_functions = true # re-enable for this field (sphinx-needs ≥ 6.3)
[needs.links.implements]
parse_dynamic_functions = true # re-enable for this link (sphinx-needs ≥ 6.3)
- parse_dynamic_functions
Type:
boolean(default:true)Whether dynamic functions are parsed by default. Applies to extra fields and links unless overridden per-field/per-link. Core fields are not affected by this global setting.
The global
parse_dynamic_functionsoption was added in sphinx-needs 7.0 (needs_parse_dynamic_functions). Per-field and per-linkparse_dynamic_functionswas added in sphinx-needs 6.3.
When a call reads a value another call computes¶
A call may read a value another call computes:
copy('summary', 'B') where B’s summary is itself a [[...]] or a <<...>> variant,
a calc_sum over such values,
a calc_sum(..., links_only=True) or check_linked_values whose own links carry a call,
or a variant condition naming one.
ubCode resolves the calls in dependency order:
first every call in a link type, such as links,
then every other call, each after every value it reads.
So the reader gets the computed value, on every build,
whatever order the needs and documents are written in.
A call in a link type that reads through its need’s own links,
when those are computed too, runs after them, and after every need that computes the value it reads there.
Between the two steps the backlinks are built,
so a call or a variant condition can read them as <link type>_back:
copy('links_back') lists the needs whose links name this one, sorted by id,
and is an empty list when there are none — a list either way, so it belongs in an array field;
<<['REQ_1' in links_back]:linked, unlinked>> tests it, and len(links_back) == 0 holds when none links here.
The dead-link flags are set at the same point:
has_dead_links, whether a link of the need names a need that does not exist or a part its need does not have,
and has_forbidden_dead_links, the same over the link types that do not set allow_dead_links.
Two kinds of read cannot be ordered, and each is a warning on the need that makes it:
needs.derive_cycleThe value is on a cycle of reads:
A’ssummarycopiesB’s, which copiesA’s;:hours: [[calc_sum('hours')]], which sums every need’shours, its own included; or a variant whose condition reads the field it fills,:status: <<[status == 'open']: active, inactive>>. Every value on the cycle keeps its empty value (a list keeps the items written beside its calls), and each gets one finding naming the cycle. Break the cycle: read another field, or give one of the values an authored value. A call that reads a value on a cycle reads that empty value and gets no finding of its own.needs.derive_scopeThe read needs a value that is not final where it is made: a call in a link type reading a field another call computes, or a backlink, both of which are computed after the links (through the need’s own
links, when those are computed too, a field that any need computes); a call, or a rule withoutafter = "derived", reading a field ahashrule or a value kind withafter = "derived"computes, which runs after every other field; aneed.<attr>argument naming which need or field a call reads, where that attribute is itself a call (one the need does not set at all — an unset untyped extra option is the empty string — fails the call instead,needs.dynamic_function,need.src selects what the call reads, and is not set); or aneedextendfilter naming a field that a call or variant computes on some need, since everyneedextendapplies before any call resolves. The call is not run and its field keeps its empty value (a link or an array keeps the items written beside the call); aneedextendstill applies, matched against the values as written. Read a value that is final before the read: an authored field, or, for aneedextend, write the condition as a variant or a call on the field the extend would set.
Both fail ubc check and the build gate, as warnings do,
and can be silenced like any other code:
[lint]
ignore = ["needs.derive_cycle", "needs.derive_scope"]
A silenced finding still leaves the value empty.
Earlier releases resolved every call in one pass:
a call read the value stored before resolution —
the field’s empty value, or only the literal part of a list —
and the read was reported as needs.derive_unresolved, an informational finding.
That code is retired; an ignore that still names it is harmless.
Released versions of Sphinx-Needs answer these reads by document order,
and adopt the same dependency order in 9.0.0;
see Dynamic functions resolve all seven built-ins, and their filters.
filter_data¶
Type: object (default: {})
Additional context variables that can be referenced in filter expressions throughout your needs configuration. This allows you to define custom variables for conditional logic in multiple contexts.
Where filter_data can be used:
Predicate default filters — in
global_optionspredicates (orpredicateson individual fields/links in sphinx-needs ≥ 7.0) to conditionally set default valuesneedimport item filters — to filter which needs are imported from external sources
needextend filters — to conditionally extend or modify needs based on context
[needs]
filter_data = { environment = "production", team = "backend", version = "2.0" }
# Using predicates on a field directly (sphinx-needs ≥ 7.0, recommended)
[needs.fields.priority]
default = "medium"
predicates = [
["environment == 'production'", "high"],
["team == 'backend'", "high"]
]
# Or via global_options (sphinx-needs < 7, deprecated)
# [needs.global_options.priority]
# default = "medium"
# predicates = [
# ["environment == 'production'", "high"],
# ["team == 'backend'", "high"]
# ]
Example with needimport:
.. needimport:: external_needs.json
:filter: environment == 'production' and version == '2.0'
Example with needextend:
.. needextend:: team == 'backend'
:status: critical
Common use cases:
Setting environment-specific defaults (development, staging, production)
Filtering imported needs based on release version or team ownership
Applying team or project-specific configurations
Conditional formatting or validation based on build context
Dynamic behaviour in CI/CD pipelines
import_keys¶
Type: object (default: {})
Mapping of keys for needimport directives,
allowing short keys to reference longer file paths.
Available since sphinx-needs 4.2.
[needs.import_keys]
backend = "/path/to/backend_needs.json"
frontend = "/path/to/frontend_needs.json"
JSON output¶
These keys shape the needs.json export.
They apply to whatever writes that file —
ubc build needs (the default --format json),
and ubc build html when build_json is on —
so the shaping below is identical whichever command wrote the file.
The two commands still differ in one respect these keys do not govern:
ubc build needs writes source maps by default
(an __source__ entry per need, recording where each need was written),
while the build_json sidecar omits them,
which is closer to what Sphinx-Needs writes.
Note
The keys do not shape ubc diff.
That command projects the index through the same writer
with no shaping applied, so an excluded field is still compared.
Note that its link comparison is currently condition-blind:
it reads link values without their [condition] suffixes,
so editing only a link’s condition does not register as a change.
Splitting the two uses apart —
conditions for the field-by-field comparison,
bare IDs for the impact graph, which keys nodes on them —
is a separate piece of work.
build_json¶
Type: boolean (default: false)
When true, ubc build html also writes needs.json
into the root of the output directory, beside the pages.
Clearing the key removes the copy an earlier build wrote,
provided the output directory is one ubCode owns
(one it created itself, or an empty one) —
in a directory it does not own it never deletes anything,
so a stale export would have to be removed by hand.
Warning
In an output directory ubCode owns,
needs.json at the root is a builder-managed name,
like the generated pages and the _static/ and _images/ trees.
A file you place there yourself under that name is removed by the next build,
whether or not build_json is set.
Keep a hand-maintained needs.json outside the output directory.
[needs]
build_json = true
json_exclude_fields¶
Type: array of string
Default:
json_exclude_fields = [
"lineno_content", "collapse", "hide",
"type_prefix", "type_color", "type_style",
"is_need", "is_part", "id_parent", "id_complete",
]
Need fields to leave out of the file.
A listed name is dropped from every need object
and from the file’s needs_schema,
so the schema always describes exactly what the file contains.
An authored list replaces the default rather than adding to it. Names that no need or schema uses are ignored, so listing a field the project does not define is harmless — which is why the default can carry the Sphinx-Needs core-field names that have no ubCode equivalent. Set an empty list to write every field:
[needs]
json_exclude_fields = []
json_remove_defaults¶
Type: boolean (default: false)
When true, a field whose value equals that field’s default is left out.
This shrinks the file considerably on projects
where most needs take the defaults.
The file records "needs_defaults_removed": true,
and a reader must re-apply the defaults published in needs_schema itself.
json_include_link_conditions¶
Type: boolean (default: true)
Whether outgoing link values keep the [condition] suffix they were authored with.
With the default true, a conditional link is written in full —
"links": ["REQ_001[status == \"open\"]"] —
and a target authored twice with different conditions stays two entries.
With false, only the target is written — "links": ["REQ_001"].
Backlinks never carry a condition, so they are unaffected either way.
Note
One difference from Sphinx-Needs on the false path:
a target authored twice with different conditions
becomes a single bare entry in ubCode,
where Sphinx-Needs writes it twice (["REQ_001", "REQ_001"]).
ubCode removes a duplicate the bare form cannot distinguish;
Sphinx-Needs decides the question earlier, while the conditions are still visible.
The same narrowing means len() in a :filter: counts such a target once
in ubCode and twice in Sphinx-Needs, at any setting of this key.
reproducible_json¶
Type: boolean (default: false)
Accepted for Sphinx-Needs compatibility; it has no effect in ubCode.
Sphinx-Needs writes a wall-clock created timestamp into needs.json
unless this key is set, which makes the file differ on every build.
ubCode never writes that timestamp,
so its needs.json is already byte-identical
across builds of unchanged sources —
the behaviour reproducible_json = true asks for is what ubCode always does.
The key is accepted so that a configuration shared with Sphinx-Needs
does not have to be edited.