Summary
TestDirective.run() reads three pieces of metadata off a .. doctest:: / ```{doctest} block and stores them on the docutils node. None of the three is ever read again, so all three are silently inert: an inline # doctest: flag inside a directive never applies, :options: never applies, and :skipif: never skips. Anyone using the directive form instead of a plain fence gets an unexplained failure.
Reproduction
opts.rst:
Title
=====
.. doctest::
>>> print("a b") # doctest: +NORMALIZE_WHITESPACE
a b
.. doctest::
:options: +SKIP
>>> 1 + 1
99999
.. doctest::
:skipif: True
>>> 1 + 1
88888
$ pytest opts.rst -q --no-header
Expected
The first block passes, because NORMALIZE_WHITESPACE collapses the run of spaces. The second and third are skipped. Written as a plain fence outside a directive, the same inline flag already works — nothing trims it there.
Actual
All three fail. The first reports Expected: a b against Got: a b: the flag was removed from the source before the parser ever saw it. The second and third run their bodies and report the mismatch instead of skipping.
Versions
gp-libs v0.0.19, CPython 3.14, pytest 9.1.1, docutils 0.21.
Root cause
_get_test() receives only the node's rendered text and its line number, so everything else the directive parsed stays on the node and dies there:
node["test"] holds the untrimmed source. With trim-doctest-flags on by default, run() strips # doctest: ... out of the rendered code and keeps the original here — but the finder parses node.astext(), the trimmed copy.
node["options"] holds the flag map built from :options:, including a reporter warning for an unknown flag name.
node["skipif"] holds the condition expression from :skipif:.
sphinx.ext.doctest reads the first one back:
source = node['test'] if 'test' in node else node.astext()
Why it matters
trim-doctest-flags exists so a rendered page does not show test noise to a reader. Today it trims the flag out of the test as well, so the documented workaround — :no-trim-doctest-flags: — fixes the test by putting the noise back on the page, which is the opposite of what the option is for.
:options: and :skipif: are documented options of a directive this project ships. Accepting them, validating them, warning on a bad flag name, and then discarding the result is worse than not accepting them at all.
Proposal
Pass the node's metadata down to _get_test():
- use
node["test"] when present and node.astext() otherwise
- merge
node["options"] into each parsed example's options, letting an example's own inline flag win, as Sphinx does
- evaluate
node["skipif"] and drop the block when it is true
The rendered output should stay trimmed; that part already works.
Summary
TestDirective.run()reads three pieces of metadata off a.. doctest::/```{doctest}block and stores them on the docutils node. None of the three is ever read again, so all three are silently inert: an inline# doctest:flag inside a directive never applies,:options:never applies, and:skipif:never skips. Anyone using the directive form instead of a plain fence gets an unexplained failure.Reproduction
opts.rst:$ pytest opts.rst -q --no-headerExpected
The first block passes, because
NORMALIZE_WHITESPACEcollapses the run of spaces. The second and third are skipped. Written as a plain fence outside a directive, the same inline flag already works — nothing trims it there.Actual
All three fail. The first reports
Expected: a bagainstGot: a b: the flag was removed from the source before the parser ever saw it. The second and third run their bodies and report the mismatch instead of skipping.Versions
gp-libs v0.0.19, CPython 3.14, pytest 9.1.1, docutils 0.21.
Root cause
_get_test()receives only the node's rendered text and its line number, so everything else the directive parsed stays on the node and dies there:node["test"]holds the untrimmed source. Withtrim-doctest-flagson by default,run()strips# doctest: ...out of the rendered code and keeps the original here — but the finder parsesnode.astext(), the trimmed copy.node["options"]holds the flag map built from:options:, including a reporter warning for an unknown flag name.node["skipif"]holds the condition expression from:skipif:.sphinx.ext.doctestreads the first one back:Why it matters
trim-doctest-flagsexists so a rendered page does not show test noise to a reader. Today it trims the flag out of the test as well, so the documented workaround —:no-trim-doctest-flags:— fixes the test by putting the noise back on the page, which is the opposite of what the option is for.:options:and:skipif:are documented options of a directive this project ships. Accepting them, validating them, warning on a bad flag name, and then discarding the result is worse than not accepting them at all.Proposal
Pass the node's metadata down to
_get_test():node["test"]when present andnode.astext()otherwisenode["options"]into each parsed example's options, letting an example's own inline flag win, as Sphinx doesnode["skipif"]and drop the block when it is trueThe rendered output should stay trimmed; that part already works.