Python directivesΒΆ
The Sphinx extension also runs arbitrary Python at build time. The python:* family renders what a block printed, as a code block or as live document content. The matrix directive renders a release compatibility table from a fixed generator.
See also
Both families need the click_extra.sphinx extension enabled, and the python:* one needs the build-time execution opt-in on top. The Sphinx setup covers each step.
python:* directivesΒΆ
Click Extra also adds five general-purpose Python execution directives, registered under a separate python domain (distinct from Sphinxβs built-in py domain for documenting API objects):
Directive |
Purpose |
|---|---|
|
Define and show a Python source block, executed silently. Use it to teach readers what a snippet looks like and to seed imports/variables for follow-up blocks. |
|
Execute a Python block and render its captured |
|
Execute a Python block and parse its captured |
|
Execute a Python block and parse its captured |
|
Execute a Python block and parse its captured |
These complement the Click directives: click:run is for showing simulated CLI sessions; python:run is for showing arbitrary Python output; the python:render* family is for inline content generation, replacing the regenerator-script + marker-region pattern many projects use to keep auto-tables in sync.
Hint
This project eats its own dog food: the ANSI lexer table in pygments.md is rendered live at build time by an inline python:render block that imports LEXER_MAP and prints a Markdown table. Read the exact source lines on GitHub for a real-world example of replacing a regenerator script with a one-block inline build-time computation.
Pick the right renderΒΆ
Directive |
Parser used for captured stdout |
When to use |
|---|---|---|
|
Whatever parser owns the host source file |
Generated markup matches the host file format. |
|
MyST, regardless of host |
Embed MyST-generated content in a |
|
reST, regardless of host |
Embed reST-generated content in a |
python:render reuses the host state machine, so cross-references and Sphinx-aware roles resolve naturally. The forced-parser variants (render-myst, render-rst) parse into a fresh sub-document and graft the resulting nodes back into the page.
python:render: docs as codeΒΆ
Tip
The strongest use case is replacing a docs/docs_update.py script that walks an in-process registry, renders Markdown, and rewrites a region of a .md file between <!-- start --> / <!-- end --> markers. With python:render, the same code lives inline in the page itself and runs at build time. The rendered HTML is always current because the source-of-truth registry is queried on every build.
Render the live list of Pythonβs built-in modules as a Markdown table, executed by Sphinx at build time:
```{python:render}
import sys
print("| Module | Type |")
print("|--------|------|")
for name in sorted(sys.builtin_module_names)[:5]:
print(f"| `{name}` | built-in |")
```
.. python:render::
import sys
print("| Module | Type |")
print("|--------|------|")
for name in sorted(sys.builtin_module_names)[:5]:
print(f"| `{name}` | built-in |")
Renders as a real HTML <table> (output truncated to 5 entries):
Module |
Type |
|---|---|
|
built-in |
|
built-in |
|
built-in |
|
built-in |
|
built-in |
Self-updating source with :mirror:ΒΆ
python:render accepts a :mirror: flag. On top of rendering live, a mirror block keeps a copy of its generated Markdown in the source .md, between two HTML-comment markers directly below the fence. The output stays reviewable in the raw file, in diffs, and on GitHub, which renders the mirrored Markdown even though it never executes the block. This revives the docs_update.py marker-region pattern with the generator inlined into the page: no separate regenerator script, and no drift.
Add :mirror: to a python:render fence:
```{python:render}
:mirror:
from click_extra.table import TableFormat, render_table
print(render_table(
[["Lisbon", "12:00"], ["Denver", "05:00"]],
headers=["City", "Local time"],
table_format=TableFormat.GITHUB,
))
```
Running click-extra refresh-directives on the file inserts the mirrored region below the fence, and refreshes it in place on every later run:
```{python:render}
:mirror:
...
```
<!-- mirror -->
| City | Local time |
| :----- | :--------- |
| Lisbon | 12:00 |
| Denver | 05:00 |
<!-- mirror-end -->
A few properties follow from the mirror being real Markdown:
The mirrored region is the single rendered copy, so the directive emits nothing of its own in mirror mode: otherwise the table would render twice. Add
:show-source:to also show the Python block above the region.Sphinx builds regenerate the region in memory before parsing the page, so the rendered HTML is always fresh, even when the committed region is stale. The build never writes to the source file: the committed copy is refreshed by
click-extra refresh-directives, typically from the same automation that keeps{matrix}blocks current.The
<!-- mirror -->β¦<!-- mirror-end -->pair follows the same marker grammar as the<!-- matrix β¦ -->regions, and a mirror example nested inside a longer code fence (like the ones on this page) is never executed or refreshed.The region is reformatted by
mdformatlike any other Markdown, so a mirror block must printmdformat-canonical Markdown.render_tableinGITHUBmode already does; a hand-built table may be re-aligned by the formatter and then fight the generator.
:mirror: is scoped to python:render in a Markdown host, and shares the click_extra_enable_exec_directives opt-in with the rest of the executing directives.
Hiding the generator with <!-- mirror-src -->ΒΆ
A :mirror: fence renders live and mirrors its output into the source, but the generator fence itself stays visible: on GitHub or PyPI, which show the raw Markdown without running Sphinx, the reader sees the python:render code block above the table. When the output is the whole point (a table or diagram in readme.md) and the generator is just plumbing, the <!-- mirror-src --> comment form moves the generator into an HTML comment, so only its output renders.
The generator Python lives between an opening <!-- mirror-src line and a closing -->, each on its own line:
<!-- mirror-src
from click_extra.table import TableFormat, render_table
print(render_table(
[["Lisbon", "12:00"], ["Denver", "05:00"]],
headers=["City", "Local time"],
table_format=TableFormat.GITHUB,
))
-->
Running click-extra refresh-directives executes the generator and writes its output just below the comment, closed by a <!-- mirror-src-end --> marker:
<!-- mirror-src
...
-->
| City | Local time |
| :----- | :--------- |
| Lisbon | 12:00 |
| Denver | 05:00 |
<!-- mirror-src-end -->
Both markers are HTML comments, so GitHub, PyPI, and any plain Markdown renderer show only the generated table while the generator stays out of sight. Everything else matches the :mirror: fence: Sphinx regenerates the region in memory on each build so the rendered HTML is never stale, the committed copy is refreshed offline by click-extra refresh-directives, the region is reformatted by mdformat (so the generator must print mdformat-canonical Markdown), and an example nested inside a longer code fence (like the two above) is copied verbatim, never executed.
Choose between the two forms by what should be on the page: the :mirror: fence when the generator belongs there, like a docs example teaching python:render itself; the <!-- mirror-src --> comment when the page should read as its output alone, like a readme.md rendered on PyPI.
Cross-format renderingΒΆ
python:render-myst and python:render-rst let a host file embed content authored in the other markup. This page is MyST, but the following block prints reST and parses it as such:
Note
A persimmon must be very ripe to eat raw.
In an rST host, python:render-myst provides the symmetric path: print MyST and have it parsed as MyST regardless of the surrounding .rst file.
Namespace persistenceΒΆ
Like click:source / click:run, the Python runner holds a per-document namespace, so consecutive blocks share imports and variables:
from textwrap import dedent
GREETING = "hello, sphinx"
HELLO, SPHINX
The python:source block ran silently to seed dedent and GREETING; the subsequent python:run referenced both.
The matrix directiveΒΆ
The matrix directive renders a packageβs release compatibility matrix for a given axis. Unlike the click:* and python:* families, it runs a fixed generator rather than user-supplied Python, so it carries no execution surface and is registered without the click_extra_enable_exec_directives opt-in. Two axes are built in:
{matrix} pythonrenders the interpreter matrix (release ranges Γ Python versions).{matrix} <distribution>(like{matrix} click) renders a dependency matrix (release ranges Γ that dependencyβs versions).
The generated table lives in the source, kept current by the offline updater described below, so it shows up in the raw Markdown (and in pull-request diffs) and the HTML build needs no git access (it works on a shallow clone). There are two ways to write it, both refreshed by the same refresh-directives command:
A directive fence,
```{matrix} pythonβ¦```, rendered by Sphinx. Simplest on a docs-only page, but GitHub shows the fenced block as a code block. An empty fence falls back to generating from the git tags at build time, so a freshly authored block renders before its first refresh.A comment marker region,
<!-- matrix python -->β¦<!-- matrix-end -->, with the raw table between the markers. Being plain Markdown, it renders as a real table on GitHub and PyPI as well as in Sphinx. Options go in the start comment askey=valuepairs and bare flags:<!-- matrix click show-spec -->.install.mdβs tables use this form so they render everywhere.
The examples below use the directive fence; the marker form takes the same axis and options.
The python axisΒΆ
This project uses it for the Python compatibility table in install.md. You write the block with just its axis and options:
```{matrix} python
:package: click-extra
```
and the updater fills in the table below the options, regenerated from every vMAJOR.MINOR.PATCH tag (reading the declared Python support from the Programming Language :: Python :: X.Y classifiers in pyproject.toml, falling back to requires-python, Poetryβs python = "...", then setup.pyβs python_requires). Consecutive releases that agree are grouped into one row, and a floor-only declaration is capped at the latest Python released while the range was current:
```{matrix} python
:package: click-extra
| `click-extra` | Released | `3.14` | `3.13` | `3.12` | `3.11` | `3.10` | `3.9` | `3.8` | `3.7` |
| :------------------ | :--------- | :----: | :----: | :----: | :----: | :----: | :---: | :---: | :---: |
| `6.2.x` β `8.x` | 2025-11-04 | β
| β
| β
| β
| β
| β | β | β |
| `6.0.x` β `6.1.x` | 2025-10-08 | β
| β
| β
| β
| β | β | β | β |
| `5.0.x` β `6.0.x` | 2025-05-13 | β | β
| β
| β
| β | β | β | β |
| `4.11.x` β `4.15.x` | 2024-10-08 | β | β
| β
| β
| β
| β | β | β |
| `4.9.x` β `4.10.x` | 2024-07-25 | β | β | β
| β
| β
| β
| β | β |
| `4.0.x` β `4.8.x` | 2023-05-08 | β | β | β
| β
| β
| β
| β
| β |
| `0.0.x` β `3.10.x` | 2021-10-18 | β | β | β | β
| β
| β
| β
| β
|
```
Three states, two sourcesΒΆ
A release declares its Python support twice, and the two declarations answer different questions. The classifier list is what the project claims to have tested. requires-python is what an installer enforces: fall outside it and pip refuses to install, whatever the classifiers say. The matrix keeps them apart:
Cell |
Meaning |
|---|---|
β |
Declared, via a |
β |
Ruled out by |
β |
Neither. The release never claimed that version, and nothing in its metadata stops you. |
The third state is what a two-state table has to lie about. When 4.9.0 shipped in July 2024 it declared requires-python = ">= 3.9" with classifiers up to 3.12, and Python 3.13 did not exist yet. Marking that cell β would assert an incompatibility nobody ever declared, so it renders β instead, while 3.8 stays β because the >= 3.9 floor genuinely rules it out.
The result reads as a staircase: β fills the lower-left as the floor rises over the years, β
the middle band, and β the upper-right where the future had not happened yet.
A dependency axisΒΆ
{matrix} <distribution> tracks a runtime dependency instead. For each release range it reads that distributionβs requirement specifier (PEP 621, Poetry, or setup.py) and marks β
/ β for each column version with packaging. An extras bracket and an environment marker are both transparent: tabulate[widechars]>=0.9 and tomli>=2; python_version<'3.11' each track the plain >= range. The distribution is matched on its PEP 503 normalized name, looked up in the runtime dependencies then in those behind an extra. Development dependency groups (PEP 735) are skipped, since no installer resolves them for a consumer. Columns are auto-derived: a minor series stays a single X.Y column unless an open (>=) floor pins a specific patch, in which case it splits into X.Y.0 plus that floor; the left edge is the version resolved in uv.lock. Add :show-spec: for a Spec column with each rangeβs raw specifier, in the releaseβs own spelling. Cells here stay two-valued: unlike Python, a dependency has no informational second declaration to disagree with its specifier, so there is nothing an undeclared cell could mean.
Poetryβs own range syntax is translated to PEP 440 before evaluation, since a projectβs older tags usually predate its move to PEP 621. Carets follow Poetryβs rule of bumping the leftmost non-zero component, so ^1.2.3 caps at 2.0.0 while ^0.2.3 caps at 0.3.0 and ^0.0.3 at 0.0.4: under a 0. prefix every release may break, and a caret there covers far less than the major series. Tilde and wildcard ranges (~1, ~1.2, 1.*, 1.2.*) translate the same way.
This project uses it for the Click compatibility table:
```{matrix} click
:package: click-extra
:show-spec:
| `click-extra` | Released | Spec | `8.4.2` | `8.4.1` | `8.4.0` | `8.3.3` | `8.3.1` | `8.3.0` | `8.2` | `8.1` | `8.0` |
| :------------------ | :--------- | :-------- | :-----: | :-----: | :-----: | :-----: | :-----: | :-----: | :---: | :---: | :---: |
| `8.x` | 2026-06-22 | `>=8.3.1` | β
| β
| β
| β
| β
| β | β | β | β |
| `7.17.x` β `7.20.x` | 2026-05-25 | `>=8.4.1` | β
| β
| β | β | β | β | β | β | β |
| `7.15.x` β `7.16.x` | 2026-05-03 | `>=8.3.1` | β
| β
| β
| β
| β
| β | β | β | β |
| `7.14.1` | 2026-04-26 | `>=8.1` | β
| β
| β
| β
| β
| β
| β
| β
| β |
| `7.14.0` | 2026-04-24 | `>=8.3.3` | β
| β
| β
| β
| β | β | β | β | β |
| `7.0.x` β `7.13.x` | 2025-11-17 | `>=8.3.1` | β
| β
| β
| β
| β
| β | β | β | β |
| `6.x` | 2025-09-25 | `>=8.3.0` | β
| β
| β
| β
| β
| β
| β | β | β |
| `5.x` | 2025-05-13 | `~=8.2.0` | β | β | β | β | β | β | β
| β | β |
| `4.9.x` β `4.15.x` | 2024-07-25 | `~=8.1.4` | β | β | β | β | β | β | β | β
| β |
| `1.7.x` β `4.8.x` | 2022-03-31 | `^8.1.1` | β
| β
| β
| β
| β
| β
| β
| β
| β |
| `0.0.x` β `1.6.x` | 2021-10-18 | `^8.0.2` | β
| β
| β
| β
| β
| β
| β
| β
| β
|
```
OptionsΒΆ
Option |
Effect |
Default |
|---|---|---|
|
Header column label, rendered in backticks. |
repository folder name |
|
Git working tree to walk, absolute or relative to the documented projectβs root. |
projectβs git root |
|
Drop release rows below this package version. |
none (all tags) |
|
Regex selecting release tags. |
|
|
Left-to-right ordering of the version columns: |
|
|
Top-to-bottom ordering of the release rows: |
|
|
( |
none (all columns) |
|
(dependency axis) Add a |
off |
The :path: option makes the directive reusable across repositories: point it at a sibling checkout to render another packageβs matrix.
Keeping the tables currentΒΆ
The embedded tables are refreshed offline, formatter-style, by the refresh-directives command (which needs the sphinx extra):
$ click-extra refresh-directives docs/
It walks the given Markdown files or directories, regenerates each matrix blockβs table (both the {matrix} directive fences and the <!-- matrix β¦ --> marker regions) from that blockβs axis, options, and the projectβs git tags, and rewrites the block in place. Pass --check to write nothing and exit non-zero when a block is stale, so a CI job or pre-commit hook can fail on an out-of-date matrix. The same logic is importable as click_extra.sphinx.matrix.update_matrix_blocks(paths, check=...). A block whose generation fails (missing git binary, non-repository :path:, no matching data) is left untouched, so a transient failure never wipes a good table. Examples nested inside longer code fences (like the ones on this page) are documented illustrations and are never refreshed.
The same command also refreshes the python:render :mirror: regions found in the same files, in both the visible fence and the invisible <!-- mirror-src --> comment forms, by executing each blockβs Python (click_extra.sphinx.python.update_mirror_blocks(paths, check=...) is the importable form). One invocation therefore keeps every self-updating block of a documentation tree current, whatever its kind.
Note
Only the updater (and the empty-block fallback) needs the release tags, since it is the part that shells out to git. Run it wherever the full tag history is available. The HTML build renders the embedded table verbatim and needs no git access, so shallow clones and read-only build hosts render the matrix fine.
For content a directive cannot produce on its own, like a shared registry dumped into several files or an external generatorβs output, the same marker machinery is exposed as three primitives, importable from click_extra.sphinx:
marker_res(name)builds the(open, close)regexes of a<!-- name β¦ -->/<!-- name-end -->region, the grammar every self-updating marker shares.replace_region(text, name, content)swaps the body between those markers forcontent, keeping the markers so the region round-trips. It returns the text unchanged when either marker is absent, so it is safe to fan out over files that do not all carry the region.update_blocks(paths, rewrite, check=...)applies arewrite(text, path)callback to every Markdown file underpaths, writing back only the ones it changed (or, undercheck, returning the ones it would change). It is the read-rewrite-report loop behind bothupdate_matrix_blocksandupdate_mirror_blocks.
replace_region is the counterpart to those two refreshers for content that originates outside the document rather than from an inline directive.