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
```

You can also use HTML-style comments:

<!-- skip: next -->

This would still be wrong:

```python
>>> 1 == 1
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.

When a condition is used to skip one or more following example, it will be reported as a skipped test in your test runner.

If you wish to have unconditional skips show up as skipped tests, this can be done as follows:

This example is not yet working, but I wanted to be reminded:

% skip: next "not yet working"

```python
>>> 1.1 == 1.11
True
```

This can also be done when skipping collections of examples:

And here we can see some pseudo-code that will work in a future release:

% skip: start "Fix in v5"

```python
>>> helper = Framework().make_helper()
>>> helper.describe(...)
```

% skip: end

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()])

You can also used HTML-style comments 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
```