pytest-casewise-package-install 0.3.0
A pytest plugin for test case-level dynamic dependency management
License: MIT
Requires Python: >=3.8
Keywords: pytest, plugin, package, dependency, isolation, testing

pytest-casewise-package-install
================================

A pytest plugin that emulates per-test package installation by generating deterministic stub packages. It lets you validate version-sensitive logic without reaching remote indexes or polluting the interpreter that runs your suite.

Why it helps
------------

- declare package requirements directly on tests through the `with_packages` decorator or marker exposed via the package `pytest_casewise_package_install.markers`
- bundle dependency requirements with parametrized data using helpers from `pytest_casewise_package_install.parametrize_support`
- run short-lived package environments backed by `LocalMultiPackageManager`, which caches stub directories under `~/.local_package_cache`
- rely on `package_env` and `dynamic_packages` fixtures for context-managed or ad hoc installs defined in `pytest_casewise_package_install.fixtures`
- keep the runner clean because `PackageInstallPlugin` backs up and restores `sys.path`, `PYTHONPATH`, and imported modules for every test case

> The manager creates lightweight modules (such as `requests`, `numpy`, `pandas`, `click`, `transformers`, and more) that expose `__version__` and minimal APIs. The process is offline-only; no real `pip install` occurs.

Installation
------------

```bash
pip install pytest-casewise-package-install
```

The plugin auto-registers through the `casewise-package-install` entry point. Importing the package or enabling it in `pytest.ini` is not required.

Quick start
-----------

### decorator usage

```python
from pytest_casewise_package_install import with_packages


@with_packages({"requests": "2.31.0"})
def test_requests_stub():
    import requests

    assert requests.__version__ == "2.31.0"
```

### marker usage

```python
import pytest


@pytest.mark.with_packages(packages={"requests": "2.28.0"})
def test_requests_marker():
    import requests

    assert requests.__version__ == "2.28.0"
```

### multiple packages

```python
from pytest_casewise_package_install import with_packages


@with_packages({"certifi": "2023.7.22", "urllib3": "2.0.4"})
def test_http_stack_versions():
    import certifi
    import urllib3

    assert certifi.__version__ == "2023.7.22"
    assert urllib3.__version__ == "2.0.4"
```

Parametrized tests
------------------

Use `param_with_packages` to pair fixtures with dependency declarations. Each parameter set receives its own `with_packages` mark so the plugin can discover requirements.

```python
import pytest
from pytest_casewise_package_install import param_with_packages


@pytest.mark.parametrize(
    "version, packages",
    [
        param_with_packages("2.31.0", packages={"requests": "2.31.0"}, id="requests-2-31"),
        param_with_packages("2.28.0", packages={"requests": "2.28.0"}, id="requests-2-28"),
        param_with_packages("2.25.0", packages={"requests": "2.25.0"}, id="requests-2-25"),
    ],
)
def test_requests_matrix(version, packages):
    import requests

    assert requests.__version__ == version
```

For larger tables reuse `create_model_test_params`:

```python
from pytest_casewise_package_install import create_model_test_params


MODEL_CONFIGS = [
    {
        "model_name": "bert-base-uncased",
        "packages": {"transformers": "4.30.0", "torch": "2.0.0"},
        "id": "bert-transformers-4-30",
    },
    {
        "model_name": "gpt2",
        "packages": {"transformers": "4.35.0", "torch": "2.1.0"},
        "id": "gpt2-transformers-4-35",
    },
]


@pytest.mark.parametrize("model_name, packages", create_model_test_params(MODEL_CONFIGS))
def test_models(model_name, packages):
    import transformers

    assert transformers.__version__ in {"4.30.0", "4.35.0"}
```

Fixtures
--------

- `package_env`: returns a manager that applies package sets inside a context manager and restores the environment afterward.
- `dynamic_packages`: installs stub packages in-place without automatic teardown when you need to perform multiple assertions after importing modules.

```python
from pytest_casewise_package_install import package_env


def test_numpy_context(package_env):
    with package_env.use_packages({"numpy": "1.24.0"}):
        import numpy as np

        assert np.__version__ == "1.24.0"
```

Stub catalog and cache
----------------------

`LocalMultiPackageManager` maintains two directories:

- `~/.local_package_cache/<package>_<version>` holds generated stubs and metadata about schema version and requested version
- `~/.local_package_cache/active/<package>` mirrors the selected version and is prepended to `sys.path`

Stub generators ship for:

- requests
- numpy
- pandas
- pyyaml (available through the `yaml` alias)
- click
- transformers
- pillow (available through `PIL`)
- tokenizers
- librosa
- six
- certifi
- urllib3

Any other package receives a minimal stub with only `__version__` defined. If a stub cannot be generated successfully the plugin marks the status as `FAILED` and the test exits with `pytest.fail`.

Configuration
-------------

Override defaults in `pytest.ini` or `setup.cfg`:

```ini
[pytest]
casewise_cache_dir = ~/.my_casewise_cache
casewise_auto_cleanup = true
casewise_install_timeout = 300
casewise_verbose = true
```

Environment variables take precedence over config files:

```bash
export CASEWISE_CACHE_DIR=~/.my_casewise_cache
export CASEWISE_AUTO_CLEANUP=true
export CASEWISE_INSTALL_TIMEOUT=300
export CASEWISE_VERBOSE=true
```

The `PluginConfig` loader (see `pytest_casewise_package_install.config`) normalizes these values and falls back to `~/.local_package_cache`, automatic cleanup, a 300 second timeout placeholder, and non-verbose logging.

Test lifecycle
--------------

`PackageInstallPlugin` (in `pytest_casewise_package_install.plugin`) wires into pytest hooks:

1. discover requested packages from parametrized values, markers, or decorator metadata
2. back up `PYTHONPATH`, `sys.path`, and the set of already imported modules before altering the environment
3. ensure required stubs are ready through `LocalMultiPackageManager`
4. prepend stub directories to `sys.path`, refresh imported modules, and expose environment variables so child processes inherit the setup
5. run the test body
6. restore the previous interpreter state and remove modules imported during the test

If the package requirement is not a dictionary the plugin raises `pytest.fail` with a descriptive message to avoid silent misconfiguration.

Examples
--------

Browse the `examples/` directory for working scenarios:

- `test_basic_usage.py`: decorator versus marker usage, multi-package sets, cache reuse
- `test_parametrize_simple.py`: matrix parametrization, helper usage, shared caching
- `test_fixture_usage.py`: `package_env` and `dynamic_packages` in practice
- `test_advanced_usage.py`: class-based tests and mixing strategies

Run them locally:

```bash
cd examples
pytest -v -s
```

Development
-----------

```bash
git clone https://github.com/yourusername/pytest-casewise-package-install
cd pytest-casewise-package-install
pip install -e .
pytest tests -v
```

License
-------

MIT License