Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
28 commits
Select commit Hold shift + click to select a range
797b44b
♻️ sphinx-needs: one evaluator for variant conditions, out of `if`
claude Oct 1, 2026
e70eebf
✨ sphinx-needs: `match` and `case` directives (#2011)
claude Oct 1, 2026
b929a5a
🧪 sphinx-needs: tests for `match` and `case`
claude Oct 1, 2026
57efbfe
📚 sphinx-needs: document `match` and `case`, and the changelog entry
claude Oct 1, 2026
1255bce
👌 sphinx-needs: a `case` supplied through an include is refused
claude Oct 1, 2026
4a2b185
👌 sphinx-needs: every mistake in a `match` body warns, including the …
claude Oct 1, 2026
66fe682
🧪 sphinx-needs: pin the rollback, the depth restore and the check ord…
claude Oct 1, 2026
43d2522
👌 sphinx-needs: below WARNING, a docutils message never hides a `matc…
claude Oct 1, 2026
6d3e0ff
Merge origin/master into claude/elegant-darwin-2lj6bb
claude Oct 1, 2026
69fa72e
📚 sphinx-needs: the changelog entry names its pull request
claude Oct 1, 2026
2edfb83
♻️ sphinx-needs: rename `match` / `case` to `choose` / `when`
claude Oct 1, 2026
745929f
✨ sphinx-needs: an explicit `otherwise`, and a `when` must have a con…
claude Oct 1, 2026
1a4bc83
📚 sphinx-needs: document `choose`, `when` and `otherwise`
claude Oct 1, 2026
72157c9
🧪 sphinx-needs: pin the choose check order, name the otherwise text, …
claude Oct 1, 2026
cc885e1
🔧 sphinx-needs: correct the comment on the `choose` argument's declar…
claude Oct 1, 2026
a62191f
🔧 sphinx-needs: say why each branch directive declares its argument
claude Oct 1, 2026
a1220af
🧪 sphinx-needs: pin the document order of the branch faults
claude Oct 1, 2026
48d293d
🧪 sphinx-needs-testkit: rewrite a POSIX-spelt location on Windows
claude Oct 1, 2026
dadd8c4
🧪 CI: upload coverage on pushes to master too
claude Oct 1, 2026
f685d61
♻️ CI: move the Codecov upload fix to a pull request of its own
claude Oct 3, 2026
5bef3f9
Merge remote-tracking branch 'origin/master' into claude/elegant-darw…
claude Oct 3, 2026
6683c04
🐛 sphinx-needs: refuse a branch written with one colon in a choose
claude Oct 3, 2026
e3d037d
🧪 sphinx-needs: pin the case and spacing of the one-colon rule
claude Oct 3, 2026
c34102c
👌 sphinx-needs: the one-colon hint names the MyST fence under MyST
claude Oct 3, 2026
2b6d896
🐛 sphinx-needs: locate choose warnings with an absolute path
claude Oct 3, 2026
b521eee
Revert "🧪 sphinx-needs-testkit: rewrite a POSIX-spelt location on Win…
claude Oct 3, 2026
cdc6b1b
🧪 sphinx-needs: build the choose warning rows from the source directory
claude Oct 3, 2026
b00e991
♻️ sphinx-needs: gate the choose body before parsing it
claude Oct 3, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
45 changes: 45 additions & 0 deletions packages/sphinx-needs/docs/changelog.rst
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,51 @@
Changelog
=========

Unreleased
----------

Improvements
............

- ✨ New :ref:`choose <choose>`, ``when`` and ``otherwise`` directives include one of
several branches of content, chosen by variant data (:pr:`2020`)

A ``choose`` runs its ``when`` tests in order, and the first ``when`` whose condition
is true is included; an ``otherwise``, the optional default, comes last; when no test
holds and there is no ``otherwise``, nothing is rendered. The other branches are never
parsed, so the needs inside them are never created:

.. code-block:: rst

.. choose::

.. when:: var.arch == "arm"

ARM content.

.. when:: var.arch == "x86"

x86 content.

.. otherwise::

Content for every other architecture.

Conditions are exactly those of the :ref:`if <if>` directive, evaluated by the same
code, and the conditions after the branch that is taken are not evaluated. A
``choose`` may contain only ``when`` and ``otherwise`` directives and comments, and
nothing outside a branch is ever parsed. Every mistake warns once under the new
``needs.choose`` type and skips the whole ``choose``: content outside a branch
(refused before the body is parsed, so nothing in it runs), a branch inside another
directive or supplied through an include, a branch written with one colon, no branch
at all, a ``when`` without a condition, an ``otherwise`` with one, a misplaced or
second ``otherwise``, an argument on ``choose``, variant data that is not configured,
and a condition that cannot be evaluated — so a mistake that makes a condition
unevaluable, such as a misspelt key or a syntax error, never renders a later branch
or the ``otherwise`` in its place. Works in reStructuredText and in MyST Markdown.
The undocumented warning ``if`` gives for a condition whose result is not a bool is
now listed in its documentation.

.. _`release:8.5.0`:

8.5.0
Expand Down
199 changes: 199 additions & 0 deletions packages/sphinx-needs/docs/directives/choose.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,199 @@
.. _choose:

choose
======

.. versionadded:: 8.6.0

The ``choose`` directive includes one of several branches of content,
chosen by :ref:`variant data <filter_variant_data>` at parse time.
Its branches are ``when`` and ``otherwise`` directives:
``choose`` runs its ``when`` tests in order, and the first true one is included;
``otherwise`` is the optional default, and comes last;
when no test holds and there is no ``otherwise``, nothing is rendered.
The content of every other branch is never parsed,
so the needs inside it are never created.
The names are those of ``choose`` / ``when`` / ``otherwise`` in XSLT, JSTL and MSBuild, which run their tests the same way.

.. code-block:: rst

.. choose::

.. when:: var.arch == "arm"

ARM content.

.. req:: ARM-specific requirement
:id: REQ_ARM_001

.. when:: var.arch == "x86"

x86 content.

.. a comment may stand between two branches

.. otherwise::

Content for every other architecture.

A ``choose`` is the many-branched form of :ref:`if <if>`:
the example includes the ARM content, the x86 content or the content of the ``otherwise``,
and never more than one of them.
Unlike a ``switch`` or a ``match`` statement, a ``choose`` has no subject:
every ``when`` holds a whole condition.

MyST Markdown
-------------

In MyST Markdown, ``choose``, ``when`` and ``otherwise`` are fenced directives like any other.
With colon fences (the ``colon_fence`` extension):

.. code-block:: md

::::{choose}
:::{when} var.arch == "arm"
ARM content.
:::
% a comment may stand between two branches
:::{otherwise}
Content for every other architecture.
:::
::::

and with backtick fences:

.. code-block:: md

````{choose}
```{when} var.arch == "arm"
ARM content.
```
```{otherwise}
Content for every other architecture.
```
````

An outer fence must be longer than the fences inside it,
so a ``choose`` takes one more colon (or backtick) than its branches,
and a ``choose`` nested in a branch takes one fewer than that branch:

.. code-block:: md

::::::{choose}
:::::{when} var.debug
::::{choose}
:::{when} var.arch == "arm"
ARM debug content.
:::
::::
:::::
::::::

``%`` starts a MyST comment, and a ``+++`` block break counts as one too.

Rules
-----

- **The first true branch wins.**
The conditions are evaluated in order, and the first ``when`` whose condition is true is included.
The conditions after it are not evaluated at all, so they cannot warn.
When no condition is true, the ``otherwise`` is included;
without an ``otherwise``, the ``choose`` then includes nothing, without a warning, as a false ``if`` does.
- **Every test has a condition, and the default has none.**
A ``when`` without a condition is a mistake rather than a default,
so a condition forgotten on the last ``when`` cannot make it the branch for every other variant.
An ``otherwise`` takes no condition.
A ``choose`` has at most one ``otherwise``, and it must be its last branch.
- **Only branches and comments.**
A ``choose`` may contain only ``when`` and ``otherwise`` directives and comments:
reStructuredText comments (``..``), and in MyST ``%`` comments and ``+++`` block breaks.
In MyST, an HTML comment (``<!-- -->``) is raw HTML rather than a comment, so it is a mistake here.
Any other content outside a branch is a mistake,
refused before anything in the body is parsed, so nothing in it ever runs.
A branch belongs directly in a ``choose``:
one anywhere else is a mistake too,
whether it is written loose in the content of another branch
or inside another directive in the ``choose``,
even one that passes its content through, such as a true ``if`` or a ``rst-class``.
- **The branches are written in place.**
Every branch of a ``choose`` is written in the body of that ``choose``, in the same file,
so that one choice is one directive in one place.
An ``.. include::`` (in MyST, an ``{include}``) may not supply the branches;
it may be used inside the content of a branch,
and a whole ``choose`` may stand in an included file.
- **The included branch is ordinary content.**
It may hold headings, which become sections where the ``choose`` stands,
needs, any other directive, and further ``choose`` directives.
An ``.. include::`` may supply part of the content of a branch,
and a ``choose`` may stand in the content of a need.
- **Parse-time evaluation**, as for ``if``:
the content of a branch that is not included is never parsed,
so its needs are never created and its mistakes are never reported.

Conditions
----------

A ``when`` condition is exactly a condition of the :ref:`if <if>` directive,
evaluated by the same code:
a Python expression over the ``var`` namespace, with no built-in functions
(see the ``if`` directive's :ref:`if_expression_context`).
A result that is not a ``bool`` is warned about and then used as its truth value, as for ``if``.

A condition wrapped onto a second line is joined with a line break,
which is a syntax error unless the break falls inside brackets or is escaped with a backslash;
keep conditions on one line.

Warnings
--------

Every mistake warns once, under the ``needs.choose`` type
(suppressible via ``suppress_warnings = ["needs.choose"]``),
at the line of the directive or the content that has it,
and skips the **whole** ``choose``: nothing of it is included, not even its ``otherwise``.
The mistakes are:

- ``needs_variant_data`` is not configured, even when the ``choose`` holds only an ``otherwise``.
- A condition cannot be evaluated (a syntax error, an unknown key, etc.) before a branch is taken.
So a mistake that makes a condition unevaluable, such as a misspelt key or a syntax error,
never renders a later branch or the ``otherwise`` in its place.
(A mistake that leaves a valid condition, such as a misspelt value, cannot be told apart
from a condition that is false.)
- The ``choose`` contains something that is neither a ``when``, an ``otherwise`` nor a comment.
A line of only punctuation, such as ``---`` between two branches, is such content too.
It is refused at its line before anything in the body is parsed,
so a directive there (a need, an ``.. include::``, a false ``if``) never runs.
- 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 ``when`` written so would hand the choice
to the ``otherwise``, and an ``otherwise`` written so would make the default vanish.
- A branch is written inside another directive in the ``choose`` rather than directly in it,
or supplied through an include: the directive or the include is such content,
and the warning points at its line (the included file is never read).
- A ``when`` has no condition: write the default as an ``otherwise``.
- An ``otherwise`` is given a condition. The warning names it:
content written on the line right after ``.. otherwise::``, with no blank line between, is read as one.
- The ``choose`` has more than one ``otherwise``, or an ``otherwise`` that is not its last branch.
- The ``choose`` has no ``when`` or ``otherwise`` at all.
- The ``choose`` is given an argument: the conditions go on the ``when`` directives.

A ``when`` or an ``otherwise`` outside a ``choose`` warns as well, and its content is skipped.
A condition whose result is not a ``bool`` warns, and its truth value is used.
A line that docutils or MyST would report, such as an unknown directive name,
is refused by the ``choose`` before either parses it,
so it is reported once, by the ``choose``, whatever the project's ``report_level``.

.. note::

The body of a ``choose`` is read line by line before it is parsed,
so content outside a branch is refused even where it would leave no trace in the document:
a directive that produces no node, such as ``default-role`` or a **false** ``if``,
and a MyST substitution reference (``{{ sub }}``), which is a line of text to the ``choose``,
whatever branches its definition holds.

.. note::

The content of the branch that is taken is parsed on its own, as the body of a true ``if`` is,
so a directive in it that checks its parent does not find the parent of the ``choose``.
A sphinx-design ``tab-item`` in the taken branch warns
``The parent of a 'tab-item' should be a 'tab-set'``, exactly as in a true ``if``,
even when the ``choose`` stands in a ``tab-set``.
To vary the content of a tab, put the ``choose`` inside the ``tab-item``.
6 changes: 6 additions & 0 deletions packages/sphinx-needs/docs/directives/if.rst
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ The directive argument is a Python expression evaluated against the ``var``
namespace (populated from :ref:`needs_variant_data`).
If the expression evaluates to ``True``, the directive body is parsed and
included in the document. Otherwise the entire body is skipped.
To include one of several branches instead, use :ref:`choose <choose>`.

.. code-block:: rst

Expand Down Expand Up @@ -77,6 +78,8 @@ The body may contain section headers and any valid reStructuredText:

Content under a conditional heading.

.. _if_expression_context:

Expression context
------------------

Expand Down Expand Up @@ -104,6 +107,8 @@ Behavior
Use :ref:`filter` for need-aware filtering.
- **Incremental builds**: If a document is re-read (e.g., because the source
changed), all ``if`` directives in it are re-evaluated.
- **Parsed on its own**: the body does not see where the ``if`` stands, so a sphinx-design
``tab-item`` in a true ``if`` warns that its parent should be a ``tab-set``, even inside one.

Warnings
--------
Expand All @@ -112,3 +117,4 @@ The directive emits warnings (suppressible via ``suppress_warnings = ["needs.if"

- ``needs_variant_data`` is not configured but the directive is used.
- The expression raises an exception (syntax error, unknown key, etc.).
- The expression does not return a bool (the result is still used, as its truth value).
1 change: 1 addition & 0 deletions packages/sphinx-needs/docs/directives/index.rst
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ Directives for conditional content:
:maxdepth: 1

if
choose

Directives for visualizing and analyzing needs:

Expand Down
Loading
Loading