pytest-codeblock
Test your documentation code blocks.
`pytest-codeblock`_ is a Pytest plugin that discovers Python code examples in your reStructuredText and Markdown documentation files and runs them as part of your test suite. This ensures your docs stay correct and up-to-date.
Features
reStructuredText and Markdown support: Automatically find and test code blocks in reStructuredText (
.rst) and Markdown (.md) files. The only requirement here is that your code blocks shall have a name starting withtest_.Grouping by name: Split a single example across multiple code blocks; the plugin concatenates them into one test.
Pytest markers support: Add existing or custom pytest markers to the code blocks and hook into the tests life-cycle using
conftest.py.
Prerequisites
Python 3.9+
pytest is the only required dependency
Documentation
Documentation is available on Read the Docs.
For reStructuredText, see a dedicated reStructuredText docs.
For Markdown, see a dedicated Markdown docs.
Both reStructuredText docs and Markdown docs have extensive documentation on pytest markers and corresponding
conftest.pyhooks.For guidelines on contributing check the Contributor guidelines.
Installation
Install with pip:
pip install pytest-codeblock
Or install with `uv`_:
uv pip install pytest-codeblock
Configuration
Filename: pyproject.toml
[tool.pytest.ini_options]
testpaths = [
"**/*.rst",
"**/*.md",
]
Usage
reStructruredText usage
Any code directive, such as .. code-block:: python, .. code:: python,
or literal blocks with a preceding .. codeblock-name: <name>, will be
collected and executed automatically, if your pytest configuration
allows that.
code-block directive example
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 example
Note
Note that :name: value has a test_ prefix.
Filename: README.rst
.. literalinclude:: examples/python/basic_example.py
:name: test_li_basic_example
See a dedicated reStructuredText docs for more.
Markdown usage
Any fenced code block with a recognized Python language tag (e.g., python,
py) will be collected and executed automatically, if your pytest
configuration allows that.
Note
Note that name value has a test_ prefix.
Filename: README.md
```python name=test_basic_example
import math
result = math.pow(3, 2)
assert result == 9
```
See a dedicated Markdown docs for more.
Tests
Run the tests with pytest:
pytest
Writing documentation
Keep the following hierarchy.
=====
title
=====
header
======
sub-header
----------
sub-sub-header
~~~~~~~~~~~~~~
sub-sub-sub-header
^^^^^^^^^^^^^^^^^^
sub-sub-sub-sub-header
++++++++++++++++++++++
sub-sub-sub-sub-sub-header
**************************
License
MIT
Support
For security issues contact me at the e-mail given in the Author section.
For overall issues, go to GitHub.
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.
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()
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
Customisation/hooks
Tests can be extended and fine-tuned using pytest’s standard hook system.
Below is an example workflow:
Add custom pytest markers to the
code-blockorliteralinclude(fakepy,aws,openai).Implement pytest hooks in
conftest.pyto 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"]]
Add openai marker
Sample openai code to ask LLM to tell a joke. Note, that next to a
custom openai marker, xfail marker is used, which allows underlying
code to fail, without marking entire test suite as failed.
Note
Note the pytestmark directive xfail and openai markers.
Filename: README.rst
.. pytestmark: xfail
.. pytestmark: openai
.. 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)
Implement pytest hooks
In the example below:
moto is used to mock AWS S3 service for all tests marked as
aws.Environment variable
OPENAI_BASE_URLis set tohttp://localhost:11434/v1(assuming you have Ollama running) for all tests marked asopenai.FILE_REGISTRY.clean_up()is executed at the end of each test marked asfakepy.
Filename: conftest.py
import os
from contextlib import suppress
import pytest
from fake import FILE_REGISTRY
from moto import mock_aws
from pytest_codeblock.constants import CODEBLOCK_MARK
# Modify test item during collection
def pytest_collection_modifyitems(config, items):
for item in items:
if item.get_closest_marker(CODEBLOCK_MARK):
# All `pytest-codeblock` tests are automatically assigned
# a `codeblock` marker, which can be used for customisation.
# In the example below we add an additional `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):
if item.get_closest_marker("openai"):
# Send all OpenAI requests to locally running Ollama for all
# tests marked as `openai`. The tests would x-pass on environments
# where Ollama is up and running (assuming, you have created an
# alias for gpt-4o using one of the available models) and would
# x-fail on environments, where Ollama isn't runnig.
os.environ.setdefault("OPENAI_API_KEY", "ollama")
os.environ.setdefault("OPENAI_BASE_URL", "http://localhost:11434/v1")
# Teardown after the test ends
def pytest_runtest_teardown(item, nextitem):
# Run file clean up on all tests marked as `fakepy`
if item.get_closest_marker("fakepy"):
FILE_REGISTRY.clean_up()
Markdown
Usage examples
Any fenced code block with a recognized Python language tag (e.g., python,
py) will be collected and executed automatically, if
your pytest configuration allows that.
Standalone code blocks
Note
Note that name value has a test_ prefix.
Filename: README.md
```python name=test_basic_example
import math
result = math.pow(3, 2)
assert result == 9
```
Grouping multiple code blocks
It’s possible to split one logical test into multiple blocks by specifying the same name.
Note
Note that both snippts share the same name
value (test_grouping_example).
Filename: README.md
```python name=test_grouping_example
x = 1
```
Some intervening text.
```python name=test_grouping_example
print(x + 1) # Uses x from the first snippet
```
The above mentioned three snippets will run as a single test.
Adding pytest markers to code blocks
It’s possible to add custom pytest markers to your code blocks. That allows
adding custom logic and mocking in your conftest.py.
In the example below, django_db marker is added to the code block.
Note
Note the pytestmark directive django_db marker.
Filename: README.md
<!-- pytestmark: django_db -->
```python name=test_django
from django.contrib.auth.models import User
user = User.objects.first()
```
Customisation/hooks
Tests can be extended and fine-tuned using pytest’s standard hook system.
Below is an example workflow:
Add custom pytest markers to the code blocks (
fakepy,aws,openai).Implement pytest hooks in
conftest.pyto 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.md
<!-- pytestmark: fakepy -->
```python name=test_create_pdf_file
from fake import FAKER
FAKER.pdf_file()
```
Add aws marker
Sample boto3 code to create a bucket on AWS S3.
Note
Note the pytestmark directive aws marker.
Filename: README.md
<!-- pytestmark: aws -->
```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"]]
```
Add openai marker
Sample openai code to ask LLM to tell a joke. Note, that next to a
custom openai marker, xfail marker is used, which allows underlying
code to fail, without marking entire test suite as failed.
Note
Note the pytestmark directive xfail and openai markers.
Filename: README.md
<!-- pytestmark: xfail -->
<!-- pytestmark: openai -->
```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)
```
Implement pytest hooks
In the example below:
moto is used to mock AWS S3 service for all tests marked as
aws.Environment variable
OPENAI_BASE_URLis set tohttp://localhost:11434/v1(assuming you have Ollama running) for all tests marked asopenai.FILE_REGISTRY.clean_up()is executed at the end of each test marked asfakepy.
Filename: conftest.py
import os
from contextlib import suppress
import pytest
from fake import FILE_REGISTRY
from moto import mock_aws
from pytest_codeblock.constants import CODEBLOCK_MARK
# Modify test item during collection
def pytest_collection_modifyitems(config, items):
for item in items:
if item.get_closest_marker(CODEBLOCK_MARK):
# All `pytest-codeblock` tests are automatically assigned
# a `codeblock` marker, which can be used for customisation.
# In the example below we add an additional `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):
if item.get_closest_marker("openai"):
# Send all OpenAI requests to locally running Ollama for all
# tests marked as `openai`. The tests would x-pass on environments
# where Ollama is up and running (assuming, you have created an
# alias for gpt-4o using one of the available models) and would
# x-fail on environments, where Ollama isn't runnig.
os.environ.setdefault("OPENAI_API_KEY", "ollama")
os.environ.setdefault("OPENAI_BASE_URL", "http://localhost:11434/v1")
# Teardown after the test ends
def pytest_runtest_teardown(item, nextitem):
# Run file clean up on all tests marked as `fakepy`
if item.get_closest_marker("fakepy"):
FILE_REGISTRY.clean_up()
Security Policy
Reporting a Vulnerability
Do not report security issues on GitHub!
Please report security issues by emailing Artur Barseghyan <artur.barseghyan@gmail.com>.
Supported Versions
The two most recent pytest-codeblock minor release series receive security
support.
It is recommended to use the latest version.
┌─────────────────┬────────────────┐
│ Version │ Supported │
├─────────────────┼────────────────┤
│ 0.1.x │ Yes │
├─────────────────┼────────────────┤
│ < 0.1 │ No │
└─────────────────┴────────────────┘
Note
For example, during the development cycle leading to the release
of pytest-codeblock 0.17.x, support will be provided
for pytest-codeblock 0.16.x.
Upon the release of pytest-codeblock 0.18.x, security support
for pytest-codeblock 0.16.x will end.
Contributor guidelines
Developer prerequisites
pre-commit
Refer to pre-commit for installation instructions.
TL;DR:
curl -LsSf https://astral.sh/uv/install.sh | sh # Install uv
uv tool install pre-commit # Install pre-commit
pre-commit install # Install pre-commit hooks
Installing pre-commit will ensure you adhere to the project code quality standards.
Code standards
ruff and doc8 will be automatically triggered by pre-commit.
ruff is configured to do the job of black and isort as well.
Still, if you want to run checks manually:
make doc8
make ruff
Requirements
Requirements are compiled using `uv`_.
make compile-requirements
Virtual environment
You are advised to work in virtual environment.
TL;DR:
python -m venv env
pip install -e .[all]
Documentation
Check the documentation.
Testing
Check testing.
If you introduce changes or fixes, make sure to test them locally using all supported environments. For that use tox.
tox
In any case, GitHub Actions will catch potential errors, but using tox speeds things up.
For a quick test of the package and all examples, use the following Makefile command:
make test-all
Pull requests
You can contribute to the project by making a pull request.
For example:
To fix documentation typos.
To improve documentation (for instance, to add new recipe or fix an existing recipe that doesn’t seem to work).
To introduce a new feature (for instance, add support for a non-supported file type).
Good to know:
This library is almost dependency free. Do not submit pull requests with external dependencies unless it’s really necessary.
General list to go through:
Does your change require documentation update?
Does your change require update to tests?
Does your change rely on third-party package or a cloud based service? If so, please consider turning it into a dedicated standalone package, since this library is dependency free (and will always stay so).
When fixing bugs (in addition to the general list):
Make sure to add regression tests.
When adding a new feature (in addition to the general list):
Make sure to update the documentation (check whether the installation and features require changes).
GitHub Actions
Only non-EOL versions of Python and software `pytest-codeblock`_ aims to integrate with are supported.
On GitHub Actions includes tests for more than 40 different variations of Python versions and integration packages. Future, non-stable versions of Python are being tested too, so that new features/incompatibilities could be seen and adopted early.
For the list of Python versions supported by GitHub, see GitHub Actions versions manifest.
Questions
Questions can be asked on GitHub discussions.
Issues
For reporting a bug or filing a feature request, use GitHub issues.
Do not report security issues on GitHub. Check the support section.
Release history and notes
Sequence based identifiers are used for versioning (schema follows below):
major.minor[.revision]
It is always safe to upgrade within the same minor version (for example, from 0.3 to 0.3.4).
Minor version changes might be backwards incompatible. Read the release notes carefully before upgrading (for example, when upgrading from 0.3.4 to 0.4).
All backwards incompatible changes are mentioned in this document.
0.2
2025-11-15
Handle deprecations for pytest 9.x. The
fspathargument is replaced withpathand the code is updated accordingly.
0.1.8
2025-05-11
Move everything to src directory.
Add Python tests.
0.1.7
2025-05-11
Minor fixes.
0.1.6
2025-05-10
Minor fixes.
0.1.5
2025-05-07
Improve error tracebacks.
0.1.4
2025-05-05
Fixes in .. literalinclude blocks.
0.1.3
2025-05-05
Add support for .. literalinclude blocks.
0.1.2
2025-05-03
Automatically add codeblock mark to documentation tests.
Add customisation section to documentation.
0.1.1
2025-04-30
Support Python 3.9.
0.1
2025-04-29
Note
In memory of the victims of the Armenian Genocide.
Initial beta release.
Package
Indices and tables
Project source-tree
Below is the layout of our project (to 10 levels), followed by the contents of each key file.
pytest-codeblock/
├── docs
│ ├── _static
│ ├── _templates
│ ├── _implement_pytest_hooks.rst
│ ├── changelog.rst
│ ├── code_of_conduct.rst
│ ├── conf.py
│ ├── conf.py.distrib
│ ├── contributor_guidelines.rst
│ ├── documentation.rst
│ ├── index.rst
│ ├── index.rst.distrib
│ ├── llms.rst
│ ├── make.bat
│ ├── Makefile
│ ├── markdown.rst
│ ├── package.rst
│ ├── requirements.txt
│ ├── restructured_text.rst
│ ├── security.rst
│ └── source_tree.rst
├── examples
│ ├── md_example
│ │ ├── customisation.md
│ │ └── README.md
│ ├── python
│ │ ├── __init__.py
│ │ ├── basic_example.py
│ │ ├── create_bucket_example.py
│ │ ├── create_pdf_file_example.py
│ │ ├── django_example.py
│ │ └── tell_me_a_joke_example.py
│ └── rst_example
│ ├── __pycache__
│ ├── __init__.py
│ ├── customisation.rst
│ ├── django_settings.py
│ └── README.rst
├── scripts
│ └── generate_project_source_tree.py
├── src
│ └── pytest_codeblock
│ ├── __pycache__
│ ├── tests
│ │ ├── __pycache__
│ │ ├── __init__.py
│ │ ├── test_pytest_codeblock.py
│ │ └── tests.rst
│ ├── __init__.py
│ ├── collector.py
│ ├── constants.py
│ ├── md.py
│ ├── rst.py
docs/_implement_pytest_hooks.rst
In the example below:
- `moto`_ is used to mock AWS S3 service for all tests marked as ``aws``.
- Environment variable ``OPENAI_BASE_URL`` is set
to ``http://localhost:11434/v1`` (assuming you have `Ollama`_ running) for
all tests marked as ``openai``.
- ``FILE_REGISTRY.clean_up()`` is executed at the end of each test marked
as ``fakepy``.
*Filename: conftest.py*
.. code-block:: python
import os
from contextlib import suppress
import pytest
from fake import FILE_REGISTRY
from moto import mock_aws
from pytest_codeblock.constants import CODEBLOCK_MARK
# Modify test item during collection
def pytest_collection_modifyitems(config, items):
for item in items:
if item.get_closest_marker(CODEBLOCK_MARK):
# All `pytest-codeblock` tests are automatically assigned
# a `codeblock` marker, which can be used for customisation.
# In the example below we add an additional `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):
if item.get_closest_marker("openai"):
# Send all OpenAI requests to locally running Ollama for all
# tests marked as `openai`. The tests would x-pass on environments
# where Ollama is up and running (assuming, you have created an
# alias for gpt-4o using one of the available models) and would
# x-fail on environments, where Ollama isn't runnig.
os.environ.setdefault("OPENAI_API_KEY", "ollama")
os.environ.setdefault("OPENAI_BASE_URL", "http://localhost:11434/v1")
# Teardown after the test ends
def pytest_runtest_teardown(item, nextitem):
# Run file clean up on all tests marked as `fakepy`
if item.get_closest_marker("fakepy"):
FILE_REGISTRY.clean_up()
docs/changelog.rst
.. include:: ../CHANGELOG.rst
docs/code_of_conduct.rst
.. include:: ../CODE_OF_CONDUCT.rst
docs/conf.py
# Configuration file for the Sphinx documentation builder.
#
# For the full list of built-in configuration values, see the documentation:
# https://www.sphinx-doc.org/en/master/usage/configuration.html
import os
import sys
sys.path.insert(0, os.path.abspath(os.path.join("..", "src")))
# -- Project information -----------------------------------------------------
# https://www.sphinx-doc.org/en/master/usage/configuration.html#project-information
try:
import pytest_codeblock
version = pytest_codeblock.__version__
project = pytest_codeblock.__title__
copyright = pytest_codeblock.__copyright__
author = pytest_codeblock.__author__
except ImportError:
version = "0.1"
project = "pytest-codeblock"
copyright = "2025, Artur Barseghyan <artur.barseghyan@gmail.com>"
author = "Artur Barseghyan <artur.barseghyan@gmail.com>"
# -- General configuration ---------------------------------------------------
# https://www.sphinx-doc.org/en/master/usage/configuration.html#general-configuration
extensions = [
"sphinx.ext.autodoc",
"sphinx.ext.viewcode",
"sphinx.ext.todo",
"sphinx_no_pragma",
]
templates_path = ["_templates"]
exclude_patterns = ["_build", "Thumbs.db", ".DS_Store"]
language = "en"
release = version
# The suffix of source filenames.
source_suffix = {
".rst": "restructuredtext",
}
pygments_style = "sphinx"
# -- Options for HTML output -------------------------------------------------
# https://www.sphinx-doc.org/en/master/usage/configuration.html#options-for-html-output
html_theme = "sphinx_rtd_theme"
html_static_path = ["_static"]
# html_extra_path = ["examples"]
prismjs_base = "//cdnjs.cloudflare.com/ajax/libs/prism/1.29.0"
html_css_files = [
f"{prismjs_base}/themes/prism.min.css",
f"{prismjs_base}/plugins/toolbar/prism-toolbar.min.css",
# "https://cdn.jsdelivr.net/gh/barseghyanartur/jsphinx@1.3.4/src/css/sphinx_rtd_theme.css",
"https://cdn.jsdelivr.net/gh/barseghyanartur/jsphinx/src/css/sphinx_rtd_theme.css",
]
html_js_files = [
f"{prismjs_base}/prism.min.js",
f"{prismjs_base}/plugins/autoloader/prism-autoloader.min.js",
f"{prismjs_base}/plugins/toolbar/prism-toolbar.min.js",
f"{prismjs_base}/plugins/copy-to-clipboard/prism-copy-to-clipboard.min.js",
# "https://cdn.jsdelivr.net/gh/barseghyanartur/jsphinx@1.3.4/src/js/download_adapter.js",
"https://cdn.jsdelivr.net/gh/barseghyanartur/jsphinx/src/js/download_adapter.js",
]
# -- Options for todo extension ----------------------------------------------
# https://www.sphinx-doc.org/en/master/usage/extensions/todo.html#configuration
todo_include_todos = True
# -- Options for Epub output ----------------------------------------------
epub_title = project
epub_author = author
epub_publisher = "GitHub"
epub_copyright = copyright
epub_identifier = "https://github.com/barseghyanartur/pytest-codeblock" # URL or ISBN
epub_scheme = "URL" # or "ISBN"
epub_uid = "https://github.com/barseghyanartur/pytest-codeblock"
docs/contributor_guidelines.rst
.. include:: ../CONTRIBUTING.rst
docs/documentation.rst
Project documentation
=====================
Contents:
.. contents:: Table of Contents
.. toctree::
:maxdepth: 2
index
restructured_text
markdown
security
contributor_guidelines
code_of_conduct
changelog
package
docs/index.rst
.. include:: ../README.rst
.. include:: documentation.rst
docs/llms.rst
.. include:: ../README.rst
----
.. include:: restructured_text.rst
----
.. include:: markdown.rst
----
.. include:: security.rst
----
.. include:: contributor_guidelines.rst
----
.. include:: changelog.rst
----
.. include:: package.rst
----
.. include:: source_tree.rst
docs/markdown.rst
Markdown
========
.. External references
.. _Markdown: https://daringfireball.net/projects/markdown/
.. _pytest: https://docs.pytest.org
.. _Django: https://www.djangoproject.com
.. _pip: https://pypi.org/project/pip/
.. _uv: https://pypi.org/project/uv/
.. _fake.py: https://github.com/barseghyanartur/fake.py
.. _boto3: https://github.com/boto/boto3
.. _moto: https://github.com/getmoto/moto
.. _openai: https://github.com/openai/openai-python
.. _Ollama: https://github.com/ollama/ollama
Usage examples
--------------
Any fenced code block with a recognized Python language tag (e.g., ``python``,
``py``) will be collected and executed automatically, if
your `pytest`_ :ref:`configuration <configuration>` allows that.
Standalone code blocks
~~~~~~~~~~~~~~~~~~~~~~
.. note:: Note that ``name`` value has a ``test_`` prefix.
*Filename: README.md*
.. code-block:: markdown
```python name=test_basic_example
import math
result = math.pow(3, 2)
assert res ult == 9
```
----
Grouping multiple code blocks
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
It's possible to split one logical test into multiple blocks by specifying
the same name.
.. note:: Note that both snippts share the same ``name``
value (``test_grouping_example``).
*Filename: README.md*
.. code-block:: markdown
```python name=test_grouping_example
x = 1
```
Some intervening text.
```python name=test_grouping_example
print(x + 1) # Uses x from the first snippet
```
The above mentioned three snippets will run as a single test.
----
Adding pytest markers to code blocks
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
It's possible to add custom pytest markers to your code blocks. That allows
adding custom logic and mocking in your ``conftest.py``.
In the example below, ``django_db`` marker is added to the code block.
.. note:: Note the ``pytestmark`` directive ``django_db`` marker.
*Filename: README.md*
.. code-block:: markdown
<!-- pytestmark: django_db -->
```python name=test_django
from django.contrib.auth.models import User
user = User.objects.first()
```
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
blocks (``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.md*
.. code-block:: markdown
<!-- pytestmark: fakepy -->
```python name=test_create_pdf_file
from fake import FAKER
FAKER.pdf_file()
```
Add ``aws`` marker
^^^^^^^^^^^^^^^^^^
Sample `boto3`_ code to create a bucket on AWS S3.
.. note:: Note the ``pytestmark`` directive ``aws`` marker.
*Filename: README.md*
.. code-block:: markdown
<!-- pytestmark: aws -->
```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"]]
```
Add ``openai`` marker
^^^^^^^^^^^^^^^^^^^^^
Sample `openai`_ code to ask LLM to tell a joke. Note, that next to a
custom ``openai`` marker, ``xfail`` marker is used, which allows underlying
code to fail, without marking entire test suite as failed.
.. note:: Note the ``pytestmark`` directive ``xfail`` and ``openai`` markers.
*Filename: README.md*
.. code-block:: markdown
<!-- pytestmark: xfail -->
<!-- pytestmark: openai -->
```python name=test_tell_me_a_joke
from openai import OpenAI
client = OpenAI()
completion = client.chat.completions.create(
model="gpt-4o",
mes sages=[
{" role": "developer", "content": "You are a fam ous comedian."},
{"role": " user", "content": "Tell me a joke."},
],
)
assert isinstance(comple tion.choices[0].message.content, str)
```
----
Implement pytest hooks
~~~~~~~~~~~~~~~~~~~~~~
.. include:: _implement_pytest_hooks.rst
docs/package.rst
Package
=======
.. toctree::
:maxdepth: 20
fake
Indices and tables
==================
* :ref:`genindex`
* :ref:`modindex`
* :ref:`search`
docs/restructured_text.rst
reStructuredText
================
.. External references
.. _reStructuredText: https://docutils.sourceforge.io/rst.html
.. _pytest: https://docs.pytest.org
.. _Django: https://www.djangoproject.com
.. _pip: https://pypi.org/project/pip/
.. _uv: https://pypi.org/project/uv/
.. _fake.py: https://github.com/barseghyanartur/fake.py
.. _boto3: https://github.com/boto/boto3
.. _moto: https://github.com/getmoto/moto
.. _openai: https://github.com/openai/openai-python
.. _Ollama: https://github.com/ollama/ollama
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`_ :ref:`configuration <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:: rst
.. code-block:: python
:name: test_basic_example
import math
result = math.pow(3, 2)
assert result == 9
----
``literalinclude`` directive
^^^^^^^^^^^^^^^^^^^^^^^^^^^^
*Filename: README.rst*
.. code-block:: 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*
.. code-block:: 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:: 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.
----
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*
.. code-block:: rst
.. pytestmark: django_db
.. code-block:: python
:name: test_django
from django.contrib.auth.models import User
user = User.objects.first()
----
In the example below, ``django_db`` marker is added to the ``literalinclude``
directive.
*Filename: README.rst*
.. code-block:: rst
.. pytestmark: django_db
.. literalinclude:: examples/python/django_example.py
:name: test_li_django_example
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*
.. code-block:: 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*
.. code-block:: 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*
.. code-block:: 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"]]
----
Add ``openai`` marker
^^^^^^^^^^^^^^^^^^^^^
Sample `openai`_ code to ask LLM to tell a joke. Note, that next to a
custom ``openai`` marker, ``xfail`` marker is used, which allows underlying
code to fail, without marking entire test suite as failed.
.. note:: Note the ``pytestmark`` directive ``xfail`` and ``openai`` markers.
*Filename: README.rst*
.. code-block:: rst
.. pytestmark: xfail
.. pytestmark: openai
.. 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)
----
Implement pytest hooks
~~~~~~~~~~~~~~~~~~~~~~
.. include:: _implement_pytest_hooks.rst
docs/security.rst
.. include:: ../SECURITY.rst
docs/source_tree.rst
Project source-tree
===================
Below is the layout of our project (to 10 levels), followed by
the contents of each key file.
.. code-block:: bash
:caption: Project directory layout
pytest-codeblock/
├── docs
│ ├── _static
│ ├── _templates
│ ├── _implement_pytest_hooks.rst
│ ├── changelog.rst
│ ├── code_of_conduct.rst
│ ├── conf.py
│ ├── conf.py.distrib
│ ├── contributor_guidelines.rst
│ ├── documentation.rst
│ ├── index.rst
│ ├── index.rst.distrib
│ ├── llms.rst
│ ├── make.bat
│ ├── Makefile
│ ├── markdown.rst
│ ├── package.rst
│ ├── requirements.txt
│ ├── restructured_text.rst
│ ├── security.rst
│ └── source_tree.rst
├── examples
│ ├── md_example
│ │ ├── customisation.md
│ │ └── README.md
│ ├── python
│ │ ├── __init__.py
│ │ ├── basic_example.py
│ │ ├── create_bucket_example.py
│ │ ├── create_pdf_file_example.py
│ │ ├── django_example.py
│ │ └── tell_me_a_joke_example.py
│ └── rst_example
│ ├── __pycache__
│ ├── __init__.py
│ ├── customisation.rst
│ ├── django_settings.py
│ └── README.rst
├── scripts
│ └── generate_project_source_tree.py
├── src
│ └── pytest_codeblock
│ ├── __pycache__
│ ├── tests
│ │ ├── __pycache__
│ │ ├── __init__.py
│ │ ├── test_pytest_codeblock.py
│ │ └── tests.rst
│ ├── __init__.py
│ ├── collector.py
│ ├── constants.py
│ ├── md.py
│ ├── rst.py
docs/_implement_pytest_hooks.rst
--------------------------------
.. literalinclude:: _implement_pytest_hooks.rst
:language: rst
:caption: docs/_implement_pytest_hooks.rst
docs/changelog.rst
------------------
.. literalinclude:: changelog.rst
:language: rst
:caption: docs/changelog.rst
docs/code_of_conduct.rst
------------------------
.. literalinclude:: code_of_conduct.rst
:language: rst
:caption: docs/code_of_conduct.rst
docs/conf.py
------------
.. literalinclude:: conf.py
:language: python
:caption: docs/conf.py
docs/contributor_guidelines.rst
-------------------------------
.. literalinclude:: contributor_guidelines.rst
:language: rst
:caption: docs/contributor_guidelines.rst
docs/documentation.rst
----------------------
.. literalinclude:: documentation.rst
:language: rst
:caption: docs/documentation.rst
docs/index.rst
--------------
.. literalinclude:: index.rst
:language: rst
:caption: docs/index.rst
docs/llms.rst
-------------
.. literalinclude:: llms.rst
:language: rst
:caption: docs/llms.rst
docs/markdown.rst
-----------------
.. literalinclude:: markdown.rst
:language: rst
:caption: docs/markdown.rst
docs/package.rst
----------------
.. literalinclude:: package.rst
:language: rst
:caption: docs/package.rst
docs/restructured_text.rst
--------------------------
.. literalinclude:: restructured_text.rst
:language: rst
:caption: docs/restructured_text.rst
docs/security.rst
-----------------
.. literalinclude:: security.rst
:language: rst
:caption: docs/security.rst
docs/source_tree.rst
--------------------
.. literalinclude:: source_tree.rst
:language: rst
:caption: docs/source_tree.rst
examples/md_example/README.md
-----------------------------
.. literalinclude:: ../examples/md_example/README.md
:language: markdown
:caption: examples/md_example/README.md
examples/md_example/customisation.md
------------------------------------
.. literalinclude:: ../examples/md_example/customisation.md
:language: markdown
:caption: examples/md_example/customisation.md
examples/python/__init__.py
---------------------------
.. literalinclude:: ../examples/python/__init__.py
:language: python
:caption: examples/python/__init__.py
examples/python/basic_example.py
--------------------------------
.. literalinclude:: ../examples/python/basic_example.py
:language: python
:caption: examples/python/basic_example.py
examples/python/create_bucket_example.py
----------------------------------------
.. literalinclude:: ../examples/python/create_bucket_example.py
:language: python
:caption: examples/python/create_bucket_example.py
examples/python/create_pdf_file_example.py
------------------------------------------
.. literalinclude:: ../examples/python/create_pdf_file_example.py
:language: python
:caption: examples/python/create_pdf_file_example.py
examples/python/django_example.py
---------------------------------
.. literalinclude:: ../examples/python/django_example.py
:language: python
:caption: examples/python/django_example.py
examples/python/tell_me_a_joke_example.py
-----------------------------------------
.. literalinclude:: ../examples/python/tell_me_a_joke_example.py
:language: python
:caption: examples/python/tell_me_a_joke_example.py
examples/rst_example/README.rst
-------------------------------
.. literalinclude:: ../examples/rst_example/README.rst
:language: rst
:caption: examples/rst_example/README.rst
examples/rst_example/__init__.py
--------------------------------
.. literalinclude:: ../examples/rst_example/__init__.py
:language: python
:caption: examples/rst_example/__init__.py
examples/rst_example/customisation.rst
--------------------------------------
.. literalinclude:: ../examples/rst_example/customisation.rst
:language: rst
:caption: examples/rst_example/customisation.rst
examples/rst_example/django_settings.py
---------------------------------------
.. literalinclude:: ../examples/rst_example/django_settings.py
:language: python
:caption: examples/rst_example/django_settings.py
scripts/generate_project_source_tree.py
---------------------------------------
.. literalinclude:: ../scripts/generate_project_source_tree.py
:language: python
:caption: scripts/generate_project_source_tree.py
src/pytest_codeblock/__init__.py
--------------------------------
.. literalinclude:: ../src/pytest_codeblock/__init__.py
:language: python
:caption: src/pytest_codeblock/__init__.py
src/pytest_codeblock/collector.py
---------------------------------
.. literalinclude:: ../src/pytest_codeblock/collector.py
:language: python
:caption: src/pytest_codeblock/collector.py
src/pytest_codeblock/constants.py
---------------------------------
.. literalinclude:: ../src/pytest_codeblock/constants.py
:language: python
:caption: src/pytest_codeblock/constants.py
src/pytest_codeblock/md.py
--------------------------
.. literalinclude:: ../src/pytest_codeblock/md.py
:language: python
:caption: src/pytest_codeblock/md.py
src/pytest_codeblock/rst.py
---------------------------
.. literalinclude:: ../src/pytest_codeblock/rst.py
:language: python
:caption: src/pytest_codeblock/rst.py
src/pytest_codeblock/tests/__init__.py
--------------------------------------
.. literalinclude:: ../src/pytest_codeblock/tests/__init__.py
:language: python
:caption: src/pytest_codeblock/tests/__init__.py
src/pytest_codeblock/tests/test_pytest_codeblock.py
---------------------------------------------------
.. literalinclude:: ../src/pytest_codeblock/tests/test_pytest_codeblock.py
:language: python
:caption: src/pytest_codeblock/tests/test_pytest_codeblock.py
src/pytest_codeblock/tests/tests.rst
------------------------------------
.. literalinclude:: ../src/pytest_codeblock/tests/tests.rst
:language: rst
:caption: src/pytest_codeblock/tests/tests.rst
examples/md_example/README.md
# Markdown example project
This is a minimal example showing how pytest-codeblock will discover and run
only Python snippets whose `name` starts with test_.
## Simple assertion
```python name=test_simple_assert
# A trivial test that always passes
assert 2 + 2 == 4
```
## Multi-part example
It's possible to split one logical test into multiple blocks. All of them
share the same ``name``:
```python name=test_compute_square
import math
```
Some intervening text.
```python name=test_compute_square
result = math.pow(3, 2)
assert result == 9
```
Some intervening text.
```python name=test_compute_square
print(result)
```
## Ignored snippets
Blocks without a `name` or without the `test_` prefix are **not** collected:
```python name=example_not_test
# Name does not start with `test_`, so this is ignored
```
## Non-Python blocks are also ignored
```bash name=test_should_be_ignored
echo "Not Python → skipped"
```
## Custom pytest marks
<!-- pytestmark: django_db -->
```python name=test_django
from django.contrib.auth.models import User
user = User.objects.first()
```
examples/md_example/customisation.md
# Customisation
Customisation examples.
## External references
- [openai](https://github.com/openai/openai-python)
- [moto](https://docs.getmoto.org)
- [fake.py](https://github.com/barseghyanartur/fake.py)
## `fake.py` example
<!-- pytestmark: fakepy -->
```python name=test_create_pdf_file
from fake import FAKER
FAKER.pdf_file()
```
## `moto` example
<!-- pytestmark: aws -->
```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"]]
```
## `openai` example
<!-- pytestmark: xfail -->
<!-- pytestmark: openai -->
```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)
```
examples/python/__init__.py
examples/python/basic_example.py
import math
result = math.pow(3, 2)
assert result == 9
examples/python/create_bucket_example.py
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"]]
examples/python/create_pdf_file_example.py
from fake import FAKER
file = FAKER.pdf_file()
assert file.data["storage"].exists(str(file))
examples/python/django_example.py
from django.contrib.auth.models import User
user = User.objects.first()
examples/python/tell_me_a_joke_example.py
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)
examples/rst_example/README.rst
reStructuredText example project
================================
This is a minimal example showing how `pytest-codeblock` will discover
and run only Python snippets whose `:name:` starts with `test_`.
Simple assertion
----------------
.. code-block:: python
:name: test_simple_assert
# A trivial test that always passes
assert 2 + 2 == 4
Multi-part example
------------------
It's possible to split one logical test into multiple blocks.
They will be tested under the first ``:name:`` specified.
Note the ``.. continue::`` directive.
.. code-block:: python
:name: test_compute_square
import math
Some intervening text.
.. continue: test_compute_square
.. code-block:: python
:name: test_compute_square_part_2
result = math.pow(3, 2)
assert result == 9
Some intervening text.
.. continue: test_compute_square
.. code-block:: python
:name: test_compute_square_part_3
print(result)
Ignored snippets
----------------
Blocks without a `:name:` or without the `test_` prefix are **not** collected:
.. code-block:: python
# No :name:, so this is ignored
.. code-block:: python
:name: example_not_test
# Name does not start with `test_`, so this is ignored
Non-Python blocks are also ignored
----------------------------------
.. code-block:: bash
:name: test_should_be_ignored
echo "Not Python → skipped"
Custom pytest marks
-------------------
.. pytestmark: django_db
.. code-block:: python
:name: test_django
from django.contrib.auth.models import User
user = User.objects.first()
examples/rst_example/__init__.py
examples/rst_example/customisation.rst
Customisation
=============
Customisation examples.
.. External references
.. _openai: https://github.com/openai/openai-python
.. _moto: https://docs.getmoto.org
.. _fake.py: https://github.com/barseghyanartur/fake.py
`fake.py`_ example for `.. code-block::` directive
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
.. code-block:: rst
.. pytestmark: fakepy
.. code-block:: python
:name: test_create_pdf_file
from fake import FAKER
file = FAKER.pdf_file()
assert file.data["storage"].exists(str(file))
`moto`_ example for `.. code-block::` directive
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
.. code-block:: 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"]]
`openai`_ example for `.. code-block::` directive
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
.. code-block:: rst
.. pytestmark: xfail
.. pytestmark: openai
.. 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)
`fake.py`_ example for `.. literalinclude::` directive
------------------------------------------------------
.. code-block:: rst
.. pytestmark: fakepy
.. literalinclude:: examples/python/create_pdf_file_example.py
:name: test_li_create_pdf_file
`moto`_ example for `.. literalinclude::` directive
---------------------------------------------------
.. code-block:: rst
.. pytestmark: aws
.. literalinclude:: examples/python/create_bucket_example.py
:name: test_li_create_bucket
`openai`_ example for `.. literalinclude::` directive
-----------------------------------------------------
.. code-block:: rst
.. pytestmark: xfail
.. pytestmark: aws
.. literalinclude:: examples/python/tell_me_a_joke_example.py
:name: test_li_tell_me_a_joke
examples/rst_example/django_settings.py
from pathlib import Path
from django import conf, http, urls
from django.core.handlers import asgi
# Build paths inside the project like this: BASE_DIR / 'subdir'.
BASE_DIR = Path(__file__).resolve().parent
# SECURITY WARNING: don't run with debug turned on in production!
DEBUG = True
INSTALLED_APPS = [
"django.contrib.admin",
"django.contrib.auth",
"django.contrib.contenttypes",
"django.contrib.sessions",
"django.contrib.messages",
"django.contrib.staticfiles",
]
DATABASES = {
"default": {
"ENGINE": "django.db.backends.sqlite3",
"NAME": BASE_DIR / "django_db.sqlite3",
}
}
conf.settings.configure(
ALLOWED_HOSTS="*",
ROOT_URLCONF=__name__,
INSTALLED_APPS=INSTALLED_APPS,
DATABASES=DATABASES,
)
app = asgi.ASGIHandler()
async def root(_request: http.HttpRequest) -> http.JsonResponse:
return http.JsonResponse({"message": "OK"})
urlpatterns = [urls.path("", root)]
scripts/generate_project_source_tree.py
#!/usr/bin/env python3
import argparse
import fnmatch
import os
from pathlib import Path
def build_tree(
path: Path,
max_depth: int,
ignore_patterns: list,
whitelist_dirs: list,
include_all: bool,
root: Path,
prefix: str = "",
) -> str:
"""
Recursively build an ASCII tree up to max_depth, applying whitelist and
ignore rules.
"""
if max_depth < 0:
return ""
entries = sorted(
path.iterdir(), key=lambda p: (p.is_file(), p.name.lower())
)
lines = []
for i, entry in enumerate(entries):
rel_path = entry.relative_to(root).as_posix()
# Skip ignored patterns
if any(fnmatch.fnmatch(rel_path, pat) for pat in ignore_patterns):
continue
# Enforce whitelist if not including all
if not include_all and whitelist_dirs and not any(
rel_path.startswith(w.rstrip("/")) for w in whitelist_dirs
):
continue
connector = "└── " if i == len(entries) - 1 else "├── "
lines.append(f"{prefix}{connector}{entry.name}")
# Recurse into directories
if entry.is_dir():
extension = " " if i == len(entries) - 1 else "│ "
subtree = build_tree(
entry,
max_depth - 1,
ignore_patterns,
whitelist_dirs,
include_all,
root,
prefix + extension,
)
if subtree:
lines += subtree.splitlines()
return "\n".join(lines)
def detect_language(path: Path) -> str:
"""Map file suffix to Sphinx language."""
mapping = {
".py": "python",
".js": "javascript",
".java": "java",
".md": "markdown",
".yaml": "yaml",
".yml": "yaml",
".json": "json",
".sh": "bash",
".rst": "rst",
}
return mapping.get(path.suffix, "")
def main():
p = argparse.ArgumentParser(
description="Auto-generate a .rst with tree + literalinclude blocks"
)
p.add_argument(
"-p",
"--project-root",
type=Path,
default=Path("."),
help="Path to your project directory",
)
p.add_argument(
"-d",
"--depth",
type=int,
default=10,
help="How many levels deep to print in the tree",
)
p.add_argument(
"-o",
"--output",
type=Path,
default=Path("docs/source_tree.rst"),
help="Where to write the generated .rst",
)
p.add_argument(
"-e",
"--ext",
nargs="+",
default=[".py", ".md", ".js", ".rst"],
help="Which file extensions to include via literalinclude",
)
p.add_argument(
"-i",
"--ignore",
nargs="+",
default=["__pycache__", "*.pyc", "*.py,cover"],
help="Ignore files or dirs matching these glob patterns (relative to "
"project root)",
)
p.add_argument(
"-w",
"--whitelist",
nargs="+",
default=["src", "docs", "examples", "scripts"],
help="Directories (relative to project root) to include "
"unless --include-all is given",
)
p.add_argument(
"--include-all",
action="store_true",
help="Include all files regardless of whitelist",
)
args = p.parse_args()
root = args.project_root.resolve()
ignore_patterns = args.ignore
whitelist_dirs = args.whitelist
include_all = args.include_all
output = args.output.resolve()
output_dir = output.parent.resolve()
# Header + tree
header = f"""Project source-tree
===================
Below is the layout of our project (to {args.depth} levels), followed by
the contents of each key file.
.. code-block:: bash
:caption: Project directory layout
{root.name}/
"""
tree = build_tree(
root,
args.depth,
ignore_patterns,
whitelist_dirs,
include_all,
root,
prefix=" ",
)
out = [header, tree, ""]
# Walk and collect files
for filepath in sorted(root.rglob("*")):
if not filepath.is_file() or filepath.suffix not in args.ext:
continue
rel_path = filepath.relative_to(root).as_posix()
# Skip ignored
if any(fnmatch.fnmatch(rel_path, pat) for pat in ignore_patterns):
continue
# Enforce whitelist
if (
not include_all
and whitelist_dirs
and not any(
rel_path.startswith(w.rstrip("/")) for w in whitelist_dirs)
):
continue
# Compute include path relative to output_dir
include_path = os.path.relpath(filepath, output_dir).replace(
os.sep, "/"
)
title = rel_path
underline = "-" * len(title)
lang = detect_language(filepath)
out += [
title,
underline,
"",
f".. literalinclude:: {include_path}",
f" :language: {lang}" if lang else "",
f" :caption: {rel_path}",
# " :linenos:",
"",
]
# Write output
args.output.write_text("\n".join(line for line in out if line is not None))
print(f"Wrote {args.output}")
if __name__ == "__main__":
main()
src/pytest_codeblock/__init__.py
from pathlib import Path
from .md import MarkdownFile
from .rst import RSTFile
__title__ = "pytest-codeblock"
__version__ = "0.2"
__author__ = "Artur Barseghyan <artur.barseghyan@gmail.com>"
__copyright__ = "2025 Artur Barseghyan"
__license__ = "MIT"
__all__ = (
"pytest_collect_file",
)
def pytest_collect_file(parent, path):
"""Collect .md and .rst files for codeblock tests."""
# Determine file extension (works for py.path or pathlib.Path)
file_name = str(path).lower()
if file_name.endswith((".md", ".markdown")):
# Use the MarkdownFile collector for Markdown files
return MarkdownFile.from_parent(parent=parent, path=Path(path))
if file_name.endswith(".rst"):
# Use the RSTFile collector for reStructuredText files
return RSTFile.from_parent(parent=parent, path=Path(path))
return None
src/pytest_codeblock/collector.py
from dataclasses import dataclass, field
from typing import Optional
__author__ = "Artur Barseghyan <artur.barseghyan@gmail.com>"
__copyright__ = "2025 Artur Barseghyan"
__license__ = "MIT"
__all__ = (
"CodeSnippet",
"group_snippets",
)
@dataclass
class CodeSnippet:
"""Data container for an extracted code snippet."""
code: str # The code content
line: int # Starting line number in the source
name: Optional[str] = None # Identifier for grouping (None if anonymous)
marks: list[str] = field(default_factory=list)
# Collected pytest marks (e.g. ['django_db']), parsed from doc comments
def group_snippets(snippets: list[CodeSnippet]) -> list[CodeSnippet]:
"""
Merge snippets with the same name into one CodeSnippet,
concatenating their code and accumulating marks.
Unnamed snippets get unique auto-names.
"""
combined: list[CodeSnippet] = []
seen: dict[str, CodeSnippet] = {}
anon_count = 0
for sn in snippets:
key = sn.name
if not key:
anon_count += 1
key = f"codeblock{anon_count}"
if key in seen:
seen_sn = seen[key]
seen_sn.code += "\n" + sn.code
seen_sn.marks.extend(sn.marks)
else:
sn.marks = list(sn.marks) # copy
seen[key] = sn
combined.append(sn)
return combined
src/pytest_codeblock/constants.py
__author__ = "Artur Barseghyan <artur.barseghyan@gmail.com>"
__copyright__ = "2025 Artur Barseghyan"
__license__ = "MIT"
__all__ = (
"CODEBLOCK_MARK",
"DJANGO_DB_MARKS",
"TEST_PREFIX",
)
DJANGO_DB_MARKS = {
"django_db",
"db",
"transactional_db",
}
TEST_PREFIX = "test_"
CODEBLOCK_MARK = "codeblock"
src/pytest_codeblock/md.py
import re
from typing import Optional
import pytest
from .collector import CodeSnippet, group_snippets
from .constants import CODEBLOCK_MARK, DJANGO_DB_MARKS, TEST_PREFIX
__author__ = "Artur Barseghyan <artur.barseghyan@gmail.com>"
__copyright__ = "2025 Artur Barseghyan"
__license__ = "MIT"
__all__ = (
"MarkdownFile",
"parse_markdown",
)
def parse_markdown(text: str) -> list[CodeSnippet]:
"""
Parse Markdown text and extract Python code snippets as CodeSnippet
objects.
Supports:
- <!-- pytestmark: <mark> --> comments immediately before a code fence
- <!-- codeblock-name: <name> --> comments for naming
- Fenced code blocks with ```python (and optional name=<name> in the
info string)
Captures each snippet’s name, code, starting line, and any pytest marks.
"""
snippets: list[CodeSnippet] = []
lines = text.splitlines()
pending_name: Optional[str] = None
pending_marks: list[str] = [CODEBLOCK_MARK]
in_block = False
fence = ""
block_indent = 0
code_buffer: list[str] = []
snippet_name: Optional[str] = None
start_line = 0
for idx, line in enumerate(lines, start=1):
stripped = line.strip()
if not in_block:
# Check for pytest mark comment
if stripped.startswith("<!--") and "pytestmark:" in stripped:
m = re.match(r"<!--\s*pytestmark:\s*(\w+)\s*-->", stripped)
if m:
pending_marks.append(m.group(1))
continue
# Check for name comment
if stripped.startswith("<!--") and "codeblock-name:" in stripped:
m = re.match(
r"<!--\s*codeblock-name:\s*([^ >]+)\s*-->", stripped
)
if m:
pending_name = m.group(1)
continue
# Start of fenced code block?
if line.lstrip().startswith("```"):
indent = len(line) - len(line.lstrip())
m = re.match(r"^`{3,}", line.lstrip())
if not m:
continue
fence = m.group(0)
info = line.lstrip()[len(fence):].strip()
parts = info.split(None, 1)
lang = parts[0].lower() if parts else ""
extra = parts[1] if len(parts) > 1 else ""
if lang in ("python", "py", "python3"):
in_block = True
block_indent = indent
start_line = idx + 1
code_buffer = []
# determine name from info string or pending comment
snippet_name = None
for token in extra.split():
if (
token.startswith("name=")
or token.startswith("name:")
):
snippet_name = (
token.split("=", 1)[-1]
if "=" in token
else token.split(":", 1)[-1]
)
break
if snippet_name is None:
snippet_name = pending_name
# reset pending_name; marks stay until block closes
pending_name = None
continue
else:
# inside a fenced code block
if line.lstrip().startswith(fence):
# end of block
in_block = False
code_text = "\n".join(code_buffer)
snippets.append(CodeSnippet(
name=snippet_name,
code=code_text,
line=start_line,
marks=pending_marks.copy(),
))
# reset pending marks after collecting
pending_marks.clear()
snippet_name = None
else:
# collect code lines (dedent by block_indent)
if line.strip() == "":
code_buffer.append("")
else:
if len(line) >= block_indent:
code_buffer.append(line[block_indent:])
else:
code_buffer.append(line.lstrip())
continue
return snippets
class MarkdownFile(pytest.File):
"""
Collector for Markdown files, extracting only `test_`-prefixed code
snippets.
"""
def collect(self):
text = self.path.read_text(encoding="utf-8")
raw = parse_markdown(text)
# keep only snippets named test_*
tests = [
sn for sn in raw if sn.name and sn.name.startswith(TEST_PREFIX)
]
combined = group_snippets(tests)
for sn in combined:
# generate a real pytest Function so fixtures work
if DJANGO_DB_MARKS.intersection(sn.marks):
def make_func(code):
def test_block(db):
exec(code, {})
return test_block
else:
def make_func(code):
def test_block():
exec(code, {})
return test_block
callobj = make_func(sn.code)
fn = pytest.Function.from_parent(
parent=self,
name=sn.name,
callobj=callobj,
)
# apply any marks (e.g. django_db)
for m in sn.marks:
fn.add_marker(getattr(pytest.mark, m))
yield fn
src/pytest_codeblock/rst.py
import re
import textwrap
import traceback
from pathlib import Path
from typing import Optional, Union
import pytest
from .collector import CodeSnippet, group_snippets
from .constants import CODEBLOCK_MARK, DJANGO_DB_MARKS, TEST_PREFIX
__author__ = "Artur Barseghyan <artur.barseghyan@gmail.com>"
__copyright__ = "2025 Artur Barseghyan"
__license__ = "MIT"
__all__ = (
"RSTFile",
"parse_rst",
"resolve_literalinclude_path",
"get_literalinclude_content",
)
def resolve_literalinclude_path(
base_dir: Union[str, Path],
include_path: str,
) -> Optional[str]:
"""
Resolve the full path for a literalinclude directive.
Returns None if the file doesn't exist.
"""
_include_path = Path(include_path)
# If `include_path` is already absolute or relative and exists, done
if _include_path.exists():
return str(_include_path.resolve())
# If base_path is a file, switch to its parent directory
_base_path = Path(base_dir)
if _base_path.is_file():
_base_path = _base_path.parent
try:
full_path = _base_path / include_path
if full_path.exists():
return str(full_path.resolve())
except Exception:
pass
return None
def get_literalinclude_content(path):
try:
with open(path) as f:
return f.read()
except Exception as e:
raise RuntimeError(
f"Failed to read literalinclude file {path}: {e}"
) from e
def parse_rst(text: str, base_dir: Path) -> list[CodeSnippet]:
"""
Parse an RST document into CodeSnippet objects, capturing:
- .. pytestmark: <mark>
- .. continue: <name>
- .. codeblock-name: <name>
- .. code-block:: python
"""
snippets: list[CodeSnippet] = []
lines = text.splitlines()
n = len(lines)
pending_name: Optional[str] = None
pending_marks: list[str] = [CODEBLOCK_MARK]
pending_continue: Optional[str] = None
i = 0
while i < n:
line = lines[i]
# --------------------------------------------------------------------
# Collect `.. pytestmark: xyz`
# --------------------------------------------------------------------
m = re.match(r"^\s*\.\.\s*pytestmark:\s*(\w+)\s*$", line)
if m:
pending_marks.append(m.group(1))
i += 1
continue
# --------------------------------------------------------------------
# The `.. literalinclude` directive
# --------------------------------------------------------------------
if line.strip().startswith(".. literalinclude::"):
path = line.split(".. literalinclude::", 1)[1].strip()
name = None
# Look ahead for name
j = i + 1
while j < len(lines) and lines[j].strip():
if ":name:" in lines[j]:
name = lines[j].split(":name:", 1)[1].strip()
break
j += 1
if name and name.startswith("test_"):
full_path = resolve_literalinclude_path(base_dir, path)
if full_path:
snippet = CodeSnippet(
code=get_literalinclude_content(full_path),
line=i + 1,
name=name,
marks=pending_marks.copy(),
)
snippets.append(snippet)
i = j + 1
continue
# --------------------------------------------------------------------
# Collect `.. continue: foo`
# --------------------------------------------------------------------
m = re.match(r"^\s*\.\.\s*continue:\s*(\S+)\s*$", line)
if m:
pending_continue = m.group(1)
i += 1
continue
# --------------------------------------------------------------------
# Collect `.. codeblock-name: foo`
# --------------------------------------------------------------------
m = re.match(r"^\s*\.\.\s*codeblock-name:\s*(\S+)\s*$", line)
if m:
pending_name = m.group(1)
i += 1
continue
# --------------------------------------------------------------------
# The `.. code-block` directive
# --------------------------------------------------------------------
m = re.match(r"^(\s*)\.\. (?:code-block|code)::\s*(\w+)", line)
if m:
base_indent = len(m.group(1))
lang = m.group(2).lower()
if lang in ("python", "py", "python3"):
# Parse :name: option
name_val: Optional[str] = None
j = i + 1
while j < n:
ln = lines[j]
if not ln.strip():
j += 1
continue
indent = len(ln) - len(ln.lstrip())
if ln.lstrip().startswith(":") and indent > base_indent:
opt = ln.lstrip()
if opt.lower().startswith(":name:"):
name_val = opt.split(":", 2)[2].strip().split()[0]
j += 1
continue
break
# The j is first code line
if j >= n:
i = j
continue
first = lines[j]
content_indent = len(first) - len(first.lstrip())
if content_indent <= base_indent:
i = j
continue
# Collect code
buf: list[str] = []
k = j
while k < n:
ln = lines[k]
if not ln.strip():
buf.append("")
k += 1
continue
ind = len(ln) - len(ln.lstrip())
if ind >= content_indent:
buf.append(ln[content_indent:])
k += 1
else:
break
# Decide snippet name: continue overrides name_val/pending_name
if pending_continue:
sn_name = pending_continue
pending_continue = None
else:
sn_name = name_val or pending_name
sn_marks = pending_marks.copy()
pending_name = None
pending_marks.clear()
snippets.append(CodeSnippet(
name=sn_name,
code="\n".join(buf),
line=j + 1,
marks=sn_marks,
))
i = k
continue
else:
i += 1
continue
# --------------------------------------------------------------------
# The literal-block via "::"
# --------------------------------------------------------------------
if line.rstrip().endswith("::") and pending_name:
# Similar override logic
if pending_continue:
sn_name = pending_continue
pending_continue = None
else:
sn_name = pending_name
sn_marks = pending_marks.copy()
pending_name = None
pending_marks.clear()
j = i + 1
if j < n and not lines[j].strip():
j += 1
if j >= n:
i = j
continue
first = lines[j]
content_indent = len(first) - len(first.lstrip())
buf: list[str] = []
k = j
while k < n:
ln = lines[k]
if not ln.strip():
buf.append("")
k += 1
continue
ind = len(ln) - len(ln.lstrip())
if ind >= content_indent:
buf.append(ln[content_indent:])
k += 1
else:
break
snippets.append(CodeSnippet(
name=sn_name,
code="\n".join(buf),
line=j + 1,
marks=sn_marks,
))
i = k
continue
i += 1
return snippets
class RSTFile(pytest.File):
"""Collect RST code-block tests as real test functions."""
def collect(self):
text = self.path.read_text(encoding="utf-8")
raw = parse_rst(text, self.path)
# Only keep test_* snippets
tests = [
sn for sn in raw if sn.name and sn.name.startswith(TEST_PREFIX)
]
combined = group_snippets(tests)
for sn in combined:
# Bind the values we need so we don't close over `sn` itself
_sn_name = sn.name
_fpath = str(self.path)
# Create a Python function for this snippet
if DJANGO_DB_MARKS.intersection(sn.marks):
# Function *requests* the db fixture
def make_func(code, sn_name=_sn_name, fpath=_fpath):
def test_block(db):
compiled = compile(code, fpath, "exec")
try:
exec(compiled, {})
except Exception as err:
raise Exception(
f"Error in "
f"codeblock `{sn_name}` in {fpath}:\n"
f"\n{textwrap.indent(code, prefix=' ')}\n\n"
f"{traceback.format_exc()}"
) from err
return test_block
else:
def make_func(code, sn_name=_sn_name, fpath=_fpath):
def test_block():
compiled = compile(code, fpath, "exec")
try:
exec(compiled, {})
except Exception as err:
raise Exception(
f"Error in "
f"codeblock `{sn_name}` in {fpath}:\n"
f"\n{textwrap.indent(code, prefix=' ')}\n\n"
f"{traceback.format_exc()}"
) from err
return test_block
callobj = make_func(sn.code)
fn = pytest.Function.from_parent(
parent=self,
name=sn.name,
callobj=callobj
)
# Re-apply any pytest.mark.<foo> markers
for m in sn.marks:
fn.add_marker(getattr(pytest.mark, m))
yield fn
src/pytest_codeblock/tests/__init__.py
src/pytest_codeblock/tests/test_pytest_codeblock.py
from pytest_codeblock.collector import CodeSnippet, group_snippets
from pytest_codeblock.md import parse_markdown
from pytest_codeblock.rst import (
get_literalinclude_content,
parse_rst,
resolve_literalinclude_path,
)
__author__ = "Artur Barseghyan <artur.barseghyan@gmail.com>"
__copyright__ = "2025 Artur Barseghyan"
__license__ = "MIT"
__all__ = (
"test_group_snippets_different_names",
"test_group_snippets_merges_named",
"test_parse_markdown_simple",
"test_parse_markdown_with_pytestmark",
"test_parse_rst_literalinclude",
"test_parse_rst_simple",
"test_resolve_literalinclude_and_content",
)
def test_group_snippets_merges_named():
# Two snippets with the same name should be combined
sn1 = CodeSnippet(name="foo", code="a=1", line=1, marks=["codeblock"])
sn2 = CodeSnippet(name="foo", code="b=2", line=2, marks=["codeblock", "m"])
combined = group_snippets([sn1, sn2])
assert len(combined) == 1
cs = combined[0]
assert cs.name == "foo"
# Both code parts should appear
assert "a=1" in cs.code
assert "b=2" in cs.code
# Marks should accumulate
assert "m" in cs.marks
def test_group_snippets_different_names():
# Snippets with different names are not grouped
sn1 = CodeSnippet(name="foo", code="x=1", line=1)
sn2 = CodeSnippet(name="bar", code="y=2", line=2)
combined = group_snippets([sn1, sn2])
assert len(combined) == 2
assert combined[0].name.startswith("foo")
assert combined[1].name.startswith("bar")
def test_parse_markdown_simple():
text = """
```python name=test_example
x=1
assert x==1
```"""
snippets = parse_markdown(text)
assert len(snippets) == 1
sn = snippets[0]
assert sn.name == "test_example"
assert "x=1" in sn.code
def test_parse_markdown_with_pytestmark():
text = """
<!-- pytestmark: django_db -->
```python name=test_db
from django.db import models
```"""
snippets = parse_markdown(text)
assert len(snippets) == 1
sn = snippets[0]
# Should include both default and django_db marks
assert "django_db" in sn.marks
assert "codeblock" in sn.marks
def test_resolve_literalinclude_and_content(tmp_path):
base = tmp_path / "dir"
base.mkdir()
file = base / "a.py"
file.write_text("print('hello')")
# Absolute path resolution
abs_path = resolve_literalinclude_path(base, str(file))
assert abs_path == str(file.resolve())
# Relative path resolution
rel_path = resolve_literalinclude_path(base, "a.py")
assert rel_path == str(file.resolve())
# Content read
content = get_literalinclude_content(str(file))
assert content == "print('hello')"
def test_parse_rst_simple(tmp_path):
# Basic code-block directive
rst = """
.. code-block:: python
:name: test_simple
a=2
assert a==2
"""
snippets = parse_rst(rst, tmp_path)
assert len(snippets) == 1
sn = snippets[0]
assert sn.name == "test_simple"
assert "a=2" in sn.code
def test_parse_rst_literalinclude(tmp_path):
# Create an external file to include
include_dir = tmp_path / "inc"
include_dir.mkdir()
target = include_dir / "foo.py"
target.write_text("z=3\nassert z==3")
rst = f"""
.. literalinclude:: {target.name}
:name: test_li
"""
snippets = parse_rst(rst, include_dir)
assert len(snippets) == 1
sn = snippets[0]
assert sn.name == "test_li"
assert "z=3" in sn.code
src/pytest_codeblock/tests/tests.rst
Tests
=====
.. code-block:: python
:name: test_group_snippets_merges_named
import pytest
from pathlib import Path
from pytest_codeblock.collector import CodeSnippet, group_snippets
from pytest_codeblock.md import parse_markdown
from pytest_codeblock.rst import (
parse_rst,
resolve_literalinclude_path,
get_literalinclude_content,
)
# Two snippets with the same name should be combined
sn1 = CodeSnippet(name="foo", code="a=1", line=1, marks=["codeblock"])
sn2 = CodeSnippet(name="foo", code="b=2", line=2, marks=["codeblock", "m"])
combined = group_snippets([sn1, sn2])
assert len(combined) == 1
cs = combined[0]
assert cs.name == "foo"
# Both code parts should appear
assert "a=1" in cs.code
assert "b=2" in cs.code
# Marks should accumulate
assert "m" in cs.marks
----
.. code-block:: python
:name: test_group_snippets_different_names
import pytest
from pathlib import Path
from pytest_codeblock.collector import CodeSnippet, group_snippets
from pytest_codeblock.md import parse_markdown
from pytest_codeblock.rst import (
parse_rst,
resolve_literalinclude_path,
get_literalinclude_content,
)
# Snippets with different names are not grouped
sn1 = CodeSnippet(name="foo", code="x=1", line=1)
sn2 = CodeSnippet(name="bar", code="y=2", line=2)
combined = group_snippets([sn1, sn2])
assert len(combined) == 2
assert combined[0].name.startswith("foo")
assert combined[1].name.startswith("bar")
----
.. code-block:: python
:name: test_parse_markdown_simple
import pytest
from pathlib import Path
from pytest_codeblock.collector import CodeSnippet, group_snippets
from pytest_codeblock.md import parse_markdown
from pytest_codeblock.rst import (
parse_rst,
resolve_literalinclude_path,
get_literalinclude_content,
)
text = """
```python name=test_example
x=1
assert x==1
```"""
snippets = parse_markdown(text)
assert len(snippets) == 1
sn = snippets[0]
assert sn.name == "test_example"
assert "x=1" in sn.code
----
.. code-block:: python
:name: test_parse_markdown_with_pytestmark
import pytest
from pathlib import Path
from pytest_codeblock.collector import CodeSnippet, group_snippets
from pytest_codeblock.md import parse_markdown
from pytest_codeblock.rst import (
parse_rst,
resolve_literalinclude_path,
get_literalinclude_content,
)
text = """
<!-- pytestmark: django_db -->
```python name=test_db
from django.db import models
```"""
snippets = parse_markdown(text)
assert len(snippets) == 1
sn = snippets[0]
# Should include both default and django_db marks
assert "django_db" in sn.marks
assert "codeblock" in sn.marks