Migrating from pytest¶
tiderace is its own framework, not a pytest wrapper (ADR-E001 / E012) — it does not run pytest at runtime. Migration is a one-time source codemod: tiderace migrate rewrites a pytest test file into tiderace's native model, and after that pytest is never in the loop.
You don't have to migrate — tiderace already runs ordinary pytest-style function, method, and unittest.TestCase tests with fixtures. Migration is for suites that want to drop the pytest dependency entirely and author against tiderace's type-driven model.
How it works¶
The codemod only ast.parses your source, so pytest need not be installed to migrate.
# Report only — writes nothing. Inspect what would change first.
python -m tiderace.migrate tests/test_foo.py
# Apply — writes tests/test_foo.tiderace.py alongside the original.
python -m tiderace.migrate tests/test_foo.py --write
The exit code is non-zero while anything remains in the can't-map list, so you can wire the report straight into an adoption gate ("fail CI until this suite is fully migratable").
Formatting
The rewrite uses stdlib ast + ast.unparse: it normalizes formatting and drops comments in the written output. The report is exact regardless. (A later version may swap in libcst to preserve formatting.)
What migrates automatically¶
tiderace resolves fixtures by type — the return type of a provider is what a test parameter wires to. The codemod translates the mechanical parts of pytest to that model:
| pytest | → tiderace | notes |
|---|---|---|
import pytest | import tiderace | |
@pytest.fixture | @tiderace.provides | scope= / autouse= / name= carried over |
fixture with -> T return type | @tiderace.provides + inject-by T | the type is what tests wire to |
def test(db) where db is a typed fixture | def test(db: Db) | type inferred from the provider's return type |
@pytest.mark.parametrize("a,b", [...]) | @tiderace.cases([...]) | ids= preserved |
@pytest.mark.skipif(c, reason=r) | @tiderace.skip_if(c, reason=r) | |
@pytest.mark.skip / xfail | @tiderace.skip / @tiderace.xfail | |
@pytest.mark.<name> (other) | @tiderace.tag("<name>") | selection metadata |
What needs a human (the report names each one)¶
Because tiderace wires by type and pytest fixtures rarely carry types, these are flagged for you rather than guessed:
- Untyped fixture — no
-> Tand no inferable type. The decorator is rewritten but flagged: add-> <Type>. (This is the single biggest migration cost of type-DI — owned openly.) - Test param off an untyped fixture — can't annotate
param: ?; flagged for manual annotation. - Parametrized fixture (
@pytest.fixture(params=[...])) — provider-level params aren't in tiderace yet; convert to@tiderace.caseson the tests, or split the resource. request(incl.request.getfixturevalue/request.addfinalizer) — dynamic; port to typed deps +yieldteardown.@pytest.mark.usefixtures("x")— a string name carries no type; request it as a typed param, or mark the providerautouse=True.- pytest builtins (
tmp_path,monkeypatch,capsys, …) — provide your own resource. pytest_*hooks /from pytest import …— port manually.
After migrating¶
The output authors against tiderace only. An untyped provider fails at import — tiderace needs the type — which is deliberate: the gap fails loudly rather than silently keeping you on pytest. Add the types the report asks for, then run with the engine (see Quick Start); no pytest in the loop.
How complete is it?¶
Auto-map rate is tracked against pinned real-world suites (conformance/):
| repo | auto-mapped |
|---|---|
| pallets/click | 95% |
| agronholm/anyio | 99% |
| pallets/flask | 83% |
| overall | 91% |
The remaining gap is dominated by untyped providers and untyped fixture params — exactly the cases above where the codemod flags rather than guesses, because inventing a type would be wrong more often than right.