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
675f37b
doctest_docutils(fix[find]): Keep document order
tony Aug 1, 2026
0862169
tests(doctest_docutils): Pin document-order collection
tony Aug 1, 2026
a6e248b
py(deps[dev]) Add pytest-xdist
tony Aug 1, 2026
1aaa674
doctest_docutils(fix[find]): Name tests by page
tony Aug 1, 2026
6c401f9
tests(pytest_doctest_docutils): Pin node ids
tony Aug 1, 2026
a588036
doctest_docutils(fix[find]): Read untrimmed source
tony Aug 1, 2026
07751bb
tests(doctest_docutils): Cover inline flags
tony Aug 1, 2026
9b4d68c
doctest_docutils(fix[find]): Apply :options:
tony Aug 1, 2026
ee93d51
tests(doctest_docutils): Cover directive options
tony Aug 1, 2026
3dc968e
doctest_docutils(fix[find]): Line nested blocks
tony Aug 1, 2026
67f5f4a
tests(doctest_docutils): Cover nested blocks
tony Aug 1, 2026
1200db1
doctest_docutils(fix[find]): Honor :skipif:
tony Aug 1, 2026
4df5800
tests(doctest_docutils): Cover skipif
tony Aug 1, 2026
cd7926e
doctest_docutils(fix[flags]): Register HIDE
tony Aug 1, 2026
772c139
tests(doctest_docutils): Cover HIDE outside pytest
tony Aug 1, 2026
f809c04
doctest_docutils(refactor[log]): Log lazily
tony Aug 1, 2026
565c1f4
doctest_docutils(feat[find]): Share namespaces
tony Aug 1, 2026
b0d1214
tests(doctest_docutils): Cover namespace merging
tony Aug 1, 2026
eced8fb
pytest_doctest_docutils(feat[ini]): Scope option
tony Aug 1, 2026
b8324e8
tests(pytest_doctest_docutils): Cover scoping
tony Aug 1, 2026
040b6b8
docs(modules): Document namespace scope
tony Aug 1, 2026
5636483
doctest_docutils(fix[directive]): Order :pyversion: args
tony Aug 1, 2026
7a68b7f
tests(doctest_docutils): Cover pyversion
tony Aug 1, 2026
982651d
tests(doctest_docutils): Pin directive-source behaviour
tony Aug 1, 2026
e63555c
doctest_docutils(fix[skipif]): Skip, don't drop
tony Aug 1, 2026
ed1fd0b
tests(doctest_docutils): Cover skipif skip flag
tony Aug 1, 2026
1fa8326
docs(how-to[skipif]): Say what a skip covers
tony Aug 1, 2026
8e92715
doctest_docutils(fix[testsetup]): Order the group
tony Aug 1, 2026
1b84de4
tests(doctest_docutils): Cover setup/cleanup order
tony Aug 1, 2026
27d2b88
doctest_docutils(fix[groups]): Join every group
tony Aug 1, 2026
8db229a
tests(doctest_docutils): Cover multi-group blocks
tony Aug 1, 2026
b0ace6f
docs(how-to[groups]): Say how a group is joined
tony Aug 1, 2026
896595c
doctest_docutils(fix[skipif]): Make a condition a gate
tony Aug 1, 2026
adb53bf
docs(how-to[skipif]): Say a condition is a gate
tony Aug 1, 2026
2624491
doctest_docutils(fix[skip]): Report a gated block
tony Aug 1, 2026
43eb5e5
tests(skip): Cover a lifted gated block
tony Aug 1, 2026
f8827a5
docs(how-to[skip]): Say a gated block reports
tony Aug 1, 2026
1a30f07
doctest_docutils(fix[merge]): Lay out blocks by page order
tony Aug 1, 2026
4b34b05
tests(merge): Pin lines for out-of-order phases
tony Aug 1, 2026
c5ddc40
doctest_docutils(docs[merge]): Document the keep parameter
tony Aug 1, 2026
d8c7742
doctest_docutils(refactor[log]): Record events in past tense
tony Aug 1, 2026
226d3a8
docs(how-to[merge]): Say what a failure leaves behind
tony Aug 1, 2026
52dede6
tests(log): Filter records by message
tony Aug 1, 2026
07c3f77
doctest_docutils(fix[log]): Log details in extra
tony Aug 1, 2026
893f9ed
doctest_docutils(docs[find]): Correct two overstatements
tony Aug 1, 2026
ede267e
tests(merge): Pin lines as source pairs
tony Aug 1, 2026
692f261
doctest_docutils(fix[pyversion]): Drop a dead catch
tony Aug 1, 2026
f24bf56
tests(doctest_docutils): Cover two unexercised paths
tony Aug 1, 2026
520c512
pytest_doctest_docutils(docs[skip]): Pin the empty case
tony Aug 1, 2026
1f16032
doctest_docutils(feat[items]): One test per block
tony Aug 1, 2026
24d00c6
pytest_doctest_docutils(feat[ini]): Items option
tony Aug 1, 2026
ddc2aba
tests(doctest_docutils): Cover per-block layout
tony Aug 1, 2026
2efc5a7
tests(pytest_doctest_docutils): Cover per-block
tony Aug 1, 2026
3b6e622
docs(how-to[items]): Document per-block items
tony Aug 1, 2026
262c4f4
doctest_docutils(fix[merge]): Read blocks, don't consume
tony Aug 1, 2026
f0e4e0f
tests(merge): Pin that merging is repeatable
tony Aug 1, 2026
ff500bd
tests(retry): Pin a failure through a retry
tony Aug 1, 2026
1014574
docs(how-to[items]): Name the retry hazard
tony Aug 1, 2026
b4897a0
pytest_doctest_docutils(fix[items]): Refuse a repeat
tony Aug 1, 2026
e12939f
tests(items): Cover a refused repeat
tony Aug 1, 2026
fe9822b
docs(how-to[items]): Show the refused repeat
tony Aug 1, 2026
c025958
pytest_doctest_docutils(fix[dist]): Fill scheduler
tony Aug 1, 2026
e188dd1
tests(dist): Cover the filled-in scheduler
tony Aug 1, 2026
08a9c5b
docs(how-to[items]): Say what fills in a scheduler
tony Aug 1, 2026
2c8d9f5
pytest_doctest_docutils(fix[dist]): Count a multiplier
tony Aug 1, 2026
421e5fd
tests(dist): Cover a multiplied worker spec
tony Aug 1, 2026
8446ce1
doctest_docutils(fix[groups]): Refuse a taken name
tony Aug 1, 2026
10ebf3a
tests(groups): Cover a taken namespace name
tony Aug 1, 2026
a9b4c97
docs(how-to[groups]): Say a name can be taken
tony Aug 1, 2026
76aa012
pytest_doctest_docutils(fix[dist]): Match xdist's expansion
tony Aug 1, 2026
39d1e7a
tests(dist): Pin a count that asks for none
tony Aug 1, 2026
040e3af
doctest_docutils(fix[groups]): Refuse a taken lifted name
tony Aug 1, 2026
4d70bc1
tests(groups): Cover a taken lifted name
tony Aug 1, 2026
33928a6
tests(fixtures): Pin a page-wide fixture
tony Aug 2, 2026
378cf2d
docs(how-to[fixtures]): Name the scope a page is
tony Aug 2, 2026
5e460f8
doctest_docutils(feat[testcode]): Run a prompt-free block
tony Aug 2, 2026
3bb5bca
tests(testcode): Cover a page written to be pasted
tony Aug 2, 2026
efb84d2
docs(how-to[testcode]): Show the prompt-free form
tony Aug 2, 2026
4f2a2bf
doctest_docutils(fix[groups]): Name a prompt-free block for its page
tony Aug 2, 2026
a8c013b
tests(groups): Cover where the two forms meet
tony Aug 2, 2026
70bf8b6
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
Loading