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.
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
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]
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
Does anyone know of a better solution?
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"