Restructured Text Parsers#
Sybil includes a range of parsers for extracting and checking examples from
Restructured Text including the ability to capture previous blocks into a
variable in the namespace
, and skip the evaluation of
examples where necessary.
doctest#
Parsers are included for both classic doctest examples
along with those in doctest
directives.
They are evaluated in the document’s namespace
.
The parsers can optionally be instantiated with
doctest option flags.
Here are some classic doctest examples:
>>> context = 'This'
>>> context
'This'
>>> 1 + 1
2
These could be parsed with the a sybil.parsers.rest.DocTestParser
in the
following configuration:
from sybil import Sybil
from sybil.parsers.rest import DocTestParser
sybil = Sybil(parsers=[DocTestParser()])
If you want to ensure that only examples within a doctest
directive are checked,
and any other doctest examples are ignored, then you can use the
sybil.parsers.rest.DocTestDirectiveParser
instead:
This example will never work:
>>> 1 + 1
3
However, this one will:
.. doctest::
>>> 1 + 1
2
These could be checked with the following configuration:
from sybil import Sybil
from sybil.parsers.rest import DocTestDirectiveParser
sybil = Sybil(parsers=[DocTestDirectiveParser()])
Note
You will have to enable sphinx.ext.doctest
in your conf.py
for Sphinx to render doctest
directives.
Code blocks#
The codeblock parsers extract examples from Sphinx code-block
directives and evaluate them in the document’s namespace
.
The boilerplate necessary for examples to successfully evaluate and be checked
can hinder the quality of documentation.
To help with this, these parsers also evaluate “invisible” code blocks such as this one:
.. invisible-code-block: python
remember_me = b'see how namespaces work?'
These take advantage of Sphinx comment syntax so that the code block will not be rendered in your documentation but can be used to set up the document’s namespace or make assertions about what the evaluation of other examples has put in that namespace.
Python#
Python code blocks can be checked using the sybil.parsers.rest.PythonCodeBlockParser
.
Here’s a Python code block and an invisible Python code block that checks it:
Here's a function:
.. code-block:: python
import sys
def prefix(message):
return 'prefix:'+message
This won't show up but will verify it works:
.. invisible-code-block: python
assert prefix('foo') == 'prefix:foo'
These could be checked with the following configuration:
from sybil import Sybil
from sybil.parsers.rest import PythonCodeBlockParser
sybil = Sybil(parsers=[PythonCodeBlockParser()])
Note
You should not wrap doctest examples in a python
code-block
,
they will render correctly without that and you should use the doctest
parser to check them.
Other languages#
Note
If your code-block
examples define content, such as JSON or YAML, rather than
executable code, you may find the capture parser is more useful.
sybil.parsers.rest.CodeBlockParser
can be used to check examples in any language
you require, either by instantiating with a specified language and evaluator, or by subclassing
to create your own parser.
As an example, let’s look at evaluating bash commands in a subprocess and checking the output is as expected:
.. code-block:: bash
$ echo hi there
hi there
We can do this using CodeBlockParser
as follows:
from subprocess import check_output
from textwrap import dedent
from sybil import Sybil
from sybil.parsers.codeblock import CodeBlockParser
def evaluate_bash(example):
command, expected = dedent(example.parsed).strip().split('\n')
actual = check_output(command[2:].split()).strip().decode('ascii')
assert actual == expected, repr(actual) + ' != ' + repr(expected)
parser = CodeBlockParser(language='bash', evaluator=evaluate_bash)
sybil = Sybil(parsers=[parser])
Alternatively, we can create our own parser class and use it as follows:
from subprocess import check_output
from textwrap import dedent
from sybil import Sybil
from sybil.parsers.codeblock import CodeBlockParser
class BashCodeBlockParser(CodeBlockParser):
language = 'bash'
def evaluate(self, example):
command, expected = dedent(example.parsed).strip().split('\n')
actual = check_output(command[2:].split()).strip().decode('ascii')
assert actual == expected, repr(actual) + ' != ' + repr(expected)
sybil = Sybil([BashCodeBlockParser()])
Capturing blocks#
sybil.parsers.rest.CaptureParser()
takes advantage of Sphinx comment syntax to introduce
a special comment that takes the preceding ReST block and inserts its
raw content into the document’s namespace
using the name specified.
For example:
A simple example::
root.txt
subdir/
subdir/file.txt
subdir/logs/
.. -> expected_listing
This listing could be captured into the namespace
using the following
configuration:
from sybil import Sybil
from sybil.parsers.rest import CaptureParser
sybil = Sybil(parsers=[CaptureParser()])
The above documentation source, when parsed by this parser and then evaluated,
would mean that expected_listing
could be used in other examples in the
document:
>>> expected_listing.split()
['root.txt', 'subdir/', 'subdir/file.txt', 'subdir/logs/']
It can also be used with code-block
examples that define content rather
executable code, for example:
.. code-block:: json
{
"a key": "value",
"b key": 42
}
.. -> json_source
The JSON source can now be used as follows:
>>> import json
>>> json.loads(json_source)
{'a key': 'value', 'b key': 42}
Note
It’s important that the capture directive, .. -> json_source
in this case, has identical
indentation to the code block above it for this to work.
Skipping examples#
sybil.parsers.rest.SkipParser()
takes advantage of Sphinx comment syntax to introduce
special comments that allow other examples in the document to be skipped.
This can be useful if they include pseudo code or examples that can only be
evaluated on a particular version of Python.
For example:
.. skip: next
This would be wrong:
>>> 1 == 2
True
If you need to skip a collection of examples, this can be done as follows:
This is pseudo-code:
.. skip: start
>>> foo = ...
>>> foo(..)
.. skip: end
You can also add conditions to either next
or start
as shown below:
>>> import sys
This will only work on Python 3:
.. skip: next if(sys.version_info < (3, 0), reason="python 3 only")
>>> repr(b'foo')
"b'foo'"
As you can see, any names used in the expression passed to if
must be
present in the document’s namespace
.
invisible code blocks, setup
methods or fixtures are good ways to provide these.
The above examples could be checked with the following configuration:
from sybil import Sybil
from sybil.parsers.rest import DocTestParser, SkipParser
sybil = Sybil(parsers=[DocTestParser(), SkipParser()])
Clearing the namespace#
If you want to isolate the testing of your examples within a single source file, you may want
to clear the namespace
. This can be done as follows:
>>> x = 1
>>> x
1
Now let's start a new test:
.. clear-namespace
>>> x
Traceback (most recent call last):
...
NameError: name 'x' is not defined
The following configuration is required:
from sybil import Sybil
from sybil.parsers.rest import DocTestParser, ClearNamespaceParser
sybil = Sybil(parsers=[DocTestParser(), ClearNamespaceParser()])