reStructuredText

The following directives are supported:

  • .. code-block:: python

  • .. code:: python

  • .. codeblock-name: <name>

  • .. literalinclude::

Any code directive, such as .. code-block:: python, .. code:: python, .. literalinclude:: or literal blocks with a preceding .. codeblock-name: <name>, will be collected and executed automatically, if your pytest configuration allows that.

Usage examples

Standalone code blocks

code-block directive

Note

Note that :name: value has a test_ prefix.

Filename: README.rst

.. code-block:: python
   :name: test_basic_example

   import math

   result = math.pow(3, 2)
   assert result == 9

literalinclude directive

Filename: README.rst

.. literalinclude:: examples/python/basic_example.py
    :name: test_li_basic_example

codeblock-name directive

You can also use a literal block with a preceding name comment:

Filename: README.rst

.. codeblock-name: test_grouping_example_literal_block
This is a literal block::

   y = 5
   print(y * 2)

Grouping multiple code-block directives

It’s possible to split one logical test into multiple blocks. They will be tested under the first :name: specified. Note the .. continue:: directive.

Note

Note that continue directive of the test_grouping_example_part_2 and test_grouping_example_part_3 refers to the test_grouping_example.

Filename: README.rst

.. code-block:: python
   :name: test_grouping_example

   x = 1

Some intervening text.

.. continue: test_grouping_example
.. code-block:: python
   :name: test_grouping_example_part_2

   y = x + 1  # Uses x from the first snippet
   assert y == 2

Some intervening text.

.. continue: test_grouping_example
.. code-block:: python
   :name: test_grouping_example_part_3

   print(y)  # Uses y from the previous snippet

The above mentioned three snippets will run as a single test.

Note

Note, that nameless code block can’t be served as a first block in a group, as there is no way to refer to it. Nameless code blocks can only be used as continuing blocks in a group.


Async

You can use top-level await in your code blocks. The code will be automatically wrapped in an async function.

Filename: README.rst

.. code-block:: python
    :name: test_async_example

    import asyncio

    result = await asyncio.sleep(0.1, result=42)
    assert result == 42

Adding pytest markers to code-block and literalinclude directives

It’s possible to add custom pytest markers to your code-block or literalinclude directives. That allows adding custom logic and mocking in your conftest.py.

In the example below, django_db marker is added to the code-block directive.

Note

Note the pytestmark directive django_db marker.

Filename: README.rst

.. pytestmark: django_db
.. code-block:: python
    :name: test_django

    from django.contrib.auth.models import User

    user = User.objects.first()

Running pytest-style tests within code blocks

The pytestrun marker allows code blocks to be executed as standalone pytest suites. Unlike standard code blocks that are simply executed with exec(), blocks with the pytestrun marker support full pytest functionality including test classes, fixtures, and setup/teardown within documentation snippets.

Note

Note the pytestmark directive pytestrun marker.

Filename: README.rst

.. pytestmark: pytestrun
.. code-block:: python
    :name: test_pytestrun_example

    import pytest

    class TestSystemInfo:

        @pytest.fixture
        def system_name(self):
            return "Linux"

        @pytest.fixture
        def version_number(self):
            return 5

        def test_combined_info(self, system_name, version_number):
            info = f"{system_name} v{version_number}"
            assert info == "Linux v5"

        def test_name_only(self, system_name):
            assert system_name.isalpha()

In the example below, django_db marker is added to the literalinclude directive.

Filename: README.rst

.. pytestmark: django_db
.. literalinclude:: examples/python/django_example.py
    :name: test_li_django_example

Requesting pytest fixtures for code-block and literalinclude directives

It’s possible to request existing or custom pytest fixtures in code-block or literalinclude directives. That allows adding custom logic and mocking in conftest.py.

In the example below, tmp_path fixture is requested for the code-block directive.

Note

Note the pytestfixture directive tmp_path fixture.

Filename: README.rst

.. pytestfixture: tmp_path
.. code-block:: python
    :name: test_path

    d = tmp_path / "sub"
    d.mkdir()  # Create the directory
    assert d.is_dir()  # Verify it was created and is a directory

In the example below, tmp_path fixture is requested for the literalinclude directive.

Filename: README.rst

.. pytestfixture: tmp_path
.. literalinclude:: examples/python/tmp_path_example.py
    :name: test_li_tmp_path_example

Let’s consider a sample openai code to ask LLM to tell a joke. In the example below, openai_mock fixture is requested for the code-block directive.

Note

Note the pytestfixture directive openai_mock.

Filename: README.rst

.. pytestfixture: openai_mock
.. code-block:: python
    :name: test_tell_me_a_joke

    from openai import OpenAI

    client = OpenAI()
    completion = client.chat.completions.create(
        model="gpt-4o",
        messages=[
            {"role": "developer", "content": "You are a famous comedian."},
            {"role": "user", "content": "Tell me a joke."},
        ],
    )

    assert isinstance(completion.choices[0].message.content, str)

Same could be applied to the literalinclude directive.

Filename: README.rst

.. pytestfixture: openai_mock
.. literalinclude:: examples/python/tell_me_a_joke_example.py
    :name: test_li_tell_me_a_joke

Multiple pytestfixture directives are supported. Add one on each line.

Note

When combining pytestfixture and continue directives together, request pytest-fixtures only in the first code-block, as they will automatically become available in all continuing blocks.

Custom pytest-fixtures are supported as well. Just define them in your conftest.py file.

Customisation/hooks

Tests can be extended and fine-tuned using pytest’s standard hook system.

Below is an example workflow:

  1. Add custom pytest markers to the code-block or literalinclude (fakepy, aws, openai).

  2. Implement pytest hooks in conftest.py to react to those markers.

Add custom pytest markers

Add fakepy marker

The example code below will generate a PDF file with random text using fake.py library. Note, that a fakepy marker is added to the code-block.

In the Implement pytest hooks section, you will see what can be done with the markers.

Note

Note the pytestmark directive fakepy marker.

Filename: README.rst

.. pytestmark: fakepy
.. code-block:: python
    :name: test_create_pdf_file

    from fake import FAKER

    FAKER.pdf_file()

In the example code below, a fakepy marker is added to the literalinclude block.

Filename: README.rst

.. pytestmark: fakepy
.. literalinclude:: examples/python/create_pdf_file_example.py
    :name: test_li_create_pdf_file

Add aws marker

Sample boto3 code to create a bucket on AWS S3.

Note

Note the pytestmark directive aws marker.

Filename: README.rst

.. pytestmark: aws
.. code-block:: python
    :name: test_create_bucket

    import boto3

    s3 = boto3.client("s3", region_name="us-east-1")
    s3.create_bucket(Bucket="my-bucket")
    assert "my-bucket" in [b["Name"] for b in s3.list_buckets()["Buckets"]]

Implement pytest hooks

In the example below:

  • moto is used to mock AWS S3 service for all tests marked as aws.

  • openai_mock is used to mock OpenAI API for tests requiring that.

  • FILE_REGISTRY.clean_up() is executed at the end of each test marked as fakepy.

Filename: conftest.py

import contextlib
import json
import os
from pathlib import Path
from types import SimpleNamespace

import pytest
import respx
from fake import FILE_REGISTRY
from moto import mock_aws

from pytest_codeblock.constants import CODEBLOCK_MARK

__author__ = "Artur Barseghyan <artur.barseghyan@gmail.com>"
__copyright__ = "2025-2026 Artur Barseghyan"
__license__ = "MIT"
__all__ = (
    "http_request",
    "http_request_factory",
    "markdown_simple",
    "markdown_with_pytest_mark",
    "openai_mock",
    "pytest_collection_modifyitems",
    "pytest_runtest_setup",
    "pytest_runtest_teardown",
)

pytest_plugins = ["pytester"]


# Modify test item during collection
def pytest_collection_modifyitems(
    config: pytest.Config,
    items: list[pytest.Item],
) -> None:
    """Modify collected test items after collection is done.

    :param config: The pytest configuration object.
    :param items: A list of collected test items.
    """
    for item in items:
        if item.get_closest_marker(CODEBLOCK_MARK):
            # Add `documentation` marker to `pytest-codeblock` tests
            item.add_marker(pytest.mark.documentation)
        if item.get_closest_marker("aws"):
            # Apply `mock_aws` to all tests marked as `aws`
            item.obj = mock_aws(item.obj)


# Setup before test runs
def pytest_runtest_setup(item: pytest.Item) -> None:
    """Set up test environment before each test runs.

    :param item: The test item that is about to run.
    """


# Teardown after the test ends
def pytest_runtest_teardown(item: pytest.Item, nextitem: pytest.Item) -> None:
    """Tear down test environment after each test ends.

    :param item: The test item that just finished running.
    :param nextitem: The next test item that will run (or None if this is
    """
    if item.get_closest_marker("fakepy"):
        FILE_REGISTRY.clean_up()


@pytest.fixture
def http_request_factory():
    """
    Returns a function that creates a simple namespace object
    with a 'GET' attribute set to the provided dictionary.
    """
    def _factory(get_data: dict):
        # Creates an object like: object(GET={'key': 'value'})
        return SimpleNamespace(GET=get_data)
    return _factory


@pytest.fixture
def http_request(http_request_factory):
    test_data = {"param1": "value1", "signature": "mock-sig"}
    return http_request_factory(test_data)


@pytest.fixture
def openai_mock():
    # Setup
    os.environ.setdefault("OPENAI_API_KEY", "test-key")
    cassette_path = (
        Path(__file__).parent
        / "examples"
        / "cassettes"
        / "openai_chat_completion.json"
    )
    with open(cassette_path) as f:
        response_data = json.load(f)

    mock = respx.mock()
    mock.start()
    mock.post("https://api.openai.com/v1/chat/completions").respond(
        json=response_data,
    )
    yield mock

    # Teardown
    with contextlib.suppress(Exception):
        mock.stop()


@pytest.fixture
def markdown_simple():
    return """
```python name=test_example
x=1
assert x==1
```"""


@pytest.fixture
def markdown_with_pytest_mark():
    return """
<!-- pytestmark: django_db -->
```python name=test_db
from django.db import models
```"""


@pytest.fixture
def pytester_subprocess(pytester):
    """
    Wrapper that forces subprocess mode to avoid deprecation warning conflicts
    when the plugin uses the old `path` argument signature.
    """
    pytester.runpytest = pytester.runpytest_subprocess
    return pytester