# Myst Parsers

Sybil includes a range of parsers for extracting and checking examples from 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
    ```