-
Notifications
You must be signed in to change notification settings - Fork 14
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
Changes from all commits
Commits
Show all changes
10 commits
Select commit
Hold shift + click to select a range
0440a08
Initial implementation of generator injection for async functions.
alexanderlazarev0 e6e6d80
Added injection for sync generators.
alexanderlazarev0 7dc3bb3
Added tests.
alexanderlazarev0 a3204d6
Added additional type info to typing.Generator.
alexanderlazarev0 169c375
Added additional type info to typing.AsyncGenerator.
alexanderlazarev0 45733e5
Added receive, send & return tests for sync generators.
alexanderlazarev0 cd9c57e
Added remaining tests for generators with context-resources.
alexanderlazarev0 6c71244
Added test for context resources with different scope.
alexanderlazarev0 8fc3595
Added generator injection documentation.
alexanderlazarev0 d7067be
Cleaned up after review.
alexanderlazarev0 File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file was deleted.
Oops, something went wrong.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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` |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.