Changelog¶
All notable changes to pytest-parallex are documented here. Versioning is
SemVer; the version is __version__ in
src/pytest_parallex/__init__.py (hatchling reads it) and is bumped automatically when
shipped source changes. Merging a version bump to main publishes it (see "Releasing" in the
README).
[Unreleased]¶
Fixed¶
- Fork mode: a failing
==in a worker prints pytest's comparison diff again (#2). The worker unregistered theterminalreporterplugin to keep from writing progress to the shared stdout, but pytest'sassertrepr_comparefetches its syntax highlighter throughconfig.get_terminal_writer(), which asserts that plugin is registered. Every failing comparison in a worker therefore surfaced as a bareAssertionErrorfrom_pytest/config/__init__.pywith the left and right values hidden. The worker now keeps the reporter registered and redirects its writer to/dev/nullinstead: nothing reaches the terminal from the child, and the diff comes back.
[0.2.3] - 2026-08-26¶
Fixed¶
- Fork mode: private first-import cache dirs per worker
(#1). matplotlib, numba and
pytensor build a cache on the first import in a process; N forked workers importing into
one cold shared dir raced on half-written files and could crash. Each worker now gets its
own
MPLCONFIGDIR,NUMBA_CACHE_DIRand pytensorbase_compiledir, and the controller removes them at the end. The README documents importing the library pre-fork (a hoisted session fixture) to build the cache once and inherit it.
[0.2.2] - 2026-07-17¶
Fixed¶
- Process mode: coverage data is namespaced per worker and combined on the controller.
Every pytest-cov session ends by combining
$COVERAGE_FILE.*, so N workers sharing the default name combined and deleted each other's data files mid-combine (sqlite3 "no such table",INTERNALERRORexit 3, that worker's remaining tests missing; timing-dependent, so it surfaced as flaky CI). Each worker now measures into.coverage.wNwith--cov-report=and--cov-fail-under=0pinned, and the controller's session-end combine produces the one reported, gated total.
[0.2.1] - 2026-07-16¶
Fixed¶
- Python 3.10 floor.
urllib.requestwas imported lazily insideharden_darwin_fork_child, so a first import under a fakedsys.platform="darwin"ran 3.10'sfrom _scproxy import ...on Linux and died. It is now imported at module top under the real platform, with a fallback that masks the fake platform when_scproxyis missing. - The floor task pinned
pytest==8.1.0, which upstream yanked; moved to 8.1.1. - The fork tests that fake
sys.platform="darwin"and then fork into the macOS-only hardening are skipped off macOS: the Linux CI container segfaulted (exit 139) where every dev box passed. The hardening functions stay unit-tested everywhere without forking.
[0.2.0] - 2026-07-16¶
A deliberate minor with no functional change from 0.1.13. The 0.1.x history (and the published 0.1.11 artifacts) carried a private project name in docstrings; the history was rewritten clean and 0.2.0 starts from it.
[0.1.13] - 2026-07-16¶
Changed¶
- ruff
select = ["ALL"]with every opt-out justified inline, and pyright strict withreportPrivateUsageas the one documented exception. Annotates the test suite and the scheduler, makessubprocesscheck=explicit, drops unused hook params (pluggy passes by name), moves file ops topathlib. - Mac-only tests fork into the real
_scproxyand libinfo crash paths and assert the child dies; paired survival tests assertharden_darwin_fork_childdefuses both.
Fixed¶
scripts/bump-versionleft<file>-Ebackups on macOS: BSD sed read the-Eafter-ias a backup suffix. It now replaces through a temp file.
[0.1.12] - 2026-07-16¶
Docstring polish on the macOS fork-trap code; no functional change.
[0.1.11] - 2026-07-16¶
Fixed¶
- macOS
_scproxyguard fixed at the right layer. 0.1.10 rebound the callers (urllib.request.proxy_bypass/getproxies), which misses botocore's import-timefrom urllib.request import proxy_bypass(a captured reference), and itsno_proxyfallback was wiped by tests that clearos.environ. Verified ineffective on the same Mac that found the bug. Now the*_macosx_sysconfleaves are stubbed, which the original functions resolve at call time, so every caller is covered regardless of when it imported or what the environment says. Verified: 0 dead workers, 530/530 claims, and fork became the fastest configuration on that machine (30.0s vs xdist 38.2s). - macOS: the second fork segfault, DNS resolution, is disarmed in workers. Distinct
from
_scproxyand found on the same suite once that guard held: workers died insocket.getaddrinforeached from httpx opening a cloud endpoint. Minimal reproduction: a child forked from a parent that has ever resolved a real hostname segfaults on its own first lookup (libinfo's mDNSResponder client state does not survive the fork), and the parent has almost always resolved one; pulling a container image is enough.OBJC_DISABLE_INITIALIZE_FORK_SAFETYdoes not cover it. A segfault cannot be caught, so workers now resolve real names in a short-livedpython -I -csubprocess (fresh exec, pristine libinfo) with a per-endpoint cache; numeric addresses stay on the realgetaddrinfovia anAI_NUMERICHOSTfirst attempt. Verified on the suite that found it: 0 dead workers, 187/187 accounted for.
[0.1.10] - 2026-07-16¶
Driven by the first real-world macOS run and a coverage-gated CI adoption.
Added¶
- End-of-run summary line:
pytest-parallex fork: 4 worker(s), 12 claim(s) | session fixtures: 3 hoisted, 1 per-worker. "N passed" is the same string whether the plugin parallelised, degraded, or did nothing; this line is what tells those apart, and a failed hoist now saysFAILED to hoistright in the summary. - Coverage under fork works with
patch = ["fork"]+parallel = true+COVERAGE_PROCESS_START: workers save their data explicitly beforeos._exit(which otherwise skips coverage's exit hook; measured: a 70%-gated suite read 39.8%).
Fixed¶
- macOS: the
_scproxyfork segfault is disarmed in workers (urllib proxy lookup rebound to the environment path;no_proxy='*'set when nothing proxy-ish exists). Found on a real suite: botocore's proxy detection segfaulted every worker, andOBJC_DISABLE_INITIALIZE_FORK_SAFETYdid not help (different trap). The darwin warning now explains both traps. Superseded by the 0.1.11 fix. - A session fixture that fails to hoist no longer poisons workers with the controller's cached exception; the worker builds it fresh and a real failure is attributed to the test that wanted it.
[0.1.9] - 2026-07-16¶
- Warn about the macOS ObjC
+initializefork trap and how to disarm it. - The Python 3.10 floor from
requires-pythonis now tested in CI (test-pytest-floor), not just asserted.
[0.1.8] - 2026-07-16¶
pytest_parallex.__version__mirrors the version inpyproject.toml.
[0.1.7] - 2026-07-16¶
worker_idis provided on plain serial runs too ('main'), so a conftest can request it without--parallel. With pytest-xdist installed and idle, xdist's own'master'wins.
[0.1.6] - 2026-07-16¶
- Fork mode fills every worker.
plan_fork_claimsreplacesgroup_by_scope: a module whose tests keep no package, module or class fixture alive is dealt test-by-test, so a suite with fewer modules than cores no longer caps parallelism at the module count. A module that owns such a fixture still stays on one worker, so the fixture is set up once for it.
[0.1.5] - 2026-07-16¶
- Thread and async modes: function-scoped fixtures were unsound.
FixtureDefkeeps onecached_resultand one_finalizerslist shared across threads, so two workers running two tests shared that one slot and one thread's setup ran while another's teardown cleared it. Those two attributes are now per-thread for function scope; session and module scope stay shared, which is the feature.
[0.1.4] - 2026-07-16¶
- Session-fixture hoisting was silently inert under
filterwarnings = error. pytest 9 warnsPytestRemovedIn10Warningwhengetfixturevalueis called for a node that is not on the setup stack; with warnings as errors that warning became an exception in the hoist path, every fixture fell back to the workers, and the run stayed green. The borrowed item is now pushed onto the setup stack for the hoist. Regression testTestHoistingUnderStrictWarningsasserts the fixture boot count.
[0.1.3] - 2026-07-15¶
- Benchmarks against xdist recorded in
docs/benchmarks.md, including the measurements that looked convincing and were wrong.
[0.1.2] - 2026-07-15¶
- Ship
py.typed; gate formatting in CI; shorten the PyPI blurb so search results do not truncate it.
[0.1.1] - 2026-07-15¶
First published version. 0.1.0 never left this repo: version-guard requires a code change
to carry a version bump, and the first shipped commit bumped past it.
Added¶
--parallel=fork: collect once in the controller, then fork warm workers that inherit the imported suite and the collected items via copy-on-write.- Controller-side session fixtures.
scope="session"runs once, in the controller, before the fork; children inherit the cached result. Under pytest-xdist the same fixture runs once per worker.--parallex-no-session-scoperestores the per-worker behaviour. --parallel=thread,--parallel=async,--parallel=process.--parallel-workers=N(defaultauto= CPU count).- Fork-safety refusal: the run stops with a message naming the offending thread or event loop rather than forking a process that would deadlock in the child.
- Logging
QueueListenerthreads are stopped and restarted around the fork, so a suite that merely imports litestar (or anything usingQueueHandler) can still fork. - Hooks:
pytest_parallex_setup,pytest_parallex_teardown,pytest_parallex_auto_num_workers. - Fixtures:
parallex_worker_id,parallex_mode,parallex_setup_data. - Dynamic work-claiming: workers race for module groups off a shared counter rather than being dealt a static partition, so a slow module can't strand a worker.
- Worker-scoped fixtures. A
scope="session"fixture that requestsworker_id(directly or transitively) is per-worker and is never hoisted to the controller, sopostgres_server(one container) anddatabase_url(worker_id)(one database per worker) can coexist, which is what xdist's filelock recipe exists to fake.worker_idis provided by parallex when--parallelis active, registered late enough to win the name from pytest-xdist; a plainpytest -n 4run keeps xdist's.
Requires¶
- pytest >= 8.1, not 8.0:
FixtureManager.getfixturedefstook a nodeid string until 8.1 and aNodefrom 8.1 on, and the fork runner passes aNode. - Tested against pytest 8.1, 8.4, 9.0 and 9.1 across all four modes. CI pins the floor.