Skip to content

Commit 5c9a7a7

Browse files
Support injection for generators. (#187)
* Initial implementation of generator injection for async functions. * Added injection for sync generators. * Added tests. * Added additional type info to typing.Generator. * Added additional type info to typing.AsyncGenerator. * Added receive, send & return tests for sync generators. * Added remaining tests for generators with context-resources. * Added test for context resources with different scope. * Added generator injection documentation. * Cleaned up after review.
1 parent 3656362 commit 5c9a7a7

File tree

6 files changed

+563
-37
lines changed

6 files changed

+563
-37
lines changed

docs/integrations/fastapi.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -98,7 +98,7 @@ and the global context will be set.
9898

9999
## Integrating with FastAPI Using DIContextMiddleware
100100

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

104104
> **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()`.

docs/introduction/application-settings.md

Lines changed: 0 additions & 24 deletions
This file was deleted.
Lines changed: 144 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,144 @@
1+
# Injection into Generator Functions
2+
3+
4+
`that-depends` supports dependency injections into generator functions. However, this comes
5+
with some minor limitations compared to regular functions.
6+
7+
8+
## Quickstart
9+
10+
You can use the `@inject` decorator to inject dependencies into generator functions:
11+
12+
=== "async generator"
13+
14+
```python
15+
@inject
16+
async def my_generator(value: str = Provide[Container.factory]) -> typing.AsyncGenerator[str, None]:
17+
yield value
18+
```
19+
20+
=== "sync generator"
21+
22+
```python
23+
@inject
24+
def my_generator(value: str = Provide[Container.factory]) -> typing.Generator[str, None, None]:
25+
yield value
26+
```
27+
28+
=== "async iterator"
29+
30+
```python
31+
@contextlib.asynccontextmanager
32+
@inject
33+
async def my_generator(value: str = Provide[Container.factory]) -> typing.AsyncIterator[str]:
34+
yield value
35+
```
36+
37+
=== "sync iterator"
38+
39+
```python
40+
@contextlib.contextmanager
41+
@inject
42+
def my_generator(value: str = Provide[Container.factory]) -> typing.Iterator[str]:
43+
yield value
44+
```
45+
46+
## Supported Generators
47+
48+
### Synchronous Generators
49+
50+
`that-depends` supports injection into sync generator functions with the following signature:
51+
52+
```python
53+
Callable[P, Generator[<YieldType>, <SendType>, <ReturnType>]]
54+
```
55+
56+
This means that wrapping a sync generator with `@inject` will always preserve all the behaviour of the wrapped generator:
57+
58+
- It will yield as expected
59+
- It will accept sending values via `send()`
60+
- It will raise `StopIteration` when the generator is exhausted or otherwise returns.
61+
62+
63+
### Asynchronous Generators
64+
65+
`that-depends` supports injection into async generator functions with the following signature:
66+
67+
```python
68+
Callable[P, AsyncGenerator[<YieldType>, None]]
69+
```
70+
71+
This means that wrapping an async generator with `@inject` will have the following effects:
72+
73+
- The generator will yield as expected
74+
- The generator will **not** accept values via `asend()`
75+
76+
If you need to send values to an async generator, you can simply resolve dependencies in the generator body:
77+
78+
```python
79+
80+
async def my_generator() -> typing.AsyncGenerator[float, float]:
81+
value = await Container.factory.resolve()
82+
receive = yield value # (1)!
83+
yield receive + value
84+
85+
```
86+
87+
1. This receive will always be `None` if you would wrap this generator with @inject.
88+
89+
90+
91+
## ContextResources
92+
93+
`that-depends` will **not** allow context initialization for [ContextResource](../providers/context-resources.md) providers
94+
as part of dependency injection into a generator.
95+
96+
This is the case for both async and sync injection.
97+
98+
**For example:**
99+
```python
100+
def sync_resource() -> typing.Iterator[float]:
101+
yield random.random()
102+
103+
class Container(BaseContainer):
104+
sync_provider = providers.ContextResource(sync_resource).with_config(scope=ContextScopes.INJECT)
105+
dependent_provider = providers.Factory(lambda x: x, sync_provider.cast)
106+
107+
@inject(scope=ContextScopes.INJECT) # (1)!
108+
def injected(val: float = Provide[Container.dependent_provider]) -> typing.Generator[float, None, None]:
109+
yield val
110+
111+
# This will raise a `ContextProviderError`!
112+
next(_injected())
113+
```
114+
115+
1. Matches context scope of `sync_provider` provider, which is a dependency of the `dependent_provider` provider.
116+
117+
118+
When calling `next(injected())`, `that-depends` will try to initialize a new context for the `sync_provider`,
119+
however, this is not permitted for generators, thus it will raise a `ContextProviderError`.
120+
121+
122+
Keep in mind that if context does not need to be initialized, the generator injection will work as expected:
123+
124+
```python
125+
def sync_resource() -> typing.Iterator[float]:
126+
yield random.random()
127+
128+
class Container(BaseContainer):
129+
sync_provider = providers.ContextResource(sync_resource).with_config(scope=ContextScopes.REQUEST)
130+
dependent_provider = providers.Factory(lambda x: x, sync_provider.cast)
131+
132+
@inject(scope=ContextScopes.INJECT) # (1)!
133+
def injected(val: float = Provide[Container.dependent_provider]) -> typing.Generator[float, None, None]:
134+
yield val
135+
136+
137+
with container_context(scope=ContextScopes.REQUEST):
138+
# This will resolve as expected
139+
next(_injected())
140+
```
141+
142+
Since no context initialization was needed, the generator will work as expected.
143+
144+
1. Scope provided to `@inject` no longer matches scope of the `sync_provider`

mkdocs.yml

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7,11 +7,12 @@ nav:
77
- Introduction:
88
- Containers: introduction/ioc-container.md
99
- Dependency Injection: introduction/injection.md
10-
- Scopes: introduction/scopes.md
10+
- Generator Injection: introduction/generator-injection.md
1111
- String Injection: introduction/string-injection.md
12-
- Multiple Containers: introduction/multiple-containers.md
13-
- Application Settings: introduction/application-settings.md
12+
- Scopes: introduction/scopes.md
1413
- Tear-down: introduction/tear-down.md
14+
- Multiple Containers: introduction/multiple-containers.md
15+
1516
- Providers:
1617
- Collections: providers/collections.md
1718
- Context-Resources: providers/context-resources.md

0 commit comments

Comments
 (0)