Skip to main content

Batch preparation and distribution of expensive test resources for pytest.

Project description

pytest-warmup

pytest-warmup is a pytest plugin for batch preparation and distribution of expensive test resources.

Use it when ordinary fixture-by-fixture setup becomes too slow or too hard to reason about because objects are expensive to create, depend on one another, or require extra orchestration after creation.

Typical cases:

  • creating external-domain objects in batches before a module or session runs;
  • waiting for synchronization, indexing, or propagation after creation;
  • reusing one prepared upstream object across multiple tests;
  • creating per-test instances only where the declaration explicitly asks for it;
  • replacing selected prepared values from a snapshot file for debugging;
  • exporting the selected warmup graph into a snapshot template or report.

Installation

pip install pytest-warmup

Public API

This package is intentionally narrow:

  • WarmupPlan
  • WarmupRequirement
  • WarmupError
  • @warmup_param(...)
  • warmup_mgr.use(...).prepare(...)

Quick Start

Declare resource requirements in plan classes:

from pytest_warmup import WarmupPlan, WarmupRequirement


class ProfilePlan(WarmupPlan):
    def require(
        self,
        *,
        profile_name: str,
        id: str | None = None,
        is_per_test: bool | None = None,
    ) -> WarmupRequirement:
        return super().require(
            payload={"profile_name": profile_name},
            dependencies={},
            id=id,
            is_per_test=is_per_test,
        )

    def prepare(self, nodes, runtime) -> None:
        for node in nodes:
            runtime.set(
                node,
                {
                    "profile_id": f"profile-{node.payload['profile_name']}",
                    "profile_name": node.payload["profile_name"],
                },
            )

Build requirements from those plans:

profile = ProfilePlan("profile")
profile_main = profile.require(profile_name="main", id="profile_main")

Create one explicit producer fixture. Producer fixtures may use pytest's session, package, module, class, or function scopes:

import pytest


@pytest.fixture(scope="module")
def prepare_data(warmup_mgr):
    return warmup_mgr.use(profile).prepare()

Inject the prepared resource into a test or fixture:

from pytest_warmup import warmup_param


@warmup_param("prepared_profile", profile_main)
def test_profile(prepare_data, prepared_profile):
    assert prepared_profile["profile_id"].startswith("profile-")

Multiple @warmup_param(...) bindings on one callable are supported when they all resolve through the same producer path:

@warmup_param("prepared_program", program_main)
@warmup_param("prepared_products", products_alpha)
def test_profile(prepare_data, prepared_program, prepared_products):
    assert prepared_products["program_id"] == prepared_program["program_id"]

is_per_test=True on a requirement means that requirement is materialized separately for each collected test item. If omitted, the requirement inherits per-test behavior from upstream dependencies, otherwise it stays shared within the producer scope.

For full runnable examples, see:

Requirement Identity And Reuse

pytest-warmup keeps requirement identity explicit:

  • one declaration object means one logical requirement node;
  • importing and reusing the same WarmupRequirement object means the same resource;
  • calling require(...) again creates a different declaration, even if the payload is identical;
  • public id values are addressable debug keys for overrides and diagnostics, not merge keys.

If two tests should share the same resource, declare it once and import that same requirement object where needed. Do not try to make two separate declarations collapse through a merge key or through matching payloads.

If you hit a duplicate-id error and the intent was reuse, the fix is to import the already-declared WarmupRequirement object instead of redeclaring it.

Producer Patterns

The default model stays explicit: a test or fixture depends on a producer fixture in the ordinary pytest dependency chain.

Recommended order:

  1. use an explicit producer argument for the default, most readable path;
  2. use warmup_autoresolve_producer when you want less producer boilerplate but still keep one clearly defined producer seam;
  3. use producer_fixture="..." only to disambiguate between producers that are already present in the pytest dependency chain.

Producer resolution rules:

  1. if producer_fixture="..." is provided, that fixture must already be part of the pytest dependency chain and is used as the producer;
  2. otherwise, if the dependency chain already contains exactly one prepared producer, that producer is used;
  3. otherwise, warmup_autoresolve_producer is used as a narrow fallback if it exists;
  4. otherwise, producer resolution fails fast.

Producer convenience does not bypass normal pytest scope rules. A narrower producer is still invalid for a wider-scope consumer, and warmup_autoresolve_producer does not widen fixture visibility.

Example of the fallback fixture:

@pytest.fixture
def warmup_autoresolve_producer(prepare_data):
    return prepare_data


@pytest.fixture
@warmup_param("prepared_profile", profile_main)
def prepared_profile_fixture(prepared_profile):
    return prepared_profile

Snapshot File Overrides

Debug replacement is file-based and CLI-driven.

There are two supported paths:

  • one scoped bundle for the whole run:
    • pytest --warmup-snapshot=path/to/warmup.snapshot.json
  • one targeted fragment for a producer that declares snapshot_id="...":
    • pytest --warmup-snapshot-for inventory-main=path/to/inventory.snapshot.json

Scoped bundle shape:

{
  "version": 1,
  "scopes": {
    "module:tests/test_module.py::prepare_data": {
      "shared": {
        "profile_main": {
          "value": {
            "profile_id": "debug-profile"
          }
        }
      },
      "tests": {
        "tests/test_module.py::test_case": {
          "items_alpha": {
            "value": {
              "items_id": "debug-items"
            }
          }
        }
      }
    }
  }
}

Targeted fragment shape:

{
  "version": 1,
  "shared": {
    "profile_main": {
      "value": {
        "profile_id": "debug-profile"
      }
    }
  },
  "tests": {}
}

Rules:

  • scope_id is computed from the producer scope, a stable container anchor, and the producer fixture name;
  • module, class, function, and session scopes use the usual pytest nodeid-style anchor; package scope uses the package path anchor, for example package:pkg::prepare_data;
  • shared nodes are addressed by id;
  • per-test nodes are addressed by tests[nodeid][id];
  • declarations that are effectively per-test may not be overridden through shared;
  • an empty object means "this node is addressable here, but no explicit override value is provided";
  • {"value": ...} means "use this explicit override value";
  • if one producer matches both a scoped bundle section and a targeted snapshot_id fragment, preparation fails fast instead of applying precedence magic.
  • if --warmup-snapshot-for SNAPSHOT_ID=... is provided, at least one producer in the current run must execute prepare(snapshot_id=SNAPSHOT_ID), or the run fails with a CLI-usage error.

Plans may validate, deserialize, and serialize snapshot values by overriding:

  • WarmupPlan.validate_snapshot_value(...)
  • WarmupPlan.deserialize_snapshot_value(...)
  • WarmupPlan.serialize_snapshot_value(...)

This keeps snapshot semantics plan-local instead of pushing domain conversion logic into the plugin core.

CLI Helpers

The plugin also exposes a small debug-oriented CLI surface:

  • --warmup-snapshot PATH Load a versioned scoped snapshot bundle for the whole run.
  • --warmup-snapshot-for SNAPSHOT_ID=PATH Attach one versioned snapshot fragment to one producer snapshot_id.
  • --warmup-export-template PATH Write a versioned scoped snapshot template for the selected graph and continue the test run.
  • --warmup-report PATH Write a versioned scoped JSON report describing selected roots, normalized nodes, runtime instances, overrides, trace, and batch timings.
  • --warmup-save-on-fail PATH If warmup preparation fails, write a versioned scoped snapshot containing whatever was already materialized.

These debug artifact outputs are single-process tools. When pytest-xdist is active, --warmup-export-template, --warmup-report, and --warmup-save-on-fail fail fast instead of pretending that one shared output file is safe across workers.

Troubleshooting

Common producer-resolution errors usually mean one of these:

  • no producer fixture found in pytest dependency chain ... The decorated test or fixture is not connected to any producer fixture, and no warmup_autoresolve_producer fallback exists.
  • multiple producer fixtures found in pytest dependency chain The current dependency chain exposes more than one prepared producer. Simplify the chain or use producer_fixture="..." to pick one explicitly.
  • producer fixture '...' is not in this dependency chain The named producer exists, but the current test or fixture does not depend on it through ordinary pytest wiring.
  • producer fixture '...' must return a prepared warmup scope The selected fixture returned an ordinary value instead of the prepared scope returned by warmup_mgr.use(...).prepare(...).
  • ... cannot be shared because dependency ... is per-test A shared declaration depends on a branch that is effectively per-test. Either inherit that branch or split the declaration differently.
  • duplicate id '...' within one producer scope id is not a merge key. If reuse was intended, import and reuse the same WarmupRequirement object instead of redeclaring it.

Scope Boundary

pytest-warmup is not trying to be:

  • a general-purpose factory framework;
  • a generic snapshot assertion library;
  • a container or infrastructure manager;
  • a hidden autouse preparation layer;
  • a domain-specific toolkit.

It focuses on one problem: batch creation and targeted distribution of expensive test resources.

Further design details live in:

Development

uv venv .venv
uv pip install --python .venv/bin/python -e ".[dev]"
./.venv/bin/python -m pytest -q
./.venv/bin/python -m build

Before publishing, also do one smoke check from the built wheel in a fresh virtual environment.

Attribution

The initial code, tests, and documentation in this repository were generated and iteratively refined with ChatGPT/Codex plus collaborating agents.

Named collaborating agents from the design and spike process:

  • Lovelace is an adversarial, QA-minded reviewer focused on user-facing clarity. She pushed the ergonomics tests, debug/override edge cases, and the API critiques that kept the package honest from a user perspective.
  • Herschel is a graph-minded, skeptical contributor who prefers explicit execution boundaries over hidden magic. He drove the selected-roots to reachable-subgraph execution model and kept edge-case behavior small and defensible.
  • Chandrasekhar is a no-magic, contract-first contributor focused on clear binding and injection rules. He helped shape the public injection model and the readable fail-fast behavior around overrides and producer discovery.
  • Pauli is a direct, readability-first contributor who focused on the debug surface and shared-vs-per-test semantics. He helped keep snapshot addressing and distributed-declaration behavior explicit and testable.
  • Kuhn is a pragmatic builder who prefers simple, reviewable orchestration over framework cleverness. He contributed to the manager/runtime seams and the explicit lifecycle shape used by the public prototype.

All generated material still requires human review. The repository treats generated output as draft engineering work, not as an authority.

Project details


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

pytest_warmup-0.1.4.tar.gz (35.7 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

pytest_warmup-0.1.4-py3-none-any.whl (22.2 kB view details)

Uploaded Python 3

File details

Details for the file pytest_warmup-0.1.4.tar.gz.

File metadata

  • Download URL: pytest_warmup-0.1.4.tar.gz
  • Upload date:
  • Size: 35.7 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/6.1.0 CPython/3.13.12

File hashes

Hashes for pytest_warmup-0.1.4.tar.gz
Algorithm Hash digest
SHA256 8ff3cc71141571f33bace8e0e540a1a64da4c84eb0d684727956337b67d7af20
MD5 9ae9048c226565684b16396ca99bc48c
BLAKE2b-256 bf9b2616f1e788c8ac09bcab7d03062e8a91127f0ae5056a147a4cf4fc3f68df

See more details on using hashes here.

Provenance

The following attestation bundles were made for pytest_warmup-0.1.4.tar.gz:

Publisher: publish.yml on kaor4bp/pytest-warmup

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

File details

Details for the file pytest_warmup-0.1.4-py3-none-any.whl.

File metadata

  • Download URL: pytest_warmup-0.1.4-py3-none-any.whl
  • Upload date:
  • Size: 22.2 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/6.1.0 CPython/3.13.12

File hashes

Hashes for pytest_warmup-0.1.4-py3-none-any.whl
Algorithm Hash digest
SHA256 428b0b0f5a843817d1fc26894b02804621aff33f7032caf22bf4a363efd99505
MD5 3b5893b85d9dbd0e071aac8b62a940e3
BLAKE2b-256 e840a606324413d1a96e989de6942ce5165b972a8cf88cf68b3b170ce162d985

See more details on using hashes here.

Provenance

The following attestation bundles were made for pytest_warmup-0.1.4-py3-none-any.whl:

Publisher: publish.yml on kaor4bp/pytest-warmup

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page