xdist-safe 'run once' fixture decorator for pytest (setup/teardown across workers)
Project description
pytest-once
xdist-safe "run once" pytest fixture decorator. Setup runs exactly once even with multiple workers (pytest-xdist).
Features
- ✅ Prevents duplicate execution with file lock-based inter-process synchronization
- ✅ Works with or without xdist - seamless integration
- ✅ Simple API - just one decorator
- ✅ Type-safe - full type hints and mypy support
- ✅ No teardown complexity - encourages idempotent setup patterns
Installation
pip install pytest-once
# If using with xdist
pip install pytest-xdist
Quick Start
from pytest_once import once_fixture
import pytest
@once_fixture(autouse=True, scope="session")
def bootstrap_db():
"""Setup database container - runs once across all workers."""
cleanup_old_containers() # Idempotent cleanup
start_db_container()
@pytest.fixture
def client(bootstrap_db): # Explicit dependency
"""Create a client that depends on the database."""
return create_client()
@once_fixture(autouse=True, scope="session")
def seed_data():
"""Load seed data - runs once after bootstrap_db."""
load_seed_dataset()
# You can also explicitly specify the fixture name
@once_fixture("db", autouse=True, scope="session")
def bootstrap_database():
"""Alternative: explicit fixture name."""
start_db_container()
Key Points
- Return values: This decorator doesn't return values. Use a separate regular fixture that depends on the once_fixture if you need to share resources.
- Idempotent setup recommended: Clean up previous runs within setup to ensure safe re-execution.
How It Works
When running tests in parallel with pytest-xdist, normal fixtures run independently in each worker process. Even with scope="session", setup runs multiple times (once per worker), causing:
- ⚠️ Resource waste (e.g., multiple identical containers)
- ⚠️ Port conflicts and runtime errors
- ⚠️ Increased test execution time
pytest-once uses file lock-based synchronization to ensure setup runs exactly once:
- First worker acquires lock and runs setup
- Other workers wait for the lock
- After setup completes, a marker file is created
- Subsequent workers see the marker and skip setup
Teardown Strategy
This decorator does not support teardown. Instead, we recommend these patterns:
Pattern 1: Idempotent Setup (Recommended)
Clean up in the setup phase to ensure safe re-runs:
@once_fixture("db_container", autouse=True, scope="session")
def db_container():
"""Setup with built-in cleanup."""
# Clean up any previous containers
stop_and_remove_old_containers()
# Start fresh container
start_db_container()
Pattern 2: External Cleanup Tools
Use external tools for cleanup:
# Run tests
pytest -n 4
# Clean up after tests
docker-compose down
Pattern 3: CI Auto-Cleanup
Let CI environments handle cleanup automatically:
# GitHub Actions example
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Run tests
run: pytest -n 4
# Environment is automatically destroyed after job completes
Pattern 4: Temporary Files
Use pytest's built-in temporary directory fixtures:
@pytest.fixture(scope="session")
def temp_data(tmp_path_factory):
"""Temporary files are automatically cleaned up."""
data_dir = tmp_path_factory.mktemp("data")
# pytest cleans this up automatically
return data_dir
API Reference
once_fixture(
fixture_name: str | None = None,
*,
scope: str = "session",
autouse: bool = False,
lock_timeout: float = 60.0,
namespace: str = "pytest-once",
)
Parameters
-
fixture_name (optional): Name of the registered fixture. If
None, uses the decorated function's name. Use this when you need to reference the fixture in test dependencies. -
scope: Pytest fixture scope. Default:
"session""session": Once per test session (recommended)"module": Once per test module"class": Once per test class"function": Once per test function
-
autouse: Whether to automatically use this fixture. Default:
FalseTrue: Runs automatically without explicit dependencyFalse: Must be explicitly referenced in test parameters
-
lock_timeout: Timeout in seconds for acquiring the file lock. Default:
60.0- Increase if setup takes longer than 60 seconds
- Decrease for faster failure detection
-
namespace: Directory name under pytest's temp directory for lock files. Default:
"pytest-once"- Change if you need to isolate different test suites
Advanced Examples
Multiple Once Fixtures with Dependencies
@once_fixture(autouse=True, scope="session")
def setup_infrastructure():
"""First: setup infrastructure."""
start_docker_network()
start_database()
@once_fixture(autouse=True, scope="session")
def setup_data(setup_infrastructure):
"""Second: setup data (depends on infrastructure)."""
migrate_database()
load_seed_data()
@pytest.fixture
def api_client(setup_data):
"""Regular fixture that depends on once_fixture."""
return APIClient()
Explicit Fixture Names
@once_fixture("db", autouse=True, scope="session")
def bootstrap_database():
"""Fixture registered as 'db'."""
start_db_container()
@pytest.fixture
def db_client(db): # Reference by explicit name
"""Create client that depends on 'db' fixture."""
return create_db_client()
Custom Lock Timeout
@once_fixture(autouse=True, scope="session", lock_timeout=300.0)
def slow_setup():
"""Setup that takes up to 5 minutes."""
download_large_dataset()
process_data()
Known Limitations
-
No return values: The decorator doesn't return values. Create a separate regular fixture if you need to share resources:
@once_fixture("db_setup", autouse=True, scope="session") def db_setup(): start_database() @pytest.fixture def db_connection(db_setup): """Regular fixture that returns a connection.""" return create_connection()
-
No generator functions: Functions with
yieldare not supported and will raiseTypeError:# ❌ This will raise TypeError @once_fixture(autouse=True, scope="session") def bad_fixture(): setup() yield # Not supported! teardown()
-
Crash recovery: If a worker crashes while holding the lock, the marker file may be left behind. The next run will automatically recover, but you can also manually delete the lock directory or change the
namespaceparameter.
Troubleshooting
Lock Timeout Errors
If you see timeout errors:
TimeoutError: Timed out acquiring lock for once_fixture 'my_fixture'
Solutions:
- Increase
lock_timeoutparameter - Check if a previous worker crashed (restart pytest)
- Manually clean up lock files in pytest's temp directory
Setup Running Multiple Times
If setup runs more than once:
- Verify you're using
scope="session" - Check that
autouse=Trueor the fixture is properly referenced - Ensure the fixture name is unique across your test suite
Import Errors
If you see import errors:
ImportError: cannot import name 'once_fixture'
Solution: Ensure pytest-once is installed:
pip install pytest-once
Contributing
Contributions are welcome! Please see CONTRIBUTING.md for development setup and guidelines.
License
MIT License © 2025 kiarina
Links
- Documentation: GitHub README
- Source Code: GitHub Repository
- Issue Tracker: GitHub Issues
- Changelog: CHANGELOG.md
- PyPI: pytest-once
Project details
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file pytest_once-0.1.0.tar.gz.
File metadata
- Download URL: pytest_once-0.1.0.tar.gz
- Upload date:
- Size: 33.0 kB
- Tags: Source
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.1.0 CPython/3.13.7
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
3b7d4b6a984779e37e954af799270332762c0ddba685ddaf3065edfeccbb8c4a
|
|
| MD5 |
5e614e4f89fabbde7210e6a7a682e6bf
|
|
| BLAKE2b-256 |
2cf5da1d13650b3813d61d715b423efe9c73b42f030497db531f317f9ad816c2
|
File details
Details for the file pytest_once-0.1.0-py3-none-any.whl.
File metadata
- Download URL: pytest_once-0.1.0-py3-none-any.whl
- Upload date:
- Size: 7.5 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.1.0 CPython/3.13.7
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
15b7d651e917f52210afbacc88f205c14f3458043703a39730864ddd16e62e70
|
|
| MD5 |
138648ea9df3efc1c558f3e09900a712
|
|
| BLAKE2b-256 |
c15e0706452985836e85482edf637029ace0e7e905a2d428dbaa31a103ae4eea
|