Skip to content
Back to top
View this page

# Markdown Parsers

Sybil supports Markdown, including GitHub Flavored Markdown and . 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()])

A `NUMBER` option flag is provided such that floating point numbers can be used in examples without worrying about precision errors. An example such as the following can be problematic:

    ```python
    >>> import math
    >>> math.pi
    3.14
    ```

However, it can be checked with the following configuration:

    from sybil import Sybil
    from sybil.evaluators.doctest import NUMBER
    from sybil.parsers.markdown import PythonCodeBlockParser
    sybil = Sybil(parsers=[PythonCodeBlockParser(doctest_optionflags=NUMBER)])

Since most editors strip trailing whitespace when saving files, it is ignored when comparing expected output with actual output. If you need trailing whitespace to be matched exactly, as the standard library’s `doctest` module does, a `KEEP_TRAILING_WHITESPACE` option flag is provided:

    from sybil import Sybil
    from sybil.evaluators.doctest import KEEP_TRAILING_WHITESPACE
    from sybil.parsers.markdown import PythonCodeBlockParser
    sybil = Sybil(parsers=[PythonCodeBlockParser(doctest_optionflags=KEEP_TRAILING_WHITESPACE)])

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