Patterns of Use

Several commons patterns of use for Sybil are covered here.

Documentation and source examples in Restructured Text

If your project looks like this:

├─docs/
│ ├─conf.py
│ └─index.rst
├─src/
│ └─myproj/
│   └─__init__.py
├─conftest.py
├─pytest.ini
└─setup.py

And if your documentation looks like this:

My Project
==========

.. invisible-code-block: python

    import myproj
    myproj.SEED = 0.3

This project helps you bar your foo!

>>> from myproj import foo
>>> foo('baz')
'baz bar bar bar'

With your examples in source code looking like this:

"""
This package foo's stuff so it bars.

:data:`SEED` is normally random, but if you like you
can set it as follows:

.. code-block:: python

    import myproj
    myproj.SEED = 0.2
"""
import random

SEED: float = random.random()


def foo(text: str):
    """
    Put some ``bar`` on your ``foo``!
    >>> foo('stuff')
    'stuff bar bar'

    """
    return text+int(SEED*10)*' bar'

Then the following configuration in a conftest.py will ensure all your examples are correct:

import pytest
from sybil import Sybil
from sybil.parsers.rest import DocTestParser, PythonCodeBlockParser

@pytest.fixture(scope='session')
def keep_seed():
    import myproj
    seed = myproj.SEED
    yield
    myproj.SEED = seed

pytest_collect_file = Sybil(
    parsers=[
        DocTestParser(),
        PythonCodeBlockParser(),
    ],
    patterns=['*.rst', '*.py'],
    fixtures=['keep_seed']
).pytest()

Documentation in MyST and source examples in Restructured Text

If your project looks like this:

├─docs/
│ ├─conf.py
│ └─index.md
├─src/
│ └─myproj/
│   └─__init__.py
├─conftest.py
├─pytest.ini
└─setup.py

And if your documentation looks like this:

My Project
####

% invisible-code-block: python
%
%    import myproj
%    myproj.SEED = 0.3

This project helps you bar your foo!

```{doctest}
>>> from myproj import foo
>>> foo('baz')
'baz bar bar bar'
```

With your examples in source code looking like this:

"""
This package foo's stuff so it bars.

:data:`SEED` is normally random, but if you like you
can set it as follows:

.. code-block:: python

    import myproj
    myproj.SEED = 0.2
"""
import random

SEED: float = random.random()


def foo(text: str):
    """
    Put some ``bar`` on your ``foo``!
    >>> foo('stuff')
    'stuff bar bar'

    """
    return text+int(SEED*10)*' bar'

Then the following configuration in a conftest.py will ensure all your examples are correct:

import pytest
from sybil import Sybil
from sybil.parsers.myst import (
    DocTestDirectiveParser as MarkdownDocTestParser,
    PythonCodeBlockParser as MarkdownPythonCodeBlockParser
)
from sybil.parsers.rest import (
    DocTestParser as ReSTDocTestParser,
    PythonCodeBlockParser as ReSTPythonCodeBlockParser
)


@pytest.fixture(scope='session')
def keep_seed():
    import myproj
    seed = myproj.SEED
    yield
    myproj.SEED = seed

markdown_examples = Sybil(
    parsers=[
        MarkdownDocTestParser(),
        MarkdownPythonCodeBlockParser(),
    ],
    patterns=['*.md'],
    fixtures=['keep_seed']
)

rest_examples = Sybil(
    parsers=[
        ReSTDocTestParser(),
        ReSTPythonCodeBlockParser(),
    ],
    patterns=['*.py'],
    fixtures=['keep_seed']
)


pytest_collect_file = (markdown_examples+rest_examples).pytest()

Linting and checking examples

If you wish to perform linting of examples in addition to checking that they are correct, you will need to parse each documentation file once for linting and once for checking.

This can be done by having one Sybil to do the linting and another Sybil to do the checking.

Given documentation that looks like this:

My Project
==========

Here is a function:

.. code-block: python

    def a_function(text: str) -> str:
        return f'a function called with {text}'

Let's see this function in use:

>>> a_function('baz')
'a function called with baz'

Then the following configuration in a conftest.py could be used to ensure all your examples are both correct and lint-free:

from typing import Optional

from sybil import Sybil, Example
from sybil.parsers.rest import DocTestParser, PythonCodeBlockParser, CodeBlockParser


def lint_python_source(example: Example) -> Optional[str]:
    # here you'd feed example.parsed, which contains the python source of the
    # .. code-block:: python, to your linting tool of choice
    pass


linting = Sybil(
    name='linting',
    parsers=[
        CodeBlockParser(language='python', evaluator=lint_python_source),
    ],
    patterns=['*.rst'],
)

tests = Sybil(
    name='tests',
    parsers=[
        DocTestParser(),
        PythonCodeBlockParser(),
    ],
    patterns=['*.rst'],
)

pytest_collect_file = (linting + tests).pytest()

Migrating from sphinx.ext.doctest

Sybil currently has partial support for sphinx.ext.doctest. The list below shows how to approach migrating or supporting the various directives from sphinx.ext.doctest. Adding further support won’t be hard, so if anything is missing that’s holding you back, please open an issue on GitHub. After that, it’s mainly left to stop running make doctest!