MusicalNinja
MusicalNinja

Reputation: 172

How do I get doctest to run with examples in markdown codeblocks for mkdocs?

I'm using mkdocs & mkdocstring to build my documentation and including code examples in the docstrings. I'm also using doctest (via pytest --doctest-modules) to test all those examples.

Option 1 - format for documentation

If I format my docstring like this:

    """
    Recursively flattens a nested iterable (including strings!) and returns all elements in order left to right.

    Examples:
    --------
    ```
    >>> [x for x in flatten([1,2,[3,4,[5],6],7,[8,9]])]
    [1, 2, 3, 4, 5, 6, 7, 8, 9]
    ```
    """

Then it renders nicely in the documentation but doctest fails with the error:

Expected:
    [1, 2, 3, 4, 5, 6, 7, 8, 9]
    ```
Got:
    [1, 2, 3, 4, 5, 6, 7, 8, 9]

That makes sense as doctest treats everything until a blank line as expected output and aims to match is exactly

Option 2 - format for doctest

If I format the docstring for doctest without code blocks:

    """
    Recursively flattens a nested iterable (including strings!) and returns all elements in order left to right.

    Examples:
    --------
    >>> [x for x in flatten([1,2,[3,4,[5],6],7,[8,9]])]
    [1, 2, 3, 4, 5, 6, 7, 8, 9]
    """

then doctest passes but the documentation renders

[x for x in flatten([1,2,[3,4,[5],6],7,[8,9]])][1, 2, 3, 4, 5, 6, 7, 8, 9]

Workaround? - add a blank line for doctest

If I format it with an extra blank line before the end of the codeblock:

    """
    Recursively flattens a nested iterable (including strings!) and returns all elements in order left to right.

    Examples:
    --------
    ```
    >>> [x for x in flatten([1,2,[3,4,[5],6],7,[8,9]])]
    [1, 2, 3, 4, 5, 6, 7, 8, 9]

    ```
    """

Then doctest passes but

  1. there is a blank line at the bottom of the example in the documentation (ugly)
  2. I need to remember to add a blank line at the end of each example (error prone and annoying)

Does anyone know of a better solution?

Upvotes: 3

Views: 335

Answers (2)

pawamoy
pawamoy

Reputation: 3806

mkdocstrings (specifically, Griffe) supports raw pycon snippets in Examples sections. However your Examples section is probably not recognized as such because of the extra colon at the end of the title (Examples:). Try to remove it, as well as backtick fences:

    """
    Recursively flattens a nested iterable (including strings!) and returns all elements in order left to right.

    Examples
    --------
    >>> [x for x in flatten([1,2,[3,4,[5],6],7,[8,9]])]
    [1, 2, 3, 4, 5, 6, 7, 8, 9]
    """

Now maybe doctest still wants a blank line before the closing """, not sure.

Additionally, note that I created a Python-Markdown extension that will recognize raw pycon code anywhere, not just in Examples sections: https://pawamoy.github.io/markdown-pycon/ (available to sponsors only at the time of writing).

Upvotes: 0

MusicalNinja
MusicalNinja

Reputation: 172

Patching the regex that doctest uses to identify codeblocks solved this problem. Documenting it here for those who stumble across this in the future ...

As this is not something I want to do regularly in projects(!), I created pytest-doctest-mkdocstrings as a pytest plugin to do this for me and included some additional sanity-checking, configuration options etc.

pip install pytest-doctest-mkdocstrings
pytest --doctest-mdcodeblocks --doctest-modules --doctest-glob="*.md"

For those who are looking here for the answer in code to use yourself, the required change is:


    _MD_EXAMPLE_RE = re.compile(
        r"""
            # Source consists of a PS1 line followed by zero or more PS2 lines.
            (?P<source>
                (?:^(?P<indent> [ ]*) >>>    .*)    # PS1 line
                (?:\n           [ ]*  \.\.\. .*)*)  # PS2 lines
            \n?
            # Want consists of any non-blank lines that do not start with PS1.
            (?P<want> (?:(?![ ]*$)    # Not a blank line
                        (?![ ]*```)  # Not end of a code block
                        (?![ ]*>>>)  # Not a line starting with PS1
                        .+$\n?       # But any other line
                    )*)
            """,
        re.MULTILINE | re.VERBOSE,
    )

    doctest.DocTestParser._EXAMPLE_RE = _MD_EXAMPLE_RE

Specifically I have included (?![ ]*```) # Not end of a code block in the identification of the "want"

Upvotes: 0

Related Questions