Skip to content

Support injection for generators. #187

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 10 commits into from
May 2, 2025
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
2 changes: 1 addition & 1 deletion docs/integrations/fastapi.md
Original file line number Diff line number Diff line change
Expand Up @@ -98,7 +98,7 @@ and the global context will be set.

## Integrating with FastAPI Using DIContextMiddleware

The `DIContextMiddleware` is can be used to manage context, but its features overlap with the [custom router class](#using-the-a-custom-router-class).
The `DIContextMiddleware` can be used to manage context, but its features overlap with the [custom router class](#using-a-custom-router-class).
The main advantage of using middleware is that you can set it up for your entire `FastAPI` application.

> **Note:** If you want to use both the `DIContextMiddleware` and the custom router class, you should not pass any arguments to `create_fastapi_route_class()`.
Expand Down
24 changes: 0 additions & 24 deletions docs/introduction/application-settings.md

This file was deleted.

144 changes: 144 additions & 0 deletions docs/introduction/generator-injection.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,144 @@
# Injection into Generator Functions


`that-depends` supports dependency injections into generator functions. However, this comes
with some minor limitations compared to regular functions.


## Quickstart

You can use the `@inject` decorator to inject dependencies into generator functions:

=== "async generator"

```python
@inject
async def my_generator(value: str = Provide[Container.factory]) -> typing.AsyncGenerator[str, None]:
yield value
```

=== "sync generator"

```python
@inject
def my_generator(value: str = Provide[Container.factory]) -> typing.Generator[str, None, None]:
yield value
```

=== "async iterator"

```python
@contextlib.asynccontextmanager
@inject
async def my_generator(value: str = Provide[Container.factory]) -> typing.AsyncIterator[str]:
yield value
```

=== "sync iterator"

```python
@contextlib.contextmanager
@inject
def my_generator(value: str = Provide[Container.factory]) -> typing.Iterator[str]:
yield value
```

## Supported Generators

### Synchronous Generators

`that-depends` supports injection into sync generator functions with the following signature:

```python
Callable[P, Generator[<YieldType>, <SendType>, <ReturnType>]]
```

This means that wrapping a sync generator with `@inject` will always preserve all the behaviour of the wrapped generator:

- It will yield as expected
- It will accept sending values via `send()`
- It will raise `StopIteration` when the generator is exhausted or otherwise returns.


### Asynchronous Generators

`that-depends` supports injection into async generator functions with the following signature:

```python
Callable[P, AsyncGenerator[<YieldType>, None]]
```

This means that wrapping an async generator with `@inject` will have the following effects:

- The generator will yield as expected
- The generator will **not** accept values via `asend()`

If you need to send values to an async generator, you can simply resolve dependencies in the generator body:

```python

async def my_generator() -> typing.AsyncGenerator[float, float]:
value = await Container.factory.resolve()
receive = yield value # (1)!
yield receive + value

```

1. This receive will always be `None` if you would wrap this generator with @inject.



## ContextResources

`that-depends` will **not** allow context initialization for [ContextResource](../providers/context-resources.md) providers
as part of dependency injection into a generator.

This is the case for both async and sync injection.

**For example:**
```python
def sync_resource() -> typing.Iterator[float]:
yield random.random()

class Container(BaseContainer):
sync_provider = providers.ContextResource(sync_resource).with_config(scope=ContextScopes.INJECT)
dependent_provider = providers.Factory(lambda x: x, sync_provider.cast)

@inject(scope=ContextScopes.INJECT) # (1)!
def injected(val: float = Provide[Container.dependent_provider]) -> typing.Generator[float, None, None]:
yield val

# This will raise a `ContextProviderError`!
next(_injected())
```

1. Matches context scope of `sync_provider` provider, which is a dependency of the `dependent_provider` provider.


When calling `next(injected())`, `that-depends` will try to initialize a new context for the `sync_provider`,
however, this is not permitted for generators, thus it will raise a `ContextProviderError`.


Keep in mind that if context does not need to be initialized, the generator injection will work as expected:

```python
def sync_resource() -> typing.Iterator[float]:
yield random.random()

class Container(BaseContainer):
sync_provider = providers.ContextResource(sync_resource).with_config(scope=ContextScopes.REQUEST)
dependent_provider = providers.Factory(lambda x: x, sync_provider.cast)

@inject(scope=ContextScopes.INJECT) # (1)!
def injected(val: float = Provide[Container.dependent_provider]) -> typing.Generator[float, None, None]:
yield val


with container_context(scope=ContextScopes.REQUEST):
# This will resolve as expected
next(_injected())
```

Since no context initialization was needed, the generator will work as expected.

1. Scope provided to `@inject` no longer matches scope of the `sync_provider`
7 changes: 4 additions & 3 deletions mkdocs.yml
Original file line number Diff line number Diff line change
Expand Up @@ -7,11 +7,12 @@ nav:
- Introduction:
- Containers: introduction/ioc-container.md
- Dependency Injection: introduction/injection.md
- Scopes: introduction/scopes.md
- Generator Injection: introduction/generator-injection.md
- String Injection: introduction/string-injection.md
- Multiple Containers: introduction/multiple-containers.md
- Application Settings: introduction/application-settings.md
- Scopes: introduction/scopes.md
- Tear-down: introduction/tear-down.md
- Multiple Containers: introduction/multiple-containers.md

- Providers:
- Collections: providers/collections.md
- Context-Resources: providers/context-resources.md
Expand Down
Loading