Skip to main content

xdist-safe 'run once' fixture decorator for pytest (setup/teardown across workers)

Project description

pytest-once

CI PyPI version Python versions License: MIT

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:

  1. First worker acquires lock and runs setup
  2. Other workers wait for the lock
  3. After setup completes, a marker file is created
  4. 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: False

    • True: Runs automatically without explicit dependency
    • False: 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

  1. 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()
    
  2. No generator functions: Functions with yield are not supported and will raise TypeError:

    # ❌ This will raise TypeError
    @once_fixture(autouse=True, scope="session")
    def bad_fixture():
        setup()
        yield  # Not supported!
        teardown()
    
  3. 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 namespace parameter.

Troubleshooting

Lock Timeout Errors

If you see timeout errors:

TimeoutError: Timed out acquiring lock for once_fixture 'my_fixture'

Solutions:

  • Increase lock_timeout parameter
  • 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:

  1. Verify you're using scope="session"
  2. Check that autouse=True or the fixture is properly referenced
  3. 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

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_once-0.1.0.tar.gz (33.0 kB view details)

Uploaded Source

Built Distribution

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

pytest_once-0.1.0-py3-none-any.whl (7.5 kB view details)

Uploaded Python 3

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

Hashes for pytest_once-0.1.0.tar.gz
Algorithm Hash digest
SHA256 3b7d4b6a984779e37e954af799270332762c0ddba685ddaf3065edfeccbb8c4a
MD5 5e614e4f89fabbde7210e6a7a682e6bf
BLAKE2b-256 2cf5da1d13650b3813d61d715b423efe9c73b42f030497db531f317f9ad816c2

See more details on using hashes here.

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

Hashes for pytest_once-0.1.0-py3-none-any.whl
Algorithm Hash digest
SHA256 15b7d651e917f52210afbacc88f205c14f3458043703a39730864ddd16e62e70
MD5 138648ea9df3efc1c558f3e09900a712
BLAKE2b-256 c15e0706452985836e85482edf637029ace0e7e905a2d428dbaa31a103ae4eea

See more details on using hashes here.

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