pytest-run-context
Minimal pytest plugin: one mutable, session-wide Pydantic context, explicit persistence and pluggable Storage.
Installation
Add the plugin to a uv project:
uv add pytest-run-context
Once installed, pytest loads the plugin automatically through its pytest11
entry point. Run the consuming project's tests as usual:
uv run pytest
Install the optional Redis backend with uv add 'pytest-run-context[redis]'.
Agent Skills
pytest-run-context includes four skills that help AI coding agents set up, configure, integrate,
and migrate run-context workflows. They do not add runtime dependencies or change the pytest
plugin's behavior. See Agent Skills for the available skills and installation
instructions.
Usage
flowchart TD
accTitle: Context flow inside pytest
accDescr: Pytest configures the plugin, creates or loads one shared context, and exposes it to tests.
A["pytest loads the plugin"] --> B["conftest.py calls<br/>configure"]
B --> C{"--run-id<br/>provided?"}
C -->|No| D["plugin creates RunContext<br/>and saves initial state"]
C -->|Yes| E["plugin loads existing<br/>RunContext"]
D --> F["fixture and accessor expose<br/>the same context instance"]
E --> F
F --> G["tests update context<br/>and call save explicitly"]
G --> H{"context created<br/>by pytest?"}
H -->|Yes| I["plugin finishes context<br/>and saves final state"]
H -->|No| J["external owner finishes<br/>the context later"]
conftest.py:
from typing import Optional
from pydantic import Field
from pytest_run_context import BaseCustomContext, configure
from pytest_run_context.storage import SQLiteStorage
class ProjectContext(BaseCustomContext):
environment: str = "dev"
stand_id: Optional[str] = None
service_versions: dict[str, str] = Field(default_factory=dict)
def pytest_configure(config):
configure(config, context_model=ProjectContext, storage=SQLiteStorage("runs.db"))
tests/test_example.py:
from pytest_run_context import get_run_context
def test_context(run_context):
run_context.custom.stand_id = "stand-01"
assert get_run_context() is run_context
run_context.save()
Without --run-id, pytest creates a context with a generated UUID and owns its
lifecycle. Creation calls on_run_start(), persists the initial state, and
pytest calls finish() when its session ends. finish() calls
on_run_finish(), sets finished_at, and persists both objects.
Passing --run-id means loading an existing active context; it never chooses
an ID for a new context. Pytest preserves fields prepared by another process
and leaves the context unfinished for its external lifecycle owner. It does not
automatically save changes at session completion: tests, fixtures, or other
user code must call run_context.save() explicitly. An unknown or already
finished ID causes a usage error before tests execute. A consuming project can
require --run-id in its own hook:
import pytest
def pytest_configure(config):
if not config.getoption("--run-id"):
raise pytest.UsageError("Missing --run-id. Run environment preparation first.")
configure(config, context_model=ProjectContext, storage=SQLiteStorage("runs.db"))
Creating and loading outside pytest
flowchart TD
accTitle: Context flow across external processes
accDescr: Preparation creates the context, pytest loads and updates it, and cleanup finishes it.
A["preparation creates<br/>RunContext"] --> B["on_run_start runs<br/>and initial state is saved"]
B --> C["preparation updates context<br/>and calls save"]
C --> D["pytest loads context<br/>with --run-id"]
D --> E["tests update context<br/>and call save explicitly"]
E --> F["cleanup loads the same<br/>RunContext"]
F --> G["cleanup calls finish"]
G --> H["on_run_finish runs<br/>and final state is saved"]
An external script can prepare a persistent context without a pytest
Config:
from pytest_run_context import RunContext
from pytest_run_context.storage import SQLiteStorage
storage = SQLiteStorage("runs.db")
try:
context = RunContext.create(
context_model=ProjectContext,
storage=storage,
)
context.custom.environment = "prepared"
context.save()
print(context.info.run_id)
finally:
storage.close()
The subsequent pytest process configures the same backend and runs with the printed ID:
pytest --run-id=<generated-run-id>
After pytest and environment cleanup, the creating workflow reloads and finishes the context:
storage = SQLiteStorage("runs.db")
try:
context = RunContext.load(
run_id=run_id,
context_model=ProjectContext,
storage=storage,
)
context.finish()
finally:
storage.close()
create() accepts an optional explicit run_id; storage atomically rejects a
duplicate with RunAlreadyExistsError. load() never creates or overwrites a
run. It raises RunNotFoundError for a missing run and
InvalidStoredRunError for invalid persisted data. Loading a finished run is
allowed for inspection, but calling finish() again raises
RunAlreadyFinishedError.
save() explicitly writes the current snapshot without invoking lifecycle
hooks. Mutable custom values are visible immediately to code sharing the same
in-memory context, but are not persisted merely because a pytest session ends.
create() persists the initial state and finish() persists the final state.
on_run_start() runs only during creation. Loading, including pytest loading
through --run-id, does not run it again. The owner that created the logical
run is responsible for calling finish(): pytest owns contexts it creates,
while an external preparation workflow owns contexts pytest loads.
SQLite, JSON, and Redis can share a context between processes when every
process uses the same backend configuration. Independent MemoryStorage
instances cannot. SQLite and JSON data never expire automatically; Redis TTL
is configurable and renewed on every write. JSON storage is intended for
sequential, single-process access. Storage passed to standalone APIs remains
owned by the caller and must be closed by it.
Without configure(), the plugin uses in-memory storage and the base custom
context. The plugin does not collect test results or save after individual
tests. get_run_context() uses ContextVar; new threads do not inherit it
automatically.
Examples
The examples/ directory contains runnable scenarios.
Each example focuses on one decision so that storage behavior and context
ownership can be understood independently:
memory_storage/shows the smallest storage setup when context data is needed only inside one process.json_storage/shows human-readable, file-based persistence with one JSON document per run.sqlite_storage/shows durable local persistence in a single SQLite database.redis_storage/shows shared persistence with configurable expiry and connection retries in an externally managed Redis service.standalone/focuses on the lifecycle in which pytest creates, saves, and finishes the context itself.external_preparation/shows a multi-process workflow in which an external orchestrator owns the context lifecycle while pytest loads and updates that context.
Configurable backend examples use the PYTEST_RUN_CONTEXT_* variables listed
in .env.example.
pytest-xdist
With pytest -n N, the controller creates one logical run and passes its
run_id to every worker through xdist's workerinput. Each worker loads its
own in-memory RunContext instance from the configured Storage. At session
completion the controller reloads the latest stored snapshot and finishes the
run once. Failed tests do not prevent this normal session finalization.
With pytest -n N --run-id=ID, the controller and workers load the externally
created run. None of them automatically saves or finishes it; the external
lifecycle owner remains responsible for finish().
Explicit worker save() calls write complete snapshots. Concurrent changes
from different workers are not merged, and the last completed write wins.
xdist therefore requires a Storage backend shared by all processes, such as
SQLite or Redis. MemoryStorage is rejected for multi-process runs. JSON
storage remains intended for sequential, single-process access.
Completion cannot be transactional across a user hook and arbitrary storage.
Lifecycle ownership, xdist behavior, and failure limitations are recorded in
0002-xdist-lifecycle-and-explicit-save.md.
Development with uv
The repository pins Python 3.14 for local development in .python-version.
Install the locked default development environment:
uv sync --locked
Tests are separated by purpose:
tests/
├── unit/ # isolated tests using in-memory objects and fakes
└── integration/ # persistent file backends and real Redis
Plain pytest runs only unit tests because tests/unit is configured as the
default testpaths value:
uv run pytest --strict-markers
File-backed SQLite and JSON integration tests use temporary directories and do not require external services:
uv run pytest --strict-markers tests/integration/test_file_storage.py
Redis integration tests use a disposable redis:7.4-alpine Testcontainers
instance and require a running Docker daemon. Install their non-default
development dependency group and run the integration directory explicitly:
uv sync --locked --group redis-integration
uv run --group redis-integration pytest --strict-markers tests/integration
Run the complete quality sequence (unit tests, integration tests, Python compatibility tests, Ruff lint and format checks, then ty):
./scripts/ci.sh
The complete script requires Docker and network access for the Redis container image and isolated compatibility environments.
For individual checks:
uv run pytest --strict-markers python_compatibility_tests
uv run ruff check .
uv run ruff format --check .
uv run ty check
To install the plugin in another uv project:
uv add --editable /path/to/pytest-run-context
# with optional Redis support: uv add 'pytest-run-context[redis]'
uv.lock pins dependency versions when they can be resolved through the package index. After changing dependencies, run uv lock and commit the file. Once installed, the plugin is loaded automatically through the pytest11 entry point.
Compatibility
The project supports Python 3.9 through 3.14 and pytest 7.0 or newer. The compatibility suite tests every supported Python version with the latest compatible dependencies. It also tests the minimum supported Pydantic and pytest versions on Python 3.9 through 3.11.
Run the compatibility matrix through pytest:
uv run pytest --strict-markers python_compatibility_tests -v
Or run the standalone shell script:
./python_compatibility_tests/run_python_compatibility.sh
Metadata
Release files for pytest-run-context 0.1.0
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | Provenance | |
|---|---|---|---|---|
| pytest_run_context-0.1.0.tar.gz | 1.9 MB | Without provenance | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| pytest_run_context-0.1.0-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 2.0 MB
Release files / pytest_run_context-0.1.0.tar.gz
| Download URL | pytest_run_context-0.1.0.tar.gz |
|---|---|
| Size | 1.9 MB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
5f2df952fa8961925de3a9379d98c200d56db7fdd1454417f18e2196ea8e271f
|
|
BLAKE2b-256 checksum How to use checksums |
03b303c5a44583c67131644a563775434f427e402f646eefe3ff2d927d34b31a
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
uv/0.11.17 {"installer":{"name":"uv","version":"0.11.17","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Ubuntu","version":"24.10","id":"oracular","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":null}
|
Release files / pytest_run_context-0.1.0-py3-none-any.whl
| Download URL | pytest_run_context-0.1.0-py3-none-any.whl |
|---|---|
| Size | 35.3 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
9de8f6a5f3b5ba684096cf7459a52f40118ed35e61d982fdbd7827e29e236a59
|
|
BLAKE2b-256 checksum How to use checksums |
2a083a849efd262b9ad582c2dad8e4c6344b38928cd04cf636d8de75c311042c
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
uv/0.11.17 {"installer":{"name":"uv","version":"0.11.17","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Ubuntu","version":"24.10","id":"oracular","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":null}
|
Security for pytest-run-context 0.1.0
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
No provenance found
None of the files in this release were uploaded with provenance metadata.
A note on package security
PyPI is a repository for distributing packages, not a service for auditing them. While third-party security researchers and our staff do their best to detect and remove malicious packages, PyPI cannot guarantee the quality, security or suitability of any given package.
To learn more about protecting yourself from malicious actors, visit our package security documentation.