Skip to main content

pytest-run-context logo

PyPI - Python Version License: MIT

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)

Source distribution for pytest-run-context 0.1.0
File Size Uploaded Provenance
pytest_run_context-0.1.0.tar.gz 1.9 MB Without provenance Details

Built distribution (wheel)

Table of built distributions (wheels) for pytest-run-context 0.1.0
File Interpreter ABI Platform
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.

Release history Release notifications | RSS feed

This release

0.1.0 This release

2 release files No provenance

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page