Metadata-Version: 2.4
Name: modern-di-pytest
Version: 4.0.2
Summary: Pytest integration for modern-di that turns DI dependencies into fixtures
Keywords: dependency-injection,di,ioc-container,modern-di,pytest,fixtures,python,testing
Author: Artur Shiriev
Author-email: Artur Shiriev <me@shiriev.ru>
License-Expression: MIT
Classifier: Development Status :: 5 - Production/Stable
Classifier: Intended Audience :: Developers
Classifier: Framework :: Pytest
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Classifier: Programming Language :: Python :: 3.13
Classifier: Programming Language :: Python :: 3.14
Classifier: Programming Language :: Python :: Free Threading :: 2 - Beta
Classifier: Typing :: Typed
Classifier: Topic :: Software Development :: Libraries
Classifier: Topic :: Software Development :: Testing
Requires-Dist: modern-di>=4.2,<5
Requires-Dist: pytest>=7 ; python_full_version < '3.14'
Requires-Dist: pytest>=7.4 ; python_full_version >= '3.14'
Requires-Python: >=3.11, <4
Project-URL: Homepage, https://modern-di.modern-python.org
Project-URL: Documentation, https://modern-di.modern-python.org/integrations/pytest/
Project-URL: Repository, https://github.com/modern-python/modern-di-pytest
Project-URL: Issues, https://github.com/modern-python/modern-di-pytest/issues
Project-URL: Changelog, https://github.com/modern-python/modern-di-pytest/releases
Description-Content-Type: text/markdown

<p align="center">
  <picture>
    <source media="(prefers-color-scheme: dark)"  srcset="https://raw.githubusercontent.com/modern-python/.github/main/brand/projects/modern-di-pytest/lockup-dark.svg">
    <source media="(prefers-color-scheme: light)" srcset="https://raw.githubusercontent.com/modern-python/.github/main/brand/projects/modern-di-pytest/lockup-light.svg">
    <img alt="modern-di-pytest" src="https://raw.githubusercontent.com/modern-python/.github/main/brand/projects/modern-di-pytest/lockup.png" width="420">
  </picture>
</p>

[![PyPI version](https://img.shields.io/pypi/v/modern-di-pytest.svg)](https://pypi.org/project/modern-di-pytest/)
[![Supported Python versions](https://img.shields.io/pypi/pyversions/modern-di-pytest.svg)](https://pypi.org/project/modern-di-pytest/)
[![Downloads](https://static.pepy.tech/badge/modern-di-pytest/month)](https://pepy.tech/projects/modern-di-pytest)
[![Coverage](https://img.shields.io/badge/coverage-100%25-brightgreen.svg)](https://github.com/modern-python/modern-di-pytest/actions/workflows/ci.yml)
[![CI](https://github.com/modern-python/modern-di-pytest/actions/workflows/ci.yml/badge.svg)](https://github.com/modern-python/modern-di-pytest/actions/workflows/ci.yml)
[![License](https://img.shields.io/github/license/modern-python/modern-di-pytest.svg)](https://github.com/modern-python/modern-di-pytest/blob/main/LICENSE)
[![GitHub stars](https://img.shields.io/github/stars/modern-python/modern-di-pytest)](https://github.com/modern-python/modern-di-pytest/stargazers)
[![uv](https://img.shields.io/endpoint?url=https://raw.githubusercontent.com/astral-sh/uv/main/assets/badge/v0.json)](https://github.com/astral-sh/uv)
[![Ruff](https://img.shields.io/endpoint?url=https://raw.githubusercontent.com/astral-sh/ruff/main/assets/badge/v2.json)](https://github.com/astral-sh/ruff)
[![ty](https://img.shields.io/endpoint?url=https://raw.githubusercontent.com/astral-sh/ty/main/assets/badge/v0.json)](https://github.com/astral-sh/ty)

Pytest integration for [modern-di](https://github.com/modern-python/modern-di). It turns any DI dependency into a pytest fixture with one line.

Full guide: [pytest integration docs](https://modern-di.modern-python.org/integrations/pytest/)

## Installation

```bash
uv add --dev modern-di-pytest      # or: pip install modern-di-pytest
```

## Usage

You provide the root container fixture. Pick whatever pytest scope fits the test suite:

```python
# conftest.py
import typing

import modern_di
import pytest
from modern_di_pytest import expose, modern_di_fixture

from app import ioc
from app.ioc import Dependencies
from app.services import EmailClient


@pytest.fixture
def di_container() -> typing.Iterator[modern_di.Container]:
    with modern_di.Container(groups=ioc.ALL_GROUPS) as container:
        container.validate()  # optional fail-fast; nothing validates the graph implicitly
        yield container


# Bulk: every Provider on each group becomes a pytest fixture
# named after the class attribute. Pass several groups in one call.
expose(Dependencies)

# Manual: turn a single type or Provider into a named fixture.
email_client = modern_di_fixture(EmailClient)
```

Tests then receive resolved dependencies by name:

```python
from app.services import EmailClient, UserService


def test_listing(user_service: UserService) -> None:  # generated by expose(Dependencies)
    assert user_service.list_users() == []


def test_email(email_client: EmailClient) -> None:  # generated manually
    email_client.send("hi")
```

## Fixture names and overrides

`expose()` names each fixture after its provider attribute, so an exposed fixture
follows pytest's usual override rules. It replaces any fixture with the same name
from pytest itself, from plugins, or from a parent conftest, for every test under
that conftest. A provider named `tmp_path` replaces pytest's `tmp_path`.

`expose()` raises `ValueError` if the module it installs into already defines a
name it would install, whether that name is a fixture or any other attribute. It
cannot see fixtures defined elsewhere, so it does not report those.

To avoid a clash, rename the provider or the existing attribute. To keep both
names, do not pass that group to `expose()` and create its fixtures with
`modern_di_fixture()`, giving the clashing one another name:

```python
app_settings = modern_di_fixture(Dependencies.settings)
user_service = modern_di_fixture(Dependencies.user_service)
```

Call `expose()` after the conftest's own fixtures. A fixture defined after the
call replaces the exposed fixture of the same name, and `expose()` cannot detect
that.

## Pointing a fixture at a child container

```python
import typing

import modern_di
import pytest
from modern_di_pytest import modern_di_fixture

from app.services import UserService


@pytest.fixture
def request_container(
    di_container: modern_di.Container,
) -> typing.Iterator[modern_di.Container]:
    with di_container.build_child_container(scope=modern_di.Scope.REQUEST) as container:
        yield container


request_user_service = modern_di_fixture(UserService, container_fixture="request_container")
```

## Overrides

Use `Container.override()`; overrides are shared across the container tree.
Generated fixtures resolve during test setup, so apply the override in a fixture
they depend on. Overriding `di_container` in the test module does that:

```python
import typing

import modern_di
import pytest

from app.ioc import Dependencies
from app.services import UserService
from tests.fakes import FakeRepo


@pytest.fixture
def di_container(di_container: modern_di.Container) -> typing.Iterator[modern_di.Container]:
    with di_container.override(Dependencies.user_repo, FakeRepo()):
        yield di_container


def test_with_override(user_service: UserService) -> None:
    assert user_service.list_users() == []
```

## API

### `modern_di_fixture(dependency, *, container_fixture="di_container", name=None, pytest_scope="function")`

Turn a single dependency into a pytest fixture. ``dependency`` is either a
type or a Provider; the generated fixture resolves it through
``container.resolve_dependency``. The returned object is a real pytest fixture;
assign it to a module-level name and pytest will collect it.

### `expose(*groups, container_fixture="di_container", pytest_scope="function", module=None)`

Walk each ``Group`` subclass in ``groups`` and install one pytest fixture per
Provider class attribute onto the caller's module. Fixture names equal the
class-attribute names. Non-Provider class attributes are skipped. A duplicate
attribute name across groups raises ``ValueError``, as does a provider named
the same as ``container_fixture`` or a name the target module already defines.
Calling it with no groups raises ``TypeError``. Pass ``module=`` explicitly when
stack introspection cannot identify the caller.

## 📦 [PyPI](https://pypi.org/project/modern-di-pytest)

## 📝 [License](https://github.com/modern-python/modern-di-pytest/blob/main/LICENSE)

## Part of `modern-python`

Built on [`modern-di`](https://github.com/modern-python/modern-di), a dependency-injection framework with an IoC container and scopes.

Browse the full list of templates and libraries in
[`modern-python`](https://github.com/modern-python); the org profile has the categorized index.
