Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
81 commits
Select commit Hold shift + click to select a range
f818c48
doctest_docutils(fix[find]): Keep document order
tony Aug 1, 2026
b1ed9ad
tests(doctest_docutils): Pin document-order collection
tony Aug 1, 2026
072c746
py(deps[dev]) Add pytest-xdist
tony Aug 1, 2026
0e2888d
doctest_docutils(fix[find]): Name tests by page
tony Aug 1, 2026
13b4638
tests(pytest_doctest_docutils): Pin node ids
tony Aug 1, 2026
1d036a8
doctest_docutils(fix[find]): Read untrimmed source
tony Aug 1, 2026
332adfc
tests(doctest_docutils): Cover inline flags
tony Aug 1, 2026
e36a431
doctest_docutils(fix[find]): Apply :options:
tony Aug 1, 2026
a7d15b5
tests(doctest_docutils): Cover directive options
tony Aug 1, 2026
cf03a8e
doctest_docutils(fix[find]): Line nested blocks
tony Aug 1, 2026
8c800a9
tests(doctest_docutils): Cover nested blocks
tony Aug 1, 2026
f4bc3c7
doctest_docutils(fix[find]): Honor :skipif:
tony Aug 1, 2026
2299850
tests(doctest_docutils): Cover skipif
tony Aug 1, 2026
c5c3d1f
doctest_docutils(fix[flags]): Register HIDE
tony Aug 1, 2026
8647c24
tests(doctest_docutils): Cover HIDE outside pytest
tony Aug 1, 2026
6c79461
doctest_docutils(refactor[log]): Log lazily
tony Aug 1, 2026
5e60069
doctest_docutils(feat[find]): Share namespaces
tony Aug 1, 2026
5d3bc0a
tests(doctest_docutils): Cover namespace merging
tony Aug 1, 2026
b5d3a1a
pytest_doctest_docutils(feat[ini]): Scope option
tony Aug 1, 2026
24de107
tests(pytest_doctest_docutils): Cover scoping
tony Aug 1, 2026
611af23
docs(modules): Document namespace scope
tony Aug 1, 2026
2a2a7ce
doctest_docutils(fix[directive]): Order :pyversion: args
tony Aug 1, 2026
5568284
tests(doctest_docutils): Cover pyversion
tony Aug 1, 2026
8d0c581
tests(doctest_docutils): Pin directive-source behaviour
tony Aug 1, 2026
ae92482
doctest_docutils(fix[skipif]): Skip, don't drop
tony Aug 1, 2026
af23dcf
tests(doctest_docutils): Cover skipif skip flag
tony Aug 1, 2026
3d870b4
docs(how-to[skipif]): Say what a skip covers
tony Aug 1, 2026
57b08f3
doctest_docutils(fix[testsetup]): Order the group
tony Aug 1, 2026
feaf414
tests(doctest_docutils): Cover setup/cleanup order
tony Aug 1, 2026
37994b3
doctest_docutils(fix[groups]): Join every group
tony Aug 1, 2026
61e8ee1
tests(doctest_docutils): Cover multi-group blocks
tony Aug 1, 2026
51784b4
docs(how-to[groups]): Say how a group is joined
tony Aug 1, 2026
cd2ef7c
doctest_docutils(fix[skipif]): Make a condition a gate
tony Aug 1, 2026
a70f597
docs(how-to[skipif]): Say a condition is a gate
tony Aug 1, 2026
d0008f9
doctest_docutils(fix[skip]): Report a gated block
tony Aug 1, 2026
4e534e0
tests(skip): Cover a lifted gated block
tony Aug 1, 2026
589a633
docs(how-to[skip]): Say a gated block reports
tony Aug 1, 2026
1acbfff
doctest_docutils(fix[merge]): Lay out blocks by page order
tony Aug 1, 2026
24a0635
tests(merge): Pin lines for out-of-order phases
tony Aug 1, 2026
efaee93
doctest_docutils(docs[merge]): Document the keep parameter
tony Aug 1, 2026
d96688c
doctest_docutils(refactor[log]): Record events in past tense
tony Aug 1, 2026
478f6ee
docs(how-to[merge]): Say what a failure leaves behind
tony Aug 1, 2026
6492bdc
tests(log): Filter records by message
tony Aug 1, 2026
81f29ea
doctest_docutils(fix[log]): Log details in extra
tony Aug 1, 2026
eb4bb79
doctest_docutils(docs[find]): Correct two overstatements
tony Aug 1, 2026
2015547
tests(merge): Pin lines as source pairs
tony Aug 1, 2026
eba6fd6
doctest_docutils(fix[pyversion]): Drop a dead catch
tony Aug 1, 2026
21d8755
tests(doctest_docutils): Cover two unexercised paths
tony Aug 1, 2026
d73375b
pytest_doctest_docutils(docs[skip]): Pin the empty case
tony Aug 1, 2026
1d7d8fa
doctest_docutils(feat[items]): One test per block
tony Aug 1, 2026
d2c8cbf
pytest_doctest_docutils(feat[ini]): Items option
tony Aug 1, 2026
59cc2fc
tests(doctest_docutils): Cover per-block layout
tony Aug 1, 2026
86d6000
tests(pytest_doctest_docutils): Cover per-block
tony Aug 1, 2026
058bd1a
docs(how-to[items]): Document per-block items
tony Aug 1, 2026
35a189b
doctest_docutils(fix[merge]): Read blocks, don't consume
tony Aug 1, 2026
c43a3b0
tests(merge): Pin that merging is repeatable
tony Aug 1, 2026
4bca593
tests(retry): Pin a failure through a retry
tony Aug 1, 2026
6ba2160
docs(how-to[items]): Name the retry hazard
tony Aug 1, 2026
c07f53f
pytest_doctest_docutils(fix[items]): Refuse a repeat
tony Aug 1, 2026
dac99f0
tests(items): Cover a refused repeat
tony Aug 1, 2026
d816040
docs(how-to[items]): Show the refused repeat
tony Aug 1, 2026
4b69faa
pytest_doctest_docutils(fix[dist]): Fill scheduler
tony Aug 1, 2026
9afb4b4
tests(dist): Cover the filled-in scheduler
tony Aug 1, 2026
983c5fb
docs(how-to[items]): Say what fills in a scheduler
tony Aug 1, 2026
6b8c79f
pytest_doctest_docutils(fix[dist]): Count a multiplier
tony Aug 1, 2026
899a095
tests(dist): Cover a multiplied worker spec
tony Aug 1, 2026
5bf4661
doctest_docutils(fix[groups]): Refuse a taken name
tony Aug 1, 2026
77348fe
tests(groups): Cover a taken namespace name
tony Aug 1, 2026
013a7a9
docs(how-to[groups]): Say a name can be taken
tony Aug 1, 2026
7a78afa
pytest_doctest_docutils(fix[dist]): Match xdist's expansion
tony Aug 1, 2026
3a6bdef
tests(dist): Pin a count that asks for none
tony Aug 1, 2026
ec5d2d5
doctest_docutils(fix[groups]): Refuse a taken lifted name
tony Aug 1, 2026
ddc2a34
tests(groups): Cover a taken lifted name
tony Aug 1, 2026
cbb796d
tests(fixtures): Pin a page-wide fixture
tony Aug 2, 2026
ff0c68c
docs(how-to[fixtures]): Name the scope a page is
tony Aug 2, 2026
41c65a7
doctest_docutils(feat[testcode]): Run a prompt-free block
tony Aug 2, 2026
309ada5
tests(testcode): Cover a page written to be pasted
tony Aug 2, 2026
46df4f0
docs(how-to[testcode]): Show the prompt-free form
tony Aug 2, 2026
2103c8e
doctest_docutils(fix[groups]): Name a prompt-free block for its page
tony Aug 2, 2026
7903007
tests(groups): Cover where the two forms meet
tony Aug 2, 2026
8a25122
docs(how-to[testcode]): Say where the two forms meet
tony Aug 2, 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
6 changes: 6 additions & 0 deletions docs/AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,12 @@ dogfood the tool they describe; a broken example is a failing test.
```` ```{doctest} ```` directive — the finder collects both, plus
bare doctest blocks in reST. Use ```` ```console ```` for shell
commands at a `$` prompt.
- When the block is written to be pasted, drop the prompt and fence it
as ```` ```{testcode} ````, with ```` ```{testoutput} ```` for what it
prints and `:hide:` for a block that asserts without rendering. A
page's `{testcode}` blocks are named for the page and share one
namespace; a `>>>` block joins them only at document scope, so keep
each page to one form unless it runs at that scope.
- `ELLIPSIS` and `NORMALIZE_WHITESPACE` are on globally via
`doctest_optionflags`, so variable output can elide with `...`
without a per-example flag.
Expand Down
40 changes: 40 additions & 0 deletions docs/modules/doctest_docutils/examples.md
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,46 @@ examples aligned with {class}`doctest_docutils.DocutilsDocTestFinder`.
['md', 'rst']
```

A directive keeps a reader's view of the example clean: the rendered page drops
the `# doctest: +NORMALIZE_WHITESPACE` written below, while the run still
applies it, so the two spaces in the printed output match the one below them.

```{doctest}
>>> print("a b") # doctest: +NORMALIZE_WHITESPACE
a b
```

## Blocks that share a namespace

Blocks naming the same group collect as one test, so the second reads what the
first bound:

```python
>>> import doctest_docutils
>>> finder = doctest_docutils.DocutilsDocTestFinder()
>>> source = (
... "```{doctest} intro\n>>> greeting = 'hello'\n```\n"
... "\nProse between the blocks.\n\n"
... "```{doctest} intro\n>>> greeting.upper()\n'HELLO'\n```\n"
... )
>>> tests = finder.find(source, "example.md")
>>> [(test.name, len(test.examples)) for test in tests]
[('intro', 2)]
```

Blocks naming no group keep a namespace each, until you ask for the page:

```python
>>> import doctest_docutils
>>> page = "```python\n>>> alone = 1\n```\n\n```python\n>>> alone\n1\n```\n"
>>> apart = doctest_docutils.DocutilsDocTestFinder()
>>> [test.name for test in apart.find(page, "example.md")]
['example.md[0]', 'example.md[1]']
>>> shared = doctest_docutils.DocutilsDocTestFinder(namespace_scope="document")
>>> [test.name for test in shared.find(page, "example.md")]
['example.md']
```

## Finder result names

{class}`~doctest_docutils.DocutilsDocTestFinder` names collected examples with
Expand Down
129 changes: 129 additions & 0 deletions docs/modules/doctest_docutils/how-to.md
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,135 @@ Pass `-v` for verbose standard-library doctest output:
$ python -m doctest_docutils README.md -v
```

## Let a page build one example across several blocks

Every block runs against a namespace of its own, so a name bound in one block is
gone by the next and any block can be run on its own. When a page is one session
told in pieces, widen the namespace to the whole page:

```console
$ python -m doctest_docutils README.md --namespace-scope document
```

Blocks that name a group share that group's namespace at either setting, because
naming a group is the author asking for it. A group is named as the directive's
argument, `.. doctest:: intro` in reStructuredText and its `{doctest} intro`
fence in Markdown. `--namespace-scope document` also pools the blocks that name
none.

One name a group cannot take is the one the page would generate for a block that
declares none — the page's own name at `--namespace-scope document`, the page and
the block's position at the default. Both would answer to one namespace and one
node id, so a page spelling both stops with
{exc}`~doctest_docutils.NamespaceNameCollisionError` rather than merging them.
Rename the group; a page whose every block names one generates nothing to collide
with, so `.. doctest:: README.md` on such a page is only a style choice.

Sharing costs you the guarantee that a block stands alone: a block that reads an
earlier binding fails when it is read, or run, by itself. See
{ref}`the pytest plugin's how-to <pytest_doctest_docutils-how-to>` for the same
choice under pytest, spelled `--doctest-docutils-namespace-scope` there, and for
what sharing costs a test run.

A shared page is reported as one item by default. Ask for one item per block,
each named for where the block sits, when you want to read the run block by
block:

```console
$ python -m doctest_docutils README.md --namespace-scope document --namespace-items per-block -v
```

A passing page prints nothing without `-v`. What changes without it is a
failure's heading, which names the block — `in README.md[1]` rather than
`in README.md`.

Nothing here schedules the blocks apart, so they share the namespace either way.
Under pytest they can be scheduled apart, which is what
{ref}`the plugin's how-to <pytest_doctest_docutils-how-to>` covers.

## Write a block a reader is meant to paste

A `>>>` prompt is for a session a reader reads. When the block is there to be
copied into a file, the prompt is in the way, and an expected-output line beneath
it puts an assertion into whatever the reader pasted. Such a page carries no
prompt at all — and a finder that goes looking for `>>>` cannot see it.

Write those blocks as `{testcode}`, the directive {mod}`sphinx.ext.doctest`
defines. The body is plain Python, run the way a module body runs, so it takes as
many statements as it likes and a bare expression on the last line prints
nothing:

```{testcode}
greeting = "hello"
shouted = greeting.upper()
```

A `{testcode}` expects to print nothing. When it does print, say what with a
`{testoutput}` block under it:

```{testcode}
print(shouted)
```

```{testoutput}
HELLO
```

The two blocks above share a namespace, so the second reads what the first bound.
A `{testcode}` that names no group is named for its page, because sharing the
page is the whole point of the form — a visible block and the hidden one
asserting on it have to meet somewhere. Name a group as the directive's
argument, `{testcode} intro`, to keep two runs on one page apart.

A `>>>` block is named for its page only where the scope above says so. So at
`--namespace-scope document` the two forms land in the same namespace and read
each other's names, and at the default they do not. Write a page that mixes them
at document scope, or keep each page to one form.

A page written this way sets up the same way, with no prompt:

````markdown
```{testsetup}
base = 40
```
````

A `{testsetup}` and `{testcleanup}` may still be written with prompts, which is
how the rest of these docs write them; the prompt decides how the body is read.
A page holding a `{testcode}` names its unnamed setup for the page too, so the
setup a prompt-free page writes reaches the code it is for.

That is what lets a page assert without showing its assertions. Mark a block
`:hide:` and it runs while every builder drops it, so the reader meets only the
block written to be pasted:

````markdown
```{testcode}
:hide:

assert shouted == "HELLO"
```
````

```{testcode}
:hide:

assert shouted == "HELLO"
```

The page you are reading has that hidden block in it, immediately above.

`{testoutput}` takes `:options:` for the doctest flags the comparison runs under,
and both directives take `:skipif:`. `:pyversion:` parses, because Sphinx
declares it here, but neither Sphinx nor this runner acts on it outside
`{doctest}` — the page says so when you use it. Guard a block with `:skipif:`
instead.

The cost of the prompt-free form is that there is no interleaving: one block is
one example, so a `{testoutput}` says what the block prints in total rather than
what any line in it prints. A failure quotes the block entire, so the reader sees
where they are.

## Compare with stdlib doctest

Use the stdlib command when you are checking Python modules or plain text that
Expand Down
3 changes: 2 additions & 1 deletion docs/modules/doctest_docutils/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,8 @@ Run your first documentation doctest from a Markdown page.
:::{grid-item-card} How-to
:link: how-to
:link-type: doc
Choose files, run verbose output, and map the command to stdlib doctest.
Choose files, share a namespace across blocks, run verbose output, and map
the command to stdlib doctest.
:::

:::{grid-item-card} Examples
Expand Down
7 changes: 7 additions & 0 deletions docs/modules/pytest_doctest_docutils/fixtures.md
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,13 @@ Then the documentation page can use the helper by name:
add(2, 3)
```

A helper like this one holds nothing, so how long it lives never comes up.
Seeding a *resource* — a server, a connection, a temporary directory — is where
it does, because the fixture's scope decides how long the object a page saved
stays usable. See {ref}`what per-block items cost
<pytest_doctest_docutils-per-block-costs>` before carrying one across several
blocks of a page.

## Autouse fixtures

Autouse fixtures in a visible `conftest.py` are parsed for `.rst` and `.md`
Expand Down
Loading