What
A choose directive whose content holds only when directives, an optional final otherwise, and comments. The when conditions are evaluated in order against the variant data, the first true one wins, and otherwise is the default. Only the winning branch's content is parsed, so needs inside the other branches are never created, exactly as in a falsy if today.
.. choose::
.. when:: var.arch == "arm"
ARM content.
.. when:: var.arch == "x86"
x86 content.
.. otherwise::
Content for every other architecture.
::::{choose}
:::{when} var.arch == "arm"
ARM content.
:::
:::{otherwise}
The default.
:::
::::
Implemented in #2020.
Renamed on 2026-10-01 from the match / case first proposed here, following the comment below: match and case take a subject in Python and Rust, and this construct has none. Every when holds a condition, and the default is an explicit otherwise, so a forgotten condition is reported instead of silently becoming the catch-all. match / case stay free for a subject-based form later.
Why a container rather than elif / else siblings
For authors, a choose reads the way a choice is thought about: every alternative for one piece of content stands in one block, in order, with the fallback visibly last. What is inside the block is the whole story. A paragraph or a comment between two alternatives, or an include boundary, cannot detach a branch or turn a fallback into an orphan, and a branch written inside a wrapper directive that passes its content through is refused with a warning, because every when belongs to its choose and to nothing else. Mistakes are reported once, where they are made, and a mistake that makes a condition unevaluable never shows the wrong variant: it skips the whole choice rather than falling through to the default, and a when that lost its condition is reported rather than silently becoming the catch-all. A branch can hold anything a document can (sections, needs, includes, another choose), and the branches not taken are never parsed, so no stray need or ID from another variant reaches the build. The same block builds the same way in reStructuredText, in MyST and in ubCode (what still differs is registered in ubCode's divergence register), and in ubCode the editor fades the branches that do not apply to the variant being built, so an author always sees which content is live. if is unchanged, so existing documents keep working as they are.
This is also why a container is sound where sibling elif / else are not. A docutils directive cannot see its source siblings, so sibling branches need the previous branch to leave a message behind. Every mechanism the sibling design in #1999 needs (an invisible marker node returned by every branch including every plain if, a backward scan of the parent's children, a MyST fallback through state.inliner.parent, a stripping transform, a second strip inside add_need because the need-node cache is filled before any transform, a comments-only-between-branches rule, chains crossing .. include::) is a consequence of that one fact. A container owns its branches, sees them all at once, decides once, and nothing escapes: all of it disappears by construction, and if stays untouched.
Decision (2026-10-01): implement the container instead of elif / else. The test cases of #1999 that have a counterpart in a container design are salvaged, as is its fix for the undocumented non-bool warning in docs/directives/if.rst.
Design: the deferred-content branch
when and otherwise do not parse their content. Each returns a transient placeholder carrying its StringList, content_offset, the condition (None for otherwise), its kind and its location. Both declare an optional argument: when so that a missing condition is reported by the choose with one warning (a required argument would make docutils refuse the directive with an error of its own), otherwise so that a condition on it can be refused.
choose (an optional argument declared only so that it can be refused: without a declared argument, docutils and MyST both move first-line text into the content, which would then be reported only as a stray paragraph) first reads its body's top-level lines and refuses the body, with one warning at the line, at the first that is neither a branch start, a comment nor blank (nothing is parsed then; in reStructuredText docutils' own explicit-markup constructs decide, at column 0; in MyST a branch is a {when} / {otherwise} fence closed by CommonMark's rule, and only % comments, +++ and blank lines stand between branches), then nested-parses its own body into a detached container that is never returned, validates the children (placeholders and nodes.comment only), checks that every when has a condition, that otherwise has none, is last and occurs at most once, evaluates the conditions in order with exactly if's evaluator extracted into one shared function, and parses only the winner with nested_parse_with_titles at the winner's offset (re-based under MyST, whose content_offset is relative to the directive line).
- A depth counter in
env.temp_data, raised around the body parse only, detects a when or otherwise outside any choose, including one written loose inside a branch body.
- Nothing outside a branch is ever parsed, so there is nothing to roll back: a need, a label, a
needextend, an included file or another extension's directive written in the body is refused before it runs. The gate may refuse more than the parser would accept (a refused line is never parsed); it never passes a line the parser would execute as anything else.
- No transform, no
add_node, no change to api/need.py.
A prototype of the container design, under its first names (284 lines), was built against master and passes 8 happy-path probes and 31 error-path probes in RST and MyST, -W clean, on Sphinx 9.1 / myst-parser 5.1 and on the sphinx-7 cell (Sphinx 7.4.7 / myst-parser 4.0.1), including a choose inside a need that needextract renders on another page.
Semantics (the contract ubCode implements too)
| situation |
behaviour |
several when true |
the first wins; later conditions are not evaluated, so they cannot warn |
none true, otherwise present |
otherwise is taken |
none true, no otherwise |
nothing rendered, no warning |
when without a condition |
warn once at the when; the whole choose renders nothing |
otherwise given a condition |
warn once at the otherwise; the whole choose renders nothing |
otherwise not last, or two of them |
warn once at the offending otherwise; the whole choose renders nothing |
content in the body that is neither a branch nor a comment (a line of punctuation such as --- included) |
warn once at its line, before the body is parsed; whole choose skipped; nothing in it runs (no need, label or included file is made and undone) |
a comment that begins with when: or otherwise: (a branch written with one colon, or without the space after ::, is a comment in reStructuredText; a MyST % when: comment or +++ when: block break likewise) |
warn once at the comment; whole choose skipped (a comment that merely starts with the word is still a comment) |
a branch inside another directive in the body (even a true if), or supplied through an include |
the wrapping directive or the .. include:: is itself content that is neither a branch nor a comment: warn once at its line, in the host; whole choose skipped; the included file is never read (branches are written in place; a whole choose inside an included file is fine). A directive that produces no node (a FALSE if, default-role) and a MyST {{ sub }} are refused the same way, as ubCode refuses any non-branch child |
when or otherwise outside any choose |
warn once; its content skipped |
| a condition that cannot be evaluated (before the winner) |
warn once; the whole choose is skipped, otherwise included (a mistake that makes a condition unevaluable never renders the fallback in its place) |
| a non-bool result |
warn once, coerce and use (as if) |
choose given an argument |
warn once; whole choose skipped |
empty choose, or comments only |
warn once |
| variant data not configured |
warn once at the choose; whole choose skipped, even when only an otherwise exists; the structure is checked first |
| headings inside the winner |
real sections |
| needs and errors inside a losing branch |
never created, never reported |
| comments accepted |
RST .. comments; MyST % comments and +++ block breaks; an HTML <!-- --> is a raw node and is rejected |
a sphinx-design tab-item directly inside the winner |
warns "parent should be a tab-set", exactly as inside a true if (the winner is parsed into a detached container); documented |
Warnings go under a new subtype, needs.choose. Conditions are those of if, through the same function, so choose adds no new expression language.
Deliverables
src/sphinx_needs/directives/needchoose.py; the evaluator moved out of IfDirective.run into a shared function (messages byte-identical); registration next to if; "choose" in WarningSubTypes and its description.
docs/directives/choose.rst (+ toctree, a pointer from if.rst), including the MyST rule that an outer fence must be longer than its inner fences.
tests/test_choose_directive.py with a doc project and inline projects covering every row above, the MyST twin, needextract, and one expression table run through both if and when asserting the same verdicts; tests/test_choose_gate.py, a pure table of the gate in both syntaxes.
- A changelog entry.
ubCode implements the same contract (useblocks/ubcode#3763, PR useblocks/ubcode#3783), with the editor fading each losing branch.
What
A
choosedirective whose content holds onlywhendirectives, an optional finalotherwise, and comments. Thewhenconditions are evaluated in order against the variant data, the first true one wins, andotherwiseis the default. Only the winning branch's content is parsed, so needs inside the other branches are never created, exactly as in a falsyiftoday.::::{choose} :::{when} var.arch == "arm" ARM content. ::: :::{otherwise} The default. ::: ::::Implemented in #2020.
Renamed on 2026-10-01 from the
match/casefirst proposed here, following the comment below:matchandcasetake a subject in Python and Rust, and this construct has none. Everywhenholds a condition, and the default is an explicitotherwise, so a forgotten condition is reported instead of silently becoming the catch-all.match/casestay free for a subject-based form later.Why a container rather than
elif/elsesiblingsFor authors, a
choosereads the way a choice is thought about: every alternative for one piece of content stands in one block, in order, with the fallback visibly last. What is inside the block is the whole story. A paragraph or a comment between two alternatives, or anincludeboundary, cannot detach a branch or turn a fallback into an orphan, and a branch written inside a wrapper directive that passes its content through is refused with a warning, because everywhenbelongs to itschooseand to nothing else. Mistakes are reported once, where they are made, and a mistake that makes a condition unevaluable never shows the wrong variant: it skips the whole choice rather than falling through to the default, and awhenthat lost its condition is reported rather than silently becoming the catch-all. A branch can hold anything a document can (sections, needs, includes, anotherchoose), and the branches not taken are never parsed, so no stray need or ID from another variant reaches the build. The same block builds the same way in reStructuredText, in MyST and in ubCode (what still differs is registered in ubCode's divergence register), and in ubCode the editor fades the branches that do not apply to the variant being built, so an author always sees which content is live.ifis unchanged, so existing documents keep working as they are.This is also why a container is sound where sibling
elif/elseare not. A docutils directive cannot see its source siblings, so sibling branches need the previous branch to leave a message behind. Every mechanism the sibling design in #1999 needs (an invisible marker node returned by every branch including every plainif, a backward scan of the parent's children, a MyST fallback throughstate.inliner.parent, a stripping transform, a second strip insideadd_needbecause the need-node cache is filled before any transform, a comments-only-between-branches rule, chains crossing.. include::) is a consequence of that one fact. A container owns its branches, sees them all at once, decides once, and nothing escapes: all of it disappears by construction, andifstays untouched.Decision (2026-10-01): implement the container instead of
elif/else. The test cases of #1999 that have a counterpart in a container design are salvaged, as is its fix for the undocumented non-bool warning indocs/directives/if.rst.Design: the deferred-content branch
whenandotherwisedo not parse their content. Each returns a transient placeholder carrying itsStringList,content_offset, the condition (Noneforotherwise), its kind and its location. Both declare an optional argument:whenso that a missing condition is reported by thechoosewith one warning (a required argument would make docutils refuse the directive with an error of its own),otherwiseso that a condition on it can be refused.choose(an optional argument declared only so that it can be refused: without a declared argument, docutils and MyST both move first-line text into the content, which would then be reported only as a stray paragraph) first reads its body's top-level lines and refuses the body, with one warning at the line, at the first that is neither a branch start, a comment nor blank (nothing is parsed then; in reStructuredText docutils' own explicit-markup constructs decide, at column 0; in MyST a branch is a{when}/{otherwise}fence closed by CommonMark's rule, and only%comments,+++and blank lines stand between branches), then nested-parses its own body into a detached container that is never returned, validates the children (placeholders andnodes.commentonly), checks that everywhenhas a condition, thatotherwisehas none, is last and occurs at most once, evaluates the conditions in order with exactlyif's evaluator extracted into one shared function, and parses only the winner withnested_parse_with_titlesat the winner's offset (re-based under MyST, whosecontent_offsetis relative to the directive line).env.temp_data, raised around the body parse only, detects awhenorotherwiseoutside anychoose, including one written loose inside a branch body.needextend, an included file or another extension's directive written in the body is refused before it runs. The gate may refuse more than the parser would accept (a refused line is never parsed); it never passes a line the parser would execute as anything else.add_node, no change toapi/need.py.A prototype of the container design, under its first names (284 lines), was built against master and passes 8 happy-path probes and 31 error-path probes in RST and MyST,
-Wclean, on Sphinx 9.1 / myst-parser 5.1 and on the sphinx-7 cell (Sphinx 7.4.7 / myst-parser 4.0.1), including achooseinside a need thatneedextractrenders on another page.Semantics (the contract ubCode implements too)
whentrueotherwisepresentotherwiseis takenotherwisewhenwithout a conditionwhen; the wholechooserenders nothingotherwisegiven a conditionotherwise; the wholechooserenders nothingotherwisenot last, or two of themotherwise; the wholechooserenders nothing---included)chooseskipped; nothing in it runs (no need, label or included file is made and undone)when:orotherwise:(a branch written with one colon, or without the space after::, is a comment in reStructuredText; a MyST% when:comment or+++ when:block break likewise)chooseskipped (a comment that merely starts with the word is still a comment)if), or supplied through an include.. include::is itself content that is neither a branch nor a comment: warn once at its line, in the host; wholechooseskipped; the included file is never read (branches are written in place; a wholechooseinside an included file is fine). A directive that produces no node (a FALSEif,default-role) and a MyST{{ sub }}are refused the same way, as ubCode refuses any non-branch childwhenorotherwiseoutside anychoosechooseis skipped,otherwiseincluded (a mistake that makes a condition unevaluable never renders the fallback in its place)if)choosegiven an argumentchooseskippedchoose, or comments onlychoose; wholechooseskipped, even when only anotherwiseexists; the structure is checked first..comments; MyST%comments and+++block breaks; an HTML<!-- -->is a raw node and is rejectedtab-itemdirectly inside the winnerif(the winner is parsed into a detached container); documentedWarnings go under a new subtype,
needs.choose. Conditions are those ofif, through the same function, sochooseadds no new expression language.Deliverables
src/sphinx_needs/directives/needchoose.py; the evaluator moved out ofIfDirective.runinto a shared function (messages byte-identical); registration next toif;"choose"inWarningSubTypesand its description.docs/directives/choose.rst(+ toctree, a pointer fromif.rst), including the MyST rule that an outer fence must be longer than its inner fences.tests/test_choose_directive.pywith a doc project and inline projects covering every row above, the MyST twin,needextract, and one expression table run through bothifandwhenasserting the same verdicts;tests/test_choose_gate.py, a pure table of the gate in both syntaxes.ubCode implements the same contract (useblocks/ubcode#3763, PR useblocks/ubcode#3783), with the editor fading each losing branch.