Skip to content

Commit c7e81c8

Browse files
authored
feat: ✨ Async support
1 parent 94e9147 commit c7e81c8

File tree

22 files changed

+1025
-970
lines changed

22 files changed

+1025
-970
lines changed

.github/actions/environment/action.yml

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,6 @@ runs:
2020
run: |
2121
pip install --upgrade pip
2222
pip install poetry
23-
poetry config installer.modern-installation true
2423
poetry config virtualenvs.create false
2524
poetry check
2625
poetry install --compile

documentation/basic-usage.md

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -89,6 +89,14 @@ from injection import get_instance
8989
service_a = get_instance(ServiceA)
9090
```
9191

92+
_Example with `aget_instance` function:_
93+
94+
```python
95+
from injection import aget_instance
96+
97+
service_a = await aget_instance(ServiceA)
98+
```
99+
92100
_Example with `get_lazy_instance` function:_
93101

94102
```python
@@ -99,6 +107,16 @@ lazy_service_a = get_lazy_instance(ServiceA)
99107
service_a = ~lazy_service_a
100108
```
101109

110+
_Example with `aget_lazy_instance` function:_
111+
112+
```python
113+
from injection import aget_lazy_instance
114+
115+
lazy_service_a = aget_lazy_instance(ServiceA)
116+
# ...
117+
service_a = await lazy_service_a
118+
```
119+
102120
## Inheritance
103121

104122
In the case of inheritance, you can use the decorator parameter `on` to link the injection to one or several other
@@ -159,6 +177,28 @@ def service_d_recipe() -> ServiceD:
159177
""" recipe implementation """
160178
```
161179

180+
### Async recipes
181+
182+
An asynchronous recipe is defined in the same way as a conventional recipe. To retrieve an instance of an asynchronous
183+
recipe, you need to be in an asynchronous context (decorate an asynchronous function with `@inject` or use an
184+
asynchronous getter).
185+
186+
Asynchronous singletons can be retrieved in a synchronous context if they have already been instantiated. The
187+
`all_ready` method ensures that all singletons have been instantiated.
188+
189+
```python
190+
from injection import get_instance, mod, singleton
191+
192+
@singleton
193+
async def service_e_recipe() -> ServiceE:
194+
""" recipe implementation """
195+
196+
async def main():
197+
await mod().all_ready()
198+
# ...
199+
service_e = get_instance(ServiceE)
200+
```
201+
162202
## Working with type aliases
163203

164204
```python

documentation/integrations.md

Lines changed: 0 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -2,35 +2,6 @@
22

33
**Integrations make it easy to connect `python-injection` to other frameworks.**
44

5-
## [BlackSheep](https://github.com/Neoteroi/BlackSheep)
6-
7-
_[See more](https://www.neoteroi.dev/blacksheep/dependency-injection) about BlackSheep and its dependency injection._
8-
9-
Example:
10-
11-
```python
12-
from blacksheep import Application
13-
from injection.integrations.blacksheep import InjectionServices
14-
15-
app = Application(
16-
services=InjectionServices(),
17-
)
18-
```
19-
20-
Example with a custom injection module:
21-
22-
```python
23-
from blacksheep import Application
24-
from injection import mod
25-
from injection.integrations.blacksheep import InjectionServices
26-
27-
custom_module = mod("custom_module")
28-
29-
app = Application(
30-
services=InjectionServices(custom_module),
31-
)
32-
```
33-
345
## [FastAPI](https://github.com/fastapi/fastapi)
356

367
Exemple:

injection/__init__.py

Lines changed: 8 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,16 @@
11
from ._core.descriptors import LazyInstance
2-
from ._core.module import Injectable, Mode, Module, Priority
2+
from ._core.injectables import Injectable
3+
from ._core.module import Mode, Module, Priority, mod
34

45
__all__ = (
56
"Injectable",
67
"LazyInstance",
78
"Mode",
89
"Module",
910
"Priority",
11+
"afind_instance",
12+
"aget_instance",
13+
"aget_lazy_instance",
1014
"constant",
1115
"find_instance",
1216
"get_instance",
@@ -19,14 +23,9 @@
1923
"singleton",
2024
)
2125

22-
23-
def mod(name: str | None = None, /) -> Module:
24-
if name is None:
25-
return Module.default()
26-
27-
return Module.from_name(name)
28-
29-
26+
afind_instance = mod().afind_instance
27+
aget_instance = mod().aget_instance
28+
aget_lazy_instance = mod().aget_lazy_instance
3029
constant = mod().constant
3130
find_instance = mod().find_instance
3231
get_instance = mod().get_instance

injection/__init__.pyi

Lines changed: 48 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
from abc import abstractmethod
2-
from collections.abc import Callable
2+
from collections.abc import Awaitable, Callable
33
from contextlib import ContextDecorator
44
from enum import Enum
55
from logging import Logger
@@ -21,6 +21,9 @@ from ._core.module import ModeStr, PriorityStr
2121

2222
_: Module = ...
2323

24+
afind_instance = _.afind_instance
25+
aget_instance = _.aget_instance
26+
aget_lazy_instance = _.aget_lazy_instance
2427
constant = _.constant
2528
find_instance = _.find_instance
2629
get_instance = _.get_instance
@@ -53,59 +56,59 @@ class Module:
5356
def __contains__(self, cls: _InputType[Any], /) -> bool: ...
5457
@property
5558
def is_locked(self) -> bool: ...
56-
def inject[**P, T](self, wrapped: Callable[P, T] = ..., /): # type: ignore[no-untyped-def]
59+
def inject[**P, T](self, wrapped: Callable[P, T] = ..., /) -> Any:
5760
"""
5861
Decorator applicable to a class or function. Inject function dependencies using
5962
parameter type annotations. If applied to a class, the dependencies resolved
6063
will be those of the `__init__` method.
6164
"""
6265

63-
def injectable[**P, T]( # type: ignore[no-untyped-def]
66+
def injectable[**P, T](
6467
self,
65-
wrapped: Callable[P, T] = ...,
68+
wrapped: Callable[P, T] | Callable[P, Awaitable[T]] = ...,
6669
/,
6770
*,
6871
cls: _InjectableFactory[T] = ...,
6972
inject: bool = ...,
7073
on: _TypeInfo[T] = ...,
7174
mode: Mode | ModeStr = ...,
72-
):
75+
) -> Any:
7376
"""
7477
Decorator applicable to a class or function. It is used to indicate how the
7578
injectable will be constructed. At injection time, a new instance will be
7679
injected each time.
7780
"""
7881

79-
def singleton[**P, T]( # type: ignore[no-untyped-def]
82+
def singleton[**P, T](
8083
self,
81-
wrapped: Callable[P, T] = ...,
84+
wrapped: Callable[P, T] | Callable[P, Awaitable[T]] = ...,
8285
/,
8386
*,
8487
inject: bool = ...,
8588
on: _TypeInfo[T] = ...,
8689
mode: Mode | ModeStr = ...,
87-
):
90+
) -> Any:
8891
"""
8992
Decorator applicable to a class or function. It is used to indicate how the
9093
singleton will be constructed. At injection time, the injected instance will
9194
always be the same.
9295
"""
9396

94-
def should_be_injectable[T](self, wrapped: type[T] = ..., /): # type: ignore[no-untyped-def]
97+
def should_be_injectable[T](self, wrapped: type[T] = ..., /) -> Any:
9598
"""
9699
Decorator applicable to a class. It is used to specify whether an injectable
97100
should be registered. Raise an exception at injection time if the class isn't
98101
registered.
99102
"""
100103

101-
def constant[T]( # type: ignore[no-untyped-def]
104+
def constant[T](
102105
self,
103106
wrapped: type[T] = ...,
104107
/,
105108
*,
106109
on: _TypeInfo[T] = ...,
107110
mode: Mode | ModeStr = ...,
108-
):
111+
) -> Any:
109112
"""
110113
Decorator applicable to a class or function. It is used to indicate how the
111114
constant is constructed. At injection time, the injected instance will always
@@ -131,12 +134,25 @@ class Module:
131134
wrapped: Callable[P, T],
132135
/,
133136
) -> Callable[P, T]: ...
137+
async def afind_instance[T](self, cls: _InputType[T]) -> T: ...
134138
def find_instance[T](self, cls: _InputType[T]) -> T:
135139
"""
136140
Function used to retrieve an instance associated with the type passed in
137141
parameter or an exception will be raised.
138142
"""
139143

144+
@overload
145+
async def aget_instance[T, Default](
146+
self,
147+
cls: _InputType[T],
148+
default: Default,
149+
) -> T | Default: ...
150+
@overload
151+
async def aget_instance[T](
152+
self,
153+
cls: _InputType[T],
154+
default: None = ...,
155+
) -> T | None: ...
140156
@overload
141157
def get_instance[T, Default](
142158
self,
@@ -149,12 +165,28 @@ class Module:
149165
"""
150166

151167
@overload
152-
def get_instance[T, _](
168+
def get_instance[T](
153169
self,
154170
cls: _InputType[T],
155171
default: None = ...,
156172
) -> T | None: ...
157173
@overload
174+
def aget_lazy_instance[T, Default](
175+
self,
176+
cls: _InputType[T],
177+
default: Default,
178+
*,
179+
cache: bool = ...,
180+
) -> Awaitable[T | Default]: ...
181+
@overload
182+
def aget_lazy_instance[T](
183+
self,
184+
cls: _InputType[T],
185+
default: None = ...,
186+
*,
187+
cache: bool = ...,
188+
) -> Awaitable[T | None]: ...
189+
@overload
158190
def get_lazy_instance[T, Default](
159191
self,
160192
cls: _InputType[T],
@@ -172,7 +204,7 @@ class Module:
172204
"""
173205

174206
@overload
175-
def get_lazy_instance[T, _](
207+
def get_lazy_instance[T](
176208
self,
177209
cls: _InputType[T],
178210
default: None = ...,
@@ -229,6 +261,7 @@ class Module:
229261
Function to unlock the module by deleting cached instances of singletons.
230262
"""
231263

264+
async def all_ready(self) -> None: ...
232265
def add_logger(self, logger: Logger) -> Self: ...
233266
@classmethod
234267
def from_name(cls, name: str) -> Module:
@@ -253,6 +286,8 @@ class Injectable[T](Protocol):
253286
def is_locked(self) -> bool: ...
254287
def unlock(self) -> None: ...
255288
@abstractmethod
289+
async def aget_instance(self) -> T: ...
290+
@abstractmethod
256291
def get_instance(self) -> T: ...
257292

258293
@final

injection/_core/__init__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -49,5 +49,5 @@ def standardize_input_classes[T](
4949
@Locator.static_hooks.on_update
5050
def standardize_classes[T](*_: Any, **__: Any) -> HookGenerator[Updater[T]]:
5151
updater = yield
52-
updater.classes = set(standardize_types(*updater.classes))
52+
updater.classes = frozenset(standardize_types(*updater.classes))
5353
return updater
Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
from abc import abstractmethod
2+
from collections.abc import Awaitable, Callable, Generator
3+
from dataclasses import dataclass
4+
from typing import Any, NoReturn, Protocol, override, runtime_checkable
5+
6+
7+
@dataclass(repr=False, eq=False, frozen=True, slots=True)
8+
class SimpleAwaitable[T](Awaitable[T]):
9+
callable: Callable[..., Awaitable[T]]
10+
11+
@override
12+
def __await__(self) -> Generator[Any, Any, T]:
13+
return self.callable().__await__()
14+
15+
16+
@runtime_checkable
17+
class Caller[**P, T](Protocol):
18+
__slots__ = ()
19+
20+
@abstractmethod
21+
async def acall(self, /, *args: P.args, **kwargs: P.kwargs) -> T:
22+
raise NotImplementedError
23+
24+
@abstractmethod
25+
def call(self, /, *args: P.args, **kwargs: P.kwargs) -> T:
26+
raise NotImplementedError
27+
28+
29+
@dataclass(repr=False, eq=False, frozen=True, slots=True)
30+
class AsyncCaller[**P, T](Caller[P, T]):
31+
callable: Callable[P, Awaitable[T]]
32+
33+
@override
34+
async def acall(self, /, *args: P.args, **kwargs: P.kwargs) -> T:
35+
return await self.callable(*args, **kwargs)
36+
37+
@override
38+
def call(self, /, *args: P.args, **kwargs: P.kwargs) -> NoReturn:
39+
raise RuntimeError(
40+
"Synchronous call isn't supported for an asynchronous Callable."
41+
)
42+
43+
44+
@dataclass(repr=False, eq=False, frozen=True, slots=True)
45+
class SyncCaller[**P, T](Caller[P, T]):
46+
callable: Callable[P, T]
47+
48+
@override
49+
async def acall(self, /, *args: P.args, **kwargs: P.kwargs) -> T:
50+
return self.callable(*args, **kwargs)
51+
52+
@override
53+
def call(self, /, *args: P.args, **kwargs: P.kwargs) -> T:
54+
return self.callable(*args, **kwargs)

injection/_core/common/invertible.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,8 +13,8 @@ def __invert__(self) -> T:
1313

1414
@dataclass(repr=False, eq=False, frozen=True, slots=True)
1515
class SimpleInvertible[T](Invertible[T]):
16-
getter: Callable[..., T]
16+
callable: Callable[..., T]
1717

1818
@override
1919
def __invert__(self) -> T:
20-
return self.getter()
20+
return self.callable()

0 commit comments

Comments
 (0)