Myst Parsers#
Sybil includes a range of parsers for extracting and checking examples from MyST including the ability to skip the evaluation of examples where necessary.
doctest#
A selection of parsers are included that can extract and check doctest examples in
python
fenced code blocks,
MyST code-block
directives and
MyST doctest
directives.
Most cases can be covered using a sybil.parsers.myst.PythonCodeBlockParser
.
For example:
A fenced code block:
```python
>>> x = 1+1
>>> x
2
```
A doctest in a MyST `code-block` directive:
```{code-block} python
>>> x += 1
```
All three examples in the two blocks above can be checked with the following configuration:
from sybil import Sybil
from sybil.parsers.myst import PythonCodeBlockParser
sybil = Sybil(parsers=[PythonCodeBlockParser()])
Alternatively, the ReST doctest parser will find all doctest examples in a Markdown file. If any should not be checked, you can make use of the skip parser.
Note
You can only use the ReST doctest parser if no doctest
examples are contained in examples parsed by the other parsers listed here.
If you do, ValueError
exceptions relating to overlapping regions will be raised.
doctest
directive#
If you have made use of MyST doctest
directives
such as this:
A MyST `doctest` directive:
```{doctest}
>>> x = 3
>>> x
3
```
You can use the sybil.parsers.myst.DocTestDirectiveParser
as follows:
from sybil import Sybil
from sybil.parsers.myst 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.
eval-rst
directive#
If you have used ReST doctest
directive inside a MyST eval-rst
directive such as this:
A MyST `eval-rst` directive:
```{eval-rst}
.. doctest::
>>> 1 + 1
2
```
Then you would use the normal sybil.parsers.rest.DocTestDirectiveParser
as follows:
from sybil import Sybil
from sybil.parsers.rest import DocTestDirectiveParser as ReSTDocTestDirectiveParser
sybil = Sybil(parsers=[ReSTDocTestDirectiveParser()])
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 fenced code blocks,
MyST code-block
directives and “invisible”
code blocks in both styles of Markdown mult-line comment.
Python#
Python examples can be checked in either python
fenced code blocks or
MyST code-block
directives using the
sybil.parsers.myst.PythonCodeBlockParser
.
Including all the boilerplate necessary for examples to successfully evaluate and be checked can hinder writing documentation. To help with this, “invisible” code blocks are also supported. These take advantage of either style of Markdown block comments.
For example:
% invisible-code-block: python
%
% # This could be some state setup needed to demonstrate things
% initialized = True
This fenced code block defines a function:
```python
def prefix(text: str) -> str:
return 'prefix: '+text
```
This MyST `code-block` directive then uses it:
```{code-block} python
prefixed = prefix('some text')
```
<!--- invisible-code-block: python
assert prefixed == 'prefix: some text', prefixed
--->
These examples can be checked with the following configuration:
from sybil import Sybil
from sybil.parsers.myst import PythonCodeBlockParser
sybil = Sybil(parsers=[PythonCodeBlockParser()])
Other languages#
sybil.parsers.myst.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:
```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.myst 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.myst 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()])
Skipping examples#
sybil.parsers.myst.SkipParser()
takes advantage of Markdown comments to allow checking of
specified examples to be skipped.
For example:
% skip: next
This would be wrong:
```python
>>> 1 == 2
True
```
If you need to skip a collection of examples, this can be done as follows:
This is pseudo-code:
% skip: start
```python
def foo(...) -> bool:
...
```
When you want to foo, you could do it like this:
```python
foo('baz', 'bob', ...)
```
% skip: end
You can also add conditions to either next
or start
as shown below:
% invisible-code-block: python
%
% import sys
This will only work on Python 3:
% skip: next if(sys.version_info < (3, 0), reason="python 3 only")
```python
>>> 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.myst import PythonCodeBlockParser, SkipParser
sybil = Sybil(parsers=[PythonCodeBlockParser(), 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:
```python
>>> x = 1
>>> x
1
```
Now let's start a new test:
% clear-namespace
```python
>>> x
Traceback (most recent call last):
...
NameError: name 'x' is not defined
```
The following configuration is required:
from sybil import Sybil
from sybil.parsers.myst import PythonCodeBlockParser, ClearNamespaceParser
sybil = Sybil(parsers=[PythonCodeBlockParser(), ClearNamespaceParser()])