Markdown Parsers

Sybil supports Markdown, including GitHub Flavored Markdown and MyST. If you are using Myst, then you should use the MyST parsers. For other flavors of markdown, the parsers described below support extracting and checking examples from Markdown source, including the ability to skip the evaluation of examples where necessary.

doctest

Doctest examples in python fenced code blocks, can be checked with the PythonCodeBlockParser.

For example:

A fenced code block:

```python
>>> x = 1+1
>>> x
2
```

Both examples in the single block above can be checked with the following configuration:

from sybil import Sybil
from sybil.parsers.markdown import PythonCodeBlockParser
sybil = Sybil(parsers=[PythonCodeBlockParser()])

Code blocks

The codeblock parsers extract examples from fenced code blocks and “invisible” code blocks in HTML-style Markdown mult-line comments.

Python

Python examples can be checked in python fenced code blocks using the 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 HTML-style 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
```

These examples can be checked with the following configuration:

from sybil import Sybil
from sybil.parsers.markdown import PythonCodeBlockParser
sybil = Sybil(parsers=[PythonCodeBlockParser()])

Other languages

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.markdown 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.markdown 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

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.

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.markdown 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.markdown import PythonCodeBlockParser, ClearNamespaceParser
sybil = Sybil(parsers=[PythonCodeBlockParser(), ClearNamespaceParser()])