Quickstart
Installation
structured-tutorials is published on PyPI and
thus can simply be installed via pip (or any other package management tool, such as
uv):
$ pip install structured-tutorials
To render your tutorials in Sphinx, add the extension and, optionally, where your tutorials are stored (this can be outside of the Sphinx root):
extensions = [
# ... other extensions
"structured_tutorials.sphinx",
]
# Location where your tutorials are stored (default: same directory as conf.py).
#structured_tutorials_root = Path(__file__).parent / "tutorials"
Editor integration
The JSON schema is can be found here and can be used in IDEs for auto-completion and validation. For example:
Your first tutorial
To get started with the simplest possible tutorial, create a minimal configuration file:
parts:
- commands:
- command: structured-tutorial --help
doc:
output: |
usage: structured-tutorial [-h] path
...
- command: echo "Finished running example tutorial."
doc:
text_before: "This shows your first tutorial:"
You can run this tutorial straight away:
user@host:~/example/$ structured-tutorial docs/tutorials/quickstart/tutorial.yaml
Running part 0...
+ structured-tutorial --help
usage: structured-tutorial [-h] path
...
+ echo "Finished running example tutorial."
Finished running example tutorial.
Finally, you can render the tutorial in your Sphinx tutorial:
Configure the tutorial that is being displayed - this will not show any output:
.. structured-tutorial:: quickstart/tutorial.yaml
.. structured-tutorial-part::
In fact, above lines are included below, so this is how this tutorial will render in your documentation:
Configure the tutorial that is being displayed - this will not show any output:
This shows your first tutorial:
user@host:~$ structured-tutorial --help
usage: structured-tutorial [-h] path
...
user@host:~$ echo "Finished running example tutorial."
A more advanced example
The above is nice but not very useful. Let’s show you a few new cool features next.
Commands, output, file contents and many other parts are rendered using Jinja templates. This allows you to reduce repetition of and use different values for documentation and runtime.
Tutorials can create files (that are shown appropriately in your documentation). The tutorial below shows you how to create a JSON file that shows up with proper syntax highlighting.
You can test success of a command by checking the status code, output or even if a TCP port was
opened. When checking the output, you can use regular expressions for matching and even named patterns to
update the context with runtime data. Below we create a temporary directory with mktemp and use it
later to create the file in it.
The following example will create a directory, writes to a file in it and outputs its contents:
configuration:
context: # context shared between documentation and runtime
filename: example.json
doc: # template context at runtime
context:
dest: /tmp/tmp...
contents: '{"key": "doc"}'
run: # template context at runtime
context:
# dest is captured and added to the context by mktemp at runtime
contents: '{"key": "run"}'
parts:
- id: create-directory
commands:
- command: mktemp -d
doc:
output: "{{ dest }}"
run:
show_output: false
test:
# Capture the whole output and add it to the context
- regex: (?P<dest>.*)
cleanup:
# Remove temporary directory after running the tutorial
- command: rm -rf {{ dest }}
- id: create-file
contents: "{{ contents }}\n"
destination: "{{ dest }}/{{ filename }}"
doc:
language: json
# If you use sphinxcontrib-spelling, make sure the filename is not spell-checked
ignore_spelling: true
- commands:
- command: cat {{ dest }}/{{ filename }}
doc:
output: "{{ contents }}"
- command: cat {{ dest }}/{{ filename }} | python -m json.tool
doc:
output: ...
Render this tutorial
The code in your reStructuredText doesn’t look much different. You render three parts, and the first two reference the id you have given them in the YAML file.
.. structured-tutorial:: templates/tutorial.yaml
First, create a temporary directory:
.. structured-tutorial-part:: create-directory
Then create a JSON file in it:
.. structured-tutorial-part:: create-file
You can verify the contents of the file like this:
.. structured-tutorial-part::
The above file is included below.
First, create a temporary directory:
user@host:~$ mktemp -d
/tmp/tmp...
Then create a JSON file in it:
{"key": "doc"}
You can verify the contents of the file like this:
user@host:~$ cat /tmp/tmp.../example.json
{"key": "doc"}
user@host:~$ cat /tmp/tmp.../example.json | python -m json.tool
...
Run this tutorial
When running this tutorial, it’ll do what you instructed the user to do: Create a temporary directory, then a JSON file in it, and then output it. Cleanup is assured through the cleanup directive, even if one of the commands would fail:
user@host:~/example/$ structured-tutorial docs/tutorials/templates/tutorial.yaml
Running part create-directory...
+ mktemp -d
Running part create-file...
Running part 2...
+ cat /tmp/tmp.6G6S9dX0MN/example.txt
{"key": "run"}
+ cat /tmp/tmp.6G6S9dX0MN/example.txt | python -m json.tool
...
INFO | Running cleanup commands.
+ rm -r /tmp/tmp.6G6S9dX0M
Generating documentation out of the tutorial
Long commands wrap automatically
When rendering a tutorial, long commands wrap automatically. With the following YAML file:
parts:
- commands:
# This command is very short
- command: docker run --rm -it ubuntu:24.04
# But a longer command will wrap automatically
- command: docker run --rm -it -v `pwd`:/very/long/path -e DEMO=value ubuntu:24.04
# Use '>' to break multiline strings in your YAML file, but still wrap commands
# automatically. Any newlines in your YAML will be treated as a single space
# instead:
- command: >
docker run --rm -it -v `pwd`:/very/long/path -e DEMO=value
ubuntu:24.04
echo "Run a very long command"
# You can also use a "\" to force line breaks when rendering:
- command: >
docker run --rm -it \
-v `pwd`:/example \
-e DEMO=value \
ubuntu:24.04 \
echo "Run a command"
# show how single options will never split, see how "-e DEMO=..." is in one line.
- commands:
- command: docker run --rm -it -v `pwd`:/very/long/path -e DEMO=very-long-value ubuntu:24.04
you will get:
user@host:~$ docker run --rm -it ubuntu:24.04
user@host:~$ docker run --rm -it -v `pwd`:/very/long/path -e DEMO=value ubuntu:24.04
user@host:~$ docker run --rm -it -v `pwd`:/very/long/path -e DEMO=value ubuntu:24.04 echo \
> "Run a very long command"
user@host:~$ docker run --rm -it \
> -v `pwd`:/example \
> -e DEMO=value \
> ubuntu:24.04 \
> echo "Run a command"
Note that single-value-options and respective values do not split by default, so -e DEMO=value will never split between option argument and value.
Single-character options will not be split from their respective value:
user@host:~$ docker run --rm -it -v `pwd`:/very/long/path -e DEMO=very-long-value \
> ubuntu:24.04
Show the user alternatives
Sometimes you want to present the user with different options when following a tutorial. For example, you might want to show a user how to set up your web application using either PostgreSQL or MySQL.
structured-tutorials supports alternatives. They render as tabs in documentation, but when running a tutorial, the user has to specify an alternative. Alternatives can contain either commands or files (and you could even mix them):
configuration:
doc:
# Optional: Specify additional configuration for each alternative
alternatives:
mariadb:
name: MariaDB
context:
package: mariadb-server
postgresql:
name: PostgreSQL
context:
package: postgresql
# you can also update configuration at runtime
run:
alternatives:
mariadb:
context:
dbclient: mysql
postgresql:
context:
dbclient: psql
parts:
# part 1: install database backend
- alternatives:
# We use YAML anchors here to reduce duplication. "{{ package }}" is the name of the
# correct package.
mariadb: &install-package
commands:
- command: sudo apt install {{ package }}
postgresql: *install-package
# Skip installation of packages at runtime (we don't want the tutorial
# to require sudo)
run: false
# part 2: configure some application for that database
- alternatives:
mariadb:
contents: "dbtype: mariadb"
destination: "configuration.yaml"
doc:
language: yaml
postgresql:
contents: "dbtype: postgresql"
destination: "configuration.yaml"
doc:
language: yaml
The first part will show the user how to install the respective database backend:
user@host:~$ sudo apt install mariadb-server
user@host:~$ sudo apt install postgresql
The second part will show the user how to configure your application for the respective database backend.
Note that this example of course omits configuring the database itself or any other details.
Running the tutorial
Verify success of commands
You can verify the success of commands by checking the status code, the output or even test if a port is opened properly. You can add multiple tests, and when testing the output, update the context for successive commands.
See Test commands for more information.
Select alternatives
If your tutorial contains alternatives (see Show the user alternatives), you must select one of them when running your tutorial. You wouldn’t normally install both PostgreSQL and MariaDB, for example:
user@host:~$ structured-tutorials -a postgresql ...
Ask the user for feedback
When running a tutorial, you can prompt the user to inspect the current state. You can ask the user to just press “enter” or even to confirm that the current state looks okay (with answering “yes” or “now”).
When rendering a tutorial, prompt parts are simply skipped.
As an example:
configuration:
run:
# Switch to temporary directory before running the tutorial
temporary_directory: true
parts:
- commands:
- command: echo "some content" > test.txt
- command: echo "About to give you two prompts..."
# Just prompt the user to hit 'enter' at this point:
- prompt: "Hit enter to continue... "
# Ask the user to confirm the current state - if they answer "no", the tutorial will abort.
# In this case, we output the current working directory and ask the user to confirm:
- prompt: |-
Current working directory is {{ cwd }} ...
Is the current state satisfactory (y/n)?
response: confirm
# Set to false to also raise an error if the user just presses enter:
#default: true
# You can also overwrite the error message the user will get:
#error: Custom error encountered.
In Sphinx, you can call structured-tutorial-part only twice, as prompts are simply skipped. The first
part just creates a file. Since temporary_directory: true in the configuration, this will run in
a temporary directory that is removed after running the tutorial:
user@host:~$ echo "some content" > test.txt
user@host:~$ echo "About to give you two prompts..."
When running the tutorial, the user will now be prompted to confirm the current state. The prompt would
contain the current working directory. Presumably, the user would check the contents of test.txt in that
directory.
Prevent shell injection
You may also specify commands as lists to prevent shell injection. Note that every word of your command is still rendered as template:
configuration:
doc:
context:
value: example
parts:
- commands:
- command: [echo, "value with spaces"]
- command: [echo, "tokens are also templates: {{ value }}"]
user@host:~$ echo 'value with spaces'
user@host:~$ echo 'tokens are also templates: example'