sphinx-pyrunblock¶
sphinx-pyrunblock is a Sphinx extension that executes code from a
runblock directive and inserts the real output into your documentation.
For example:
.. runblock:: pycon
>>> for i in range(5):
... print(i)
renders in the document as:
>>> for i in range(5):
... print(i)
0
1
2
3
4
Contents:
Installation¶
$ pip install sphinx-pyrunblock
Enable the extension by adding it to the extensions list in conf.py:
extensions = [
"sphinx_pyrunblock",
]
See Configuration for the full conf.py reference.
Worked examples¶
>>> from spatialmath.base import trnorm, troty
>>> from numpy import linalg
>>> T = troty(45, 'deg', t=[3, 4, 5])
>>> linalg.det(T[:3,:3]) - 1 # is a valid SO(3)
np.float64(0.0)
>>> T = T @ T @ T @ T @ T @ T @ T @ T @ T @ T @ T @ T @ T
>>> linalg.det(T[:3,:3]) - 1 # not quite a valid SE(3) anymore
np.float64(-3.3306690738754696e-16)
>>> T = trnorm(T)
>>> linalg.det(T[:3,:3]) - 1 # once more a valid SE(3)
np.float64(0.0)
>>> from spatialmath.base import qconj, qprint
>>> q = [1, 2, 3, 4]
>>> qprint(qconj(q))
1.0000 < -2.0000, -3.0000, -4.0000 >
' 1.0000 < -2.0000, -3.0000, -4.0000 >'
Per-block options¶
Options are given on the directive itself, and apply to that one block only
(compare Configuration for options that apply document-wide, via
conf.py):
:linenos:Show line numbers in the rendered code block.
:include: start-endOnly render lines in the given range of the block’s output (line numbers start at 1 and count the block’s own statements, not the configured
runfirstcode, which never appears in output).:exclude: start-endSkip lines in the given range, same numbering as
:include:.:session: [NAME]Share one namespace with the other blocks of the same session in this document, so a block can use the variables, functions and classes defined by earlier ones. See Persistent sessions.
:numpy:Prepend
import numpy as npbefore the block runs.:scipy:Prepend
import scipy as spbefore the block runs.:smtb:Append
from spatialmath import *before the block runs.:precision: NAppend
np.set_printoptions(precision=N)before the block runs. Requires numpy to already be imported (via:numpy:or a configuredrunfirst– see Configuration).
More examples¶
>>> getunit(90, 'deg', vector=False) # force a scalar output
np.float64(1.5707963267948966)
>>> getunit(1.5, 'rad', dim=0) # check argument is scalar
array([1.5])
>>> getunit(1.5, 'rad', dim=3) # check argument is a 3-vector
!! [RUNBLOCK-ERROR] source/index.rst:109
ValueError: for dim==0 input must be a scalar
>>> getunit([1.5], 'rad', dim=1) # check argument is a 1-vector
array([1.5])
>>> getunit([1.5], 'rad', dim=3) # check argument is a 3-vector
!! [RUNBLOCK-ERROR] source/index.rst:109
ValueError: incorrect vector length: expected 3, got 1
>>> getunit(90, 'deg')
array([1.5708])
A statement with an indented body (for, while, with, def,
class, …) ends at the first following line that is not indented, so
you don’t need a blank line after it:
>>> class Point:
... pass
>>> p = Point()
A blank line after the body (or a bare >>> or ...) is still accepted,
as in a real console, and is stripped off so it won’t appear in the output.
else, elif, except and finally clauses continue the preceding
statement as you’d expect.
>>> from spatialmath.base import getunit
>>> getunit(1.5, 'rad')
array([1.5])
>>> try:
... getunit(1.5, 'rad', dim=0)
... except Exception as e:
... print(f"EXCEPTION {e}")
array([1.5])
>>> for i in range(5):
... print(i)
0
1
2
3
4
Lines ending with # ignore are executed but not shown in the rendered
output – useful for setup code that would otherwise clutter the example.
The REPL prompt (>>>) is actually optional, and can be omitted if you are cutting
and pasting chunks of code from elsewhere:
.. runblock:: pycon
a = 2
print(a**3)
will render as:
>>> a = 2
>>> print(a**3)
8
Note that indentation cannot be expressed in this mode since all white space is stripped from the start of each line. If you want to include indented code, use the REPL prompt style.
If you prefer the examples to look like a script rather than a REPL session you can
suppress the output prompts and continuation lines by using the :no-prompt: option:
.. runblock:: pycon
:no-prompt:
a = 2
print(a**3)
will render as:
a = 2
print(a**3)
# → 8
Block output appears as a comment line to clearly distinguish it from the lines of input code. This means you can cut and paste the code into a Python interpreter and it will run without modification.
Persistent sessions¶
By default every runblock is isolated: it starts with an empty
namespace, so a variable defined in one block does not exist in the next.
That is the right default for independent examples, but it makes a narrative
awkward – prose interleaved with code that builds up a result step by step.
Without sessions the only way to do that is to repeat all of the earlier
lines in every block and hide them with :exclude:, which is wasteful and
means a change to the package can force edits in every block.
The :session: option makes blocks share one namespace instead. Each
block carries on where the previous one stopped:
.. runblock:: pycon
:session:
>>> readings = [20, 19, 21]
>>> len(readings)
``readings`` is still there in the next block.
.. runblock:: pycon
:session:
>>> readings.append(22)
>>> sum(readings) / len(readings)
renders as:
>>> readings = [20, 19, 21]
>>> len(readings)
3
readings is still there in the next block, along with anything else
defined so far, so the block below can use it without redefining it.
>>> readings.append(22)
>>> len(readings)
4
>>> sum(readings) / len(readings)
20.5
Functions and classes carry over in the same way, as do imports. Here we define a function, and then in a later block call it:
>>> import statistics
>>> def spread(values):
... return max(values) - min(values)
Nothing is output by that block, but the name spread and the module
statistics now exist in the session:
>>> spread(readings)
3
>>> statistics.mean(readings) == sum(readings) / len(readings)
True
Every block above this point in the section was part of one session, in the order they appear in the document. The text between them is ordinary reST, and may contain anything you like.
Named sessions¶
:session: on its own joins the document’s unnamed session. Give it a
name to keep several sessions going side by side in the same document, for
example to compare two approaches, with their blocks interleaved in any order:
>>> temperature = 20.0
>>> temperature = 293.15
The two sessions each have their own temperature. Changing one:
>>> temperature += 5
>>> temperature
25.0
does not disturb the other:
>>> temperature
293.15
The unnamed session, and each named one, are separate namespaces.
Line numbers¶
With :linenos: the numbering continues through the session, so the
blocks of a narrative read as one listing that starts at line 1. Only the
lines that are actually displayed are counted – lines hidden by
:include:, :exclude: or a trailing # ignore do not use up
numbers, so what the reader sees is always numbered consecutively. This
includes output lines, as it does for a block outside a session.
1>>> a = 3
2>>> b = 4
3>>> a * b
412
5>>> (a**2 + b**2) ** 0.5
65.0
Hiding lines¶
:include: and :exclude: still select among the statements of
that block, numbered from 1, and a trailing # ignore still hides
a line, as before. You rarely need them for the old repeat-and-hide pattern though:
state carries forward, so there is nothing to repeat.
Scope¶
A session lives within one document. Sphinx re-reads only the documents that changed, in no particular order, so if state flowed from one document to another the output of a page would depend on which pages happened to be rebuilt before it. Within a document the blocks always run afresh, top to bottom, in the order written, so the result is deterministic and a partial rebuild gives the same output as a full one.
The same session name used in two documents gives two unrelated sessions.
Code that every document needs belongs in <lang>_runfirst.
When something goes wrong¶
If a statement in an early block fails, the names it should have defined
are missing, and later blocks of the session usually report NameError
as a consequence. The error markers appear in the rendered output at the
point where each failed, and the first one is the cause, just as when you
run the code at a console. Set <lang>_fail_on_error (see
Error reporting) to have the build stop at the first failure
and report its .rst file and line.
Other code documentation tools¶
Note the copy/paste button in the top right corner of the code block, the links on the names in the code and the hoverbox showing object type. These are provided by two other Sphinx extensions.
Easy copy-paste of code¶
Copying text straight out of a rendered code block normally picks up the
>>> ``/… `` prompts and any output lines along with it, so pasting it
back into a file or interpreter either fails outright or pastes lines you
never wanted. The sphinx-copybutton extension adds a
copy-to-clipboard button to every code block (see above) and can be configured to strip
prompts on copy, so a reader always gets clean, runnable code regardless of
whether the block was written with prompts or :no-prompt:.
$ pip install sphinx-copybutton
extensions = [
"sphinx_pyrunblock",
"sphinx_copybutton",
]
# Strip >>> / ... prompts (and shell $ prompts) when the copy button is
# used; keep output lines in the copied text rather than filtering them
# out (also representative of what Peter's other toolboxes use).
copybutton_prompt_text = r">>> |\.\.\. |\$ "
copybutton_prompt_is_regexp = True
copybutton_only_copy_prompt_lines = False
copybutton_remove_prompts = True
A :no-prompt: block has no prompts for copybutton to strip in the first
place, so it’s already copy-paste-ready without any of this – the button
is mainly what makes REPL-style blocks pasteable too.
Cross-linking names to their documentation¶
sphinx-pyrunblock only executes code and renders output – it doesn’t
know anything about turning the names in an example into links. That’s a
separate, complementary extension:
sphinx-codeautolink. It
statically analyses each code block’s AST, tracks types through variable
assignments and chained method calls, and:
hyperlinks each resolved name to its
autodoc-generated (or intersphinx) documentation page,adds a cursor hoverbox showing the type of the variable name under the cursor.
The two extensions don’t know about each other, and don’t need to –
sphinx-codeautolink works by scanning the built HTML for code blocks
by CSS class (highlight-pycon, highlight-python, etc.), matched
against source it separately parsed from the doctree. Since runblock
sets the block’s language to pycon (or python, under
:no-prompt:), any runblock example gets cross-linked automatically
once sphinx-codeautolink is installed and enabled – no special
integration required on either side.
$ pip install sphinx-codeautolink
extensions = [
"sphinx_pyrunblock",
"sphinx_codeautolink",
]
from sphinx_codeautolink import clean_pycon
# "python" (used by :no-prompt: blocks) is recognised natively; pycon
# console syntax needs its prompts cleaned before the block can be
# parsed as Python, hence this entry.
codeautolink_custom_blocks = {"pycon": clean_pycon}
Require sphinx-codeautolink>=0.19.0. Earlier versions don’t resolve
Self-typed return annotations (PEP 673), which is the standard return
annotation for fluent/factory-style methods (obj = Klass.create(...))
– without the fix, cross-linking silently stops for anything chained off
a call to one of those, with no warning by default.
Error reporting¶
If the code raises an exception, a diagnostic marker is written to the Sphinx build log and a short error summary is embedded in the rendered output:
>>> img.metadata('FocalLength')
!! [RUNBLOCK-ERROR] machinevisiontoolbox/ImageIO.py:236
KeyError: 'FocalLength'
The build log entry is more detailed:
!! [RUNBLOCK-ERROR] source/numpy.rst:98
ModuleNotFoundError: No module named 'machinevisionToolbox'
Traceback (most recent call last):
File "<input>", line 1, in <module>
ModuleNotFoundError: No module named 'machinevisionToolbox'
The marker line appears before the traceback so failures are easy to
scan in a long build log. The source location (file:line) refers to the
.rst file, or the Python source file containing the docstring where the
runblock directive lives.
A syntax error in the code block is reported the same way:
>>> print(("Hello, world")
!! [RUNBLOCK-ERROR] source/api.rst:42
SyntaxError: '(' was never closed
By default the build itself still succeeds – the marker above is the
only sign anything went wrong, which means a broken example (a typo, a
missing optional dependency) can slip into published docs unnoticed. Set
pycon_fail_on_error = True (see Configuration) to turn that
into a real, CI-visible build failure instead:
autorun_languages = {}
autorun_languages["pycon_fail_on_error"] = True
sphinx.errors.SphinxError: runblock failed executing code at source/api.rst:42
The build log still has the full traceback above the SphinxError, so
the file:line in the exception message is a pointer back into your
.rst source, not the only diagnostic you get. This defaults to
False so existing docs with a runblock that’s deliberately
demonstrating an error as its expected output keep building.
Parallel build warning¶
Sphinx may print:
WARNING: the sphinx_pyrunblock extension is not safe for parallel reading
This is expected and can be ignored. Running a code block’s output means
temporarily redirecting process-wide state (sys.stdout, file
descriptor 1), which isn’t safe to do from multiple threads at once, so
the extension declares itself parallel-unsafe and Sphinx falls back to
reading serially. The build result is unaffected – only the reading
phase loses parallelism, and only when sphinx-build -j is used.