Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 4 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,8 @@ Override dependencies with context managers, keep tests isolated, and restore th
<picture>
<source media="(prefers-color-scheme: dark)" srcset="docs/pages/img/benchmarks_scoped_dark.svg">
<source media="(prefers-color-scheme: light)" srcset="docs/pages/img/benchmarks_scoped_light.svg">
<img alt="Scoped Dependency Injection Performance" src="docs/pages/img/benchmarks_scoped_light.svg" height="300">
<img alt="<img
alt="Bar chart comparing FastAPI dependency injection throughput (100k requests). Requests/sec: Manual wiring 11,044; Wireup 11,030; Wireup Class-Based 10,976; Dishka 8,538; Svcs 8,394; Aioinject 8,177; diwire 7,390; That Depends 4,892; FastAPI Depends 3,950; Injector 3,192; Dependency Injector 2,576; Lagom 898. Libraries marked † use simplified benchmark implementations and represent upper-bound performance."" src="docs/pages/img/benchmarks_scoped_light.svg" height="300">
</picture>
</p>

Expand Down Expand Up @@ -251,7 +252,7 @@ Wireup keeps the API small, but it is built for larger application graphs.
| Interfaces and protocols | [`as_type=...`](https://maldoinc.github.io/wireup/latest/interfaces/) or factory return annotations |
| Multiple implementations | [Qualifiers](https://maldoinc.github.io/wireup/latest/interfaces/) |
| All implementations | [`Sequence[T]` or `Mapping[Hashable, T]`](https://maldoinc.github.io/wireup/latest/interfaces/#collection-injection) |
| Isolated scopes with explicit context sharing (batch jobs, fan-out tasks, multi-tenant processing) | [`container.enter_scope({...})`](https://maldoinc.github.io/wireup/latest/lifetimes_and_scopes/#sharing-context-across-scopes) |
| Isolated scopes with explicit context sharing (batch jobs, fan-out tasks) | [`container.enter_scope({...})`](https://maldoinc.github.io/wireup/latest/lifetimes_and_scopes/#sharing-context-across-scopes) |
| Environment-specific graph | [Conditional registration](https://maldoinc.github.io/wireup/latest/conditional_registration/) with normal Python |
| Generic repositories/services | [Generic dependencies](https://maldoinc.github.io/wireup/latest/generic_dependencies/) |
| Modular or parametrized registration | [Reusable bundles](https://maldoinc.github.io/wireup/latest/reusable_bundles/) |
Expand Down Expand Up @@ -431,7 +432,7 @@ Wireup decorators only collect metadata. Injectables are plain classes and funct
Swap dependencies during tests with `container.override`:

```python
with container.override.injectable(target=Database, new=in_memory_database):
with container.override({Database: in_memory_database}):
# Injectables that depend on Database will receive in_memory_database
# for the duration of this context manager
response = client.get("/users")
Expand Down
2 changes: 1 addition & 1 deletion docs/pages/container.md
Original file line number Diff line number Diff line change
Expand Up @@ -135,7 +135,7 @@ db_url = container.config.get("database_url")
Substitute dependencies for testing. Access via `container.override`.

```python
with container.override.injectable(target=Database, new=mock_db):
with container.override({Database: mock_db}):
... # All injections of Database use mock_db
```

Expand Down
2 changes: 1 addition & 1 deletion docs/pages/getting_started.md
Original file line number Diff line number Diff line change
Expand Up @@ -254,7 +254,7 @@ To substitute dependencies on targets such as views in a web application you can
the fly.

```python
with container.override.injectable(WeatherService, new=test_weather_service):
with container.override({WeatherService: test_weather_service}):
response = client.get("/weather/forecast")
```

Expand Down
7 changes: 3 additions & 4 deletions docs/pages/integrations/aiohttp/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -149,10 +149,9 @@ def test_override(aiohttp_client):
def greet(self, name: str) -> str:
return f"Hi, {name}"

with get_app_container(app).override.injectable(
GreeterService,
new=DummyGreeter(),
):
container = get_app_container(app)

with container.override({GreeterService: DummyGreeter()}):
res = aiohttp_client.get("/greet?name=Test")
```

Expand Down
4 changes: 1 addition & 3 deletions docs/pages/integrations/asgi/generic_asgi.md
Original file line number Diff line number Diff line change
Expand Up @@ -80,9 +80,7 @@ def create_app():

def test_override():
app = create_app()
with app.state.container.override.injectable(
MyService, new=MyFakeService()
):
with app.state.container.override({MyService: MyFakeService()}):
...
```

Expand Down
2 changes: 1 addition & 1 deletion docs/pages/integrations/click/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -92,7 +92,7 @@ def test_random_number_command():
return 4

# Create test container with mocked service
with container.override.injectable(RandomService, new=MockRandomService()):
with container.override({RandomService: MockRandomService()}):
runner = CliRunner()
result = runner.invoke(cli, ["random-number"])

Expand Down
2 changes: 1 addition & 1 deletion docs/pages/integrations/django/request_time_injection.md
Original file line number Diff line number Diff line change
Expand Up @@ -101,4 +101,4 @@ Prefer `@inject` for request-time functions and `@inject_app` for non-request en
See [Django Testing](testing.md) for endpoint tests and dependency overrides. These patterns are tested the same way:

- call endpoints with `Client` or `AsyncClient`
- use `get_app_container().override.injectable(...)` to inject fakes
- use `get_app_container().override({...})` to inject fakes
14 changes: 7 additions & 7 deletions docs/pages/integrations/django/testing.md
Original file line number Diff line number Diff line change
Expand Up @@ -62,9 +62,9 @@ def test_override_http_endpoint():
return f"Hi {name}"

client = Client()
with get_app_container().override.injectable(
GreeterService, new=FakeGreeter()
):
container = get_app_container()

with container.override({GreeterService: FakeGreeter()}):
response = client.get("/greet/?name=World")

assert response.status_code == 200
Expand All @@ -87,9 +87,9 @@ def test_override_command():
return f"Hi {name}"

out = StringIO()
with get_app_container().override.injectable(
GreeterService, new=FakeGreeter()
):
container = get_app_container()

with container.override({GreeterService: FakeGreeter()}):
call_command("greet", "--name=World", stdout=out)

assert out.getvalue().strip() == "Hi World"
Expand All @@ -101,4 +101,4 @@ DRF and Ninja handlers should use `@inject` explicitly. Their tests follow the s

- call endpoint via test client
- assert response
- use `override.injectable(...)` when needed
- use `container.override({...})` when needed
12 changes: 6 additions & 6 deletions docs/pages/integrations/fastapi/class_based_handlers.md
Original file line number Diff line number Diff line change
Expand Up @@ -122,9 +122,9 @@ from wireup.integration.fastapi import get_app_container


def test_user_handler(app):
with get_app_container(app).override.injectable(
UserProfileService, new=MockUserService()
):
container = get_app_container(app)

with container.override({UserProfileService: MockUserService()}):
# Start the client INSIDE the override block
# The handler is initialized with the mock during startup
with TestClient(app) as client:
Expand All @@ -133,11 +133,11 @@ def test_user_handler(app):

```python title="Don't"
def test_user_handler_wrong(app):
container = get_app_container(app)

# Handler has already been instantiated at startup.
with TestClient(app) as client:
with get_app_container(app).override.injectable(
UserProfileService, new=MockUserService()
):
with container.override({UserProfileService: MockUserService()}):
client.get("/users/")
```

Expand Down
9 changes: 4 additions & 5 deletions docs/pages/integrations/fastapi/request_time_injection.md
Original file line number Diff line number Diff line change
Expand Up @@ -173,11 +173,11 @@ def test_require_auth_denied():


def test_require_auth_allowed():
container = get_app_container(app)

with TestClient(app) as client:
# Override AuthService to return True
with get_app_container(app).override.injectable(
AuthService, new=MockAuthService(allow=True)
):
with container.override({AuthService: MockAuthService(allow=True)}):
response = client.get("/users")
assert response.status_code == 200
```
Expand All @@ -193,8 +193,7 @@ def test_request_middleware_runs():

!!! tip

Use `get_app_container(app).override.injectable()` to inject mocks and fakes during tests. This works for both route
decorators and middleware.
Use `get_app_container(app).override()` to inject mocks and fakes during tests. This works for both route decorators and middleware.

## Direct Container Access

Expand Down
16 changes: 7 additions & 9 deletions docs/pages/integrations/fastapi/testing.md
Original file line number Diff line number Diff line change
Expand Up @@ -36,9 +36,9 @@ def test_override(app: FastAPI):
def greet(self, name: str) -> str:
return f"Hi, {name}"

with get_app_container(app).override.injectable(
GreeterService, new=DummyGreeter()
):
container = get_app_container(app)

with container.override({GreeterService: DummyGreeter()}):
with TestClient(app) as client:
response = client.get("/greet?name=Test")

Expand Down Expand Up @@ -73,24 +73,22 @@ from fastapi import FastAPI
from fastapi.testclient import TestClient
import wireup
import wireup.integration.fastapi
from wireup import InjectableOverride
from wireup.integration.fastapi import get_app_container


@contextlib.contextmanager
def create_test_app(
overrides: list[InjectableOverride] | None = None,
overrides: dict[type[Any], Any] | None = None,
) -> Iterator[FastAPI]:
app = create_app() # Create app, add routes, setup Wireup.
container = get_app_container(app)

with get_app_container(app).override.injectables(overrides or []):
with container.override(overrides or {}):
yield app


def test_user_handler_with_override():
overrides = [
InjectableOverride(target=UserProfileService, new=MockUserService())
]
overrides = {UserProfileService: MockUserService()}

# Override first, then start app lifecycle.
with create_test_app(overrides=overrides) as app:
Expand Down
7 changes: 3 additions & 4 deletions docs/pages/integrations/flask/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -93,10 +93,9 @@ def test_override():
def greet(self, name: str) -> str:
return f"Hi, {name}"

with get_app_container(app).override.injectable(
GreeterService,
new=DummyGreeter(),
):
container = get_app_container(app)

with container.override({GreeterService: DummyGreeter()}):
res = self.client.get("/greet?name=Test")
```

Expand Down
2 changes: 1 addition & 1 deletion docs/pages/integrations/starlette/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -144,7 +144,7 @@ class UppercaseGreeter(GreeterService):
def test_override():
container = get_app_container(app)

with container.override.injectable(GreeterService, new=UppercaseGreeter()):
with container.override({GreeterService: UppercaseGreeter()}):
response = client.get("/hello", params={"name": "world"})

assert response.text == "HELLO WORLD"
Expand Down
2 changes: 1 addition & 1 deletion docs/pages/migrate_to_wireup/dependency_injector.md
Original file line number Diff line number Diff line change
Expand Up @@ -841,7 +841,7 @@ Both libraries support context-manager based overrides in tests.

service_mock = MagicMock(spec=UserService)

with container.override.injectable(UserService, new=service_mock):
with container.override({UserService: service_mock}):
assert container.get(UserService) is service_mock
# UserService is back to normal after the block.
```
Expand Down
4 changes: 1 addition & 3 deletions docs/pages/migrate_to_wireup/fastapi_depends.md
Original file line number Diff line number Diff line change
Expand Up @@ -768,9 +768,7 @@ You still test FastAPI with


def test_get_user(app: FastAPI):
with get_app_container(app).override.injectable(
UserService, new=FakeUserService()
):
with get_app_container(app).override({UserService: FakeUserService()}):
with TestClient(app) as client:
response = client.get("/users/123")

Expand Down
64 changes: 29 additions & 35 deletions docs/pages/testing.md
Original file line number Diff line number Diff line change
Expand Up @@ -93,7 +93,7 @@ class TestGreeter(GreeterService):


def test_greet_with_override(container, client: TestClient) -> None:
with container.override.injectable(GreeterService, new=TestGreeter()):
with container.override({GreeterService: TestGreeter()}):
response = client.get("/greet", params={"name": "World"})

assert response.status_code == 200
Expand Down Expand Up @@ -132,7 +132,7 @@ See [Lifetimes & Scopes](lifetimes_and_scopes.md) for the lifetime rules behind

## Override Dependencies

Use overrides when the app/container should stay real, but one dependency should be replaced for the test.
Use overrides when the app/container should stay real, but one or more dependencies should be replaced for the test.
If you want to choose different registrations before the container is created, see
[Conditional Registration](conditional_registration.md).

Expand All @@ -144,49 +144,47 @@ applies to objects already created in the current scope.
Apply overrides before the first resolution of the object you want to affect. If your integration resolves objects at
startup, apply overrides before creating the test client or starting the app.

When using `as_type`, override the `as_type` target, not the concrete implementation. When overriding a qualified
dependency, include the qualifier in the override target.

### Override One Dependency
Pass a mapping of dependency targets to replacement values to `container.override(...)`. Use the same API for one
override, several overrides, and qualified dependencies.

```python
import pytest
from unittest.mock import MagicMock

from wireup import qualified


async def test_notification_service(container) -> None:
fake_email_client = MagicMock(spec=EmailClient)

with container.override.injectable(EmailClient, new=fake_email_client):
with container.override({EmailClient: fake_email_client}):
notifier = await container.get(NotificationService)
notifier.send_welcome_email("alice@example.com")

fake_email_client.send.assert_called_once()
```

### Override Multiple Dependencies

When several injected dependencies should be replaced together, use `container.override.injectables(...)`.

```python
import pytest
from unittest.mock import MagicMock
from wireup import InjectableOverride


async def test_checkout(container) -> None:
user_service_mock = MagicMock()
order_service_mock = MagicMock()
overrides = {
UserService: user_service_mock,
OrderService: order_service_mock,
}

overrides = [
InjectableOverride(target=UserService, new=user_service_mock),
InjectableOverride(target=OrderService, new=order_service_mock),
]

with container.override.injectables(overrides=overrides):
with container.override(overrides):
checkout_service = await container.get(CheckoutService)


async def test_cache_refresh(container) -> None:
fake_cache = MagicMock(spec=Cache)

with container.override({qualified(Cache, "redis"): fake_cache}):
cache_refresher = await container.get(CacheRefresher)
```

When using `as_type`, override the `as_type` target, not the concrete implementation. When overriding a qualified
dependency, build the override key with `qualified(TargetType, qualifier)`.

## Global Overrides with Fixtures

If many tests need the same auth or user setup, put those Wireup overrides in a fixture instead of repeating them in
Expand All @@ -208,34 +206,30 @@ class AllowAllAuth(AuthService):
@pytest.fixture
def app(request):
app = create_app()
overrides = getattr(request, "param", [])
overrides = getattr(request, "param", {})

if not overrides:
yield app
return

container = get_app_container(app)
with container.override.injectables(overrides=overrides):
with container.override(overrides):
yield app
```

```python title="test_admin.py"
import pytest
from wireup import InjectableOverride

from myapp.auth import AllowAllAuth, AuthenticatedUser, AuthService


@pytest.mark.parametrize(
"app",
[
[
InjectableOverride(target=AuthService, new=AllowAllAuth()),
InjectableOverride(
target=AuthenticatedUser,
new=AuthenticatedUser(id="test-user", is_admin=True),
),
]
{
AuthService: AllowAllAuth(),
AuthenticatedUser: AuthenticatedUser(id="test-user", is_admin=True),
}
],
indirect=True,
)
Expand All @@ -245,7 +239,7 @@ def test_admin_dashboard(client) -> None:
assert response.status_code == 200
```

If most tests in a module need the same setup, you can apply the same override list from a shared fixture instead of
If most tests in a module need the same setup, you can apply the same override mapping from a shared fixture instead of
repeating it in every test.

## Next Steps
Expand Down
Loading
Loading