Doc examples — every snippet is tested¶
Every Python code example in the docs is executed in CI. This is not a
convention you have to remember — it is enforced by discovery: a gate scans
every page under docs/ and runs its Python fences, so a new page is covered
the moment it exists. There is no opt-in list. The gate lives in
src/doppler/tests/test_doc_snippets.py; run it locally with:
Why¶
Doc snippets rot silently. The quickstart once showed HalfbandDecimator();
the constructor later gained a required argument, the example started raising
TypeError, and nothing noticed for weeks. Prose examples are the first thing a
new user runs — they must always work.
The four states of a fence¶
Every ```python / ```pycon block in docs/ is in exactly
one of these. They layer: exec proves the snippet still runs; the other
three also prove it still shows the right result.
| State | How | Proves | Use for |
|---|---|---|---|
| exec | plain ```python |
the gate runs it | it runs |
| doctest | a >>> session |
the gate checks output | it runs and the value is right |
| include | --8<-- from a tested .py |
byte-identical to code CI already runs | zero drift, by construction |
| skip | <!-- docs-snippet: skip=REASON --> |
nothing (documents why) | — |
Pseudocode is not a state — write it as ```text so it is out of scope.
Runnable-first — how to choose¶
A skipped block is not tested, so it can rot exactly like an unguarded one.
The whole point of the gate is to run the code, so reach for skip= last, not
first. When a block fails the gate, work down this list — stop at the first that
fits:
--8<--from a tested example script — the gold standard. Almost every gallery page mirrors asrc/doppler/examples/*_demo.pythat already runs in CI; pull the code in and the shown code is the tested code, forever.- exec with one line of real setup — most "fragment" failures are a real
API call one undefined name away from running (
c.steps(rx)whererxis never defined). Definerxfor real (rx = LO(0.05).steps(4096)) and the block genuinely testssteps. Do not invent a fake value that merely silences the error — a block passing against a bogusrxhides drift instead of catching it. - doctest — same, but when the value is the point.
skip=— only when the code genuinely cannot run headless: a blockingrecv(), a hardware source, a two-terminal demo, or a read of a capture file the reader supplies. Asteps()/accumulate()/push()call is not one of these — make it runnable instead.
Anti-pattern: do not add a hidden shared setup namespace so fragments "just run". A block that passes against an injected global you can't see is worse than untested — it looks green while masking a rename. Keep shown code and run code identical; that is what
--8<--guarantees for free.
exec — the default for inline prose¶
Just write a normal fenced block. A page is one notebook: its fences share a
namespace and run top to bottom, so a later block may use names an earlier block
bound. The gate runs in a throwaway working directory (writing a file is safe)
with numpy.random.seed(0) for determinism and a per-block timeout.
doctest — when the value matters¶
>>> import numpy as np
>>> from doppler.source import LO
>>> np.round(LO(0.25).steps(4), 3) # fs/4 tone: 1, j, -1, -j
array([ 1.+0.j, -0.+1.j, -1.-0.j, 0.-1.j], dtype=complex64)
Standard # doctest: directives (+SKIP, +ELLIPSIS, …) are honored — round
or use +ELLIPSIS for values with floating-point noise.
include — zero drift¶
The strongest guarantee: show code that is already tested. The example
scripts in src/doppler/examples/*.py run in CI (make test-examples-python).
Mark a self-contained region (imports included) in the tested script:
import numpy as np
from doppler.source import LO
lo = LO(0.25) # a free-running quarter-rate tone
iq = lo.steps(8) # 8 complex64 samples: 1, j, -1, -j, repeating
Then pull it into the page with a single --8<-- line inside a python fence.
The docs build inlines it — and the gate resolves it too, so the block is
really executed against the same code CI already tests. This very block is live:
import numpy as np
from doppler.source import LO
lo = LO(0.25) # a free-running quarter-rate tone
iq = lo.steps(8) # 8 complex64 samples: 1, j, -1, -j, repeating
The shown code is the tested code, so it cannot drift. Prefer this for new gallery pages and any excerpt whose correctness (not just runnability) matters. Note the region must run standalone — the gate executes exactly the marked lines, so include the imports.
skip — the last resort¶
For blocks that cannot run headless — a blocking network recv(), a hardware
source, a two-terminal demo, an intentionally-wrong example. Put the marker on
the line immediately before the fence. The reason is mandatory (a bare
marker fails the gate), so every exclusion is reviewed in the diff:
A block can also assert it raises: <!-- docs-snippet: raises=ValueError -->.
The burn-down backlog¶
Pages not yet brought under the gate are listed in
docs/.doc-snippet-ignore — a temporary backlog that shrinks to empty, the
same idiom as docs/api/.api-coverage-ignore and scripts/.serializable-ignore.
A new page is never added here; it is gated on arrival. Each run prints
doc-snippet backlog: N page(s) not yet gated. To retire a page, make its
fences pass (or skip=-mark them with reasons) and delete its line.
Building the docs locally (gotchas)¶
The docs toolchain has a few sharp edges — all now guarded, recorded here so they are not rediscovered the hard way:
docs/api.mdis generated by just-makeit, not hand-written — don't delete it (the manifest-drift gate will fail).make docs/docs-serveneed thedocsdependency group (it holds zensical + the material theme). The Makefile targets pass--group docs; a bareuv run zensicalin a dev-only venv renders themeless (no left nav).- A stale local
zensical.tomlshadowsmkdocs.yml(zensical prefers it), silently truncating the nav. Thedocs/docs-servetargetsrm -f zensical.tomlfirst.