sphinx-pyrunblock
==================
.. image:: ../figs/pyrunblock-logo.svg
:align: center
:width: 350
:alt: 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:
.. runblock:: pycon
>>> for i in range(5):
... print(i)
.. toctree::
:maxdepth: 2
:caption: Contents:
configuration
about
Installation
------------
.. code-block:: console
$ pip install sphinx-pyrunblock
Enable the extension by adding it to the ``extensions`` list in ``conf.py``:
.. code-block:: python
extensions = [
"sphinx_pyrunblock",
]
See :doc:`configuration` for the full ``conf.py`` reference.
Worked examples
----------------
.. runblock:: pycon
>>> 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)
>>> 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
>>> T = trnorm(T)
>>> linalg.det(T[:3,:3]) - 1 # once more a valid SE(3)
.. runblock:: pycon
>>> from spatialmath.base import qconj, qprint
>>> q = [1, 2, 3, 4]
>>> qprint(qconj(q))
Per-block options
------------------
Options are given on the directive itself, and apply to that one block only
(compare :doc:`configuration` for options that apply document-wide, via
``conf.py``):
``:linenos:``
Show line numbers in the rendered code block.
``:include: start-end``
Only render lines in the given range of the block's *output* (line
numbers count the block's own statements, not the configured
``runfirst`` code, which never appears in output).
``:exclude: start-end``
Skip lines in the given range, same numbering as ``:include:``.
``:numpy:``
Prepend ``import numpy as np`` before the block runs.
``:scipy:``
Prepend ``import scipy as sp`` before the block runs.
``:smtb:``
Append ``from spatialmath import *`` before the block runs.
``:precision: N``
Append ``np.set_printoptions(precision=N)`` before the block runs.
Requires numpy to already be imported (via ``:numpy:`` or a configured
``runfirst`` -- see :doc:`configuration`).
More examples
-------------
.. runblock:: pycon
:include: 5-10
>>> from spatialmath.base import getunit
>>> import numpy as np
>>> getunit(1.5, 'rad')
>>> getunit(90, 'deg')
>>> getunit(90, 'deg', vector=False) # force a scalar output
>>> getunit(1.5, 'rad', dim=0) # check argument is scalar
>>> getunit(1.5, 'rad', dim=3) # check argument is a 3-vector
>>> getunit([1.5], 'rad', dim=1) # check argument is a 1-vector
>>> getunit([1.5], 'rad', dim=3) # check argument is a 3-vector
>>> getunit(90, 'deg')
>>> getunit([90, 180], 'deg')
>>> getunit(np.r_[0.5, 1], 'rad')
>>> getunit(np.r_[90, 180], 'deg')
>>> getunit(np.r_[90, 180], 'deg', dim=2)
>>> getunit([90, 180, 270], 'deg', dim=3)
For any construct with an indented body (``for``, ``while``, ``with``), it's
important to put a blank line on the end. That will be stripped off and
won't appear in the output.
.. runblock:: pycon
:numpy:
>>> from spatialmath.base import getunit
>>> getunit(1.5, 'rad')
>>> try:
>>> getunit(1.5, 'rad', dim=0)
>>> except Exception as e:
>>> print(f"EXCEPTION {e}")
>>>
>>> for i in range(5):
>>> print(i)
>>>
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:
.. runblock:: pycon
a = 2
print(a**3)
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:
.. runblock:: pycon
:no-prompt:
a = 2
print(a**3)
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.
Other code documentation tools
==============================
.. image:: ../figs/autolink.png
:align: left
:width: 500
:alt: autolink example
.. raw:: html
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:``.
.. code-block:: console
$ pip install sphinx-copybutton
.. code-block:: python
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.
.. code-block:: console
$ pip install sphinx-codeautolink
.. code-block:: python
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:
.. code-block:: text
>>> img.metadata('FocalLength')
!! [RUNBLOCK-ERROR] machinevisiontoolbox/ImageIO.py:236
KeyError: 'FocalLength'
The build log entry is more detailed:
.. code-block:: text
!! [RUNBLOCK-ERROR] source/numpy.rst:98
ModuleNotFoundError: No module named 'machinevisionToolbox'
Traceback (most recent call last):
File "", line 1, in
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:
.. code-block:: text
>>> 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 :doc:`configuration`) to turn that
into a real, CI-visible build failure instead:
.. code-block:: python
autorun_languages = {}
autorun_languages["pycon_fail_on_error"] = True
.. code-block:: text
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.