python-injection 0.12.3__py3-none-any.whl → 0.13.1__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- injection/__init__.py +5 -0
- injection/__init__.pyi +65 -48
- injection/_core/common/asynchronous.py +12 -11
- injection/_core/common/event.py +1 -1
- injection/_core/common/key.py +5 -0
- injection/_core/common/type.py +33 -2
- injection/_core/descriptors.py +8 -2
- injection/_core/injectables.py +103 -22
- injection/_core/module.py +109 -46
- injection/_core/scope.py +209 -0
- injection/exceptions.py +12 -0
- injection/integrations/fastapi.py +6 -42
- injection/testing/__init__.py +2 -0
- injection/testing/__init__.pyi +10 -7
- injection/utils.py +7 -2
- {python_injection-0.12.3.dist-info → python_injection-0.13.1.dist-info}/METADATA +14 -15
- python_injection-0.13.1.dist-info/RECORD +25 -0
- {python_injection-0.12.3.dist-info → python_injection-0.13.1.dist-info}/WHEEL +1 -1
- injection/_core/common/threading.py +0 -11
- python_injection-0.12.3.dist-info/RECORD +0 -24
injection/__init__.py
CHANGED
@@ -1,6 +1,7 @@
|
|
1
1
|
from ._core.descriptors import LazyInstance
|
2
2
|
from ._core.injectables import Injectable
|
3
3
|
from ._core.module import Mode, Module, Priority, mod
|
4
|
+
from ._core.scope import adefine_scope, define_scope
|
4
5
|
|
5
6
|
__all__ = (
|
6
7
|
"Injectable",
|
@@ -8,16 +9,19 @@ __all__ = (
|
|
8
9
|
"Mode",
|
9
10
|
"Module",
|
10
11
|
"Priority",
|
12
|
+
"adefine_scope",
|
11
13
|
"afind_instance",
|
12
14
|
"aget_instance",
|
13
15
|
"aget_lazy_instance",
|
14
16
|
"constant",
|
17
|
+
"define_scope",
|
15
18
|
"find_instance",
|
16
19
|
"get_instance",
|
17
20
|
"get_lazy_instance",
|
18
21
|
"inject",
|
19
22
|
"injectable",
|
20
23
|
"mod",
|
24
|
+
"scoped",
|
21
25
|
"set_constant",
|
22
26
|
"should_be_injectable",
|
23
27
|
"singleton",
|
@@ -32,6 +36,7 @@ get_instance = mod().get_instance
|
|
32
36
|
get_lazy_instance = mod().get_lazy_instance
|
33
37
|
inject = mod().inject
|
34
38
|
injectable = mod().injectable
|
39
|
+
scoped = mod().scoped
|
35
40
|
set_constant = mod().set_constant
|
36
41
|
should_be_injectable = mod().should_be_injectable
|
37
42
|
singleton = mod().singleton
|
injection/__init__.pyi
CHANGED
@@ -1,17 +1,9 @@
|
|
1
1
|
from abc import abstractmethod
|
2
|
-
from collections.abc import Awaitable, Callable
|
3
|
-
from contextlib import
|
2
|
+
from collections.abc import AsyncIterator, Awaitable, Callable, Iterator
|
3
|
+
from contextlib import asynccontextmanager, contextmanager
|
4
4
|
from enum import Enum
|
5
5
|
from logging import Logger
|
6
|
-
from typing import
|
7
|
-
Any,
|
8
|
-
ContextManager,
|
9
|
-
Protocol,
|
10
|
-
Self,
|
11
|
-
final,
|
12
|
-
overload,
|
13
|
-
runtime_checkable,
|
14
|
-
)
|
6
|
+
from typing import Any, Final, Protocol, Self, final, overload, runtime_checkable
|
15
7
|
|
16
8
|
from ._core.common.invertible import Invertible as _Invertible
|
17
9
|
from ._core.common.type import InputType as _InputType
|
@@ -19,27 +11,53 @@ from ._core.common.type import TypeInfo as _TypeInfo
|
|
19
11
|
from ._core.module import InjectableFactory as _InjectableFactory
|
20
12
|
from ._core.module import ModeStr, PriorityStr
|
21
13
|
|
22
|
-
|
14
|
+
__MODULE: Final[Module] = ...
|
23
15
|
|
24
|
-
afind_instance =
|
25
|
-
aget_instance =
|
26
|
-
aget_lazy_instance =
|
27
|
-
constant =
|
28
|
-
find_instance =
|
29
|
-
get_instance =
|
30
|
-
get_lazy_instance =
|
31
|
-
inject =
|
32
|
-
injectable =
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
del _
|
16
|
+
afind_instance = __MODULE.afind_instance
|
17
|
+
aget_instance = __MODULE.aget_instance
|
18
|
+
aget_lazy_instance = __MODULE.aget_lazy_instance
|
19
|
+
constant = __MODULE.constant
|
20
|
+
find_instance = __MODULE.find_instance
|
21
|
+
get_instance = __MODULE.get_instance
|
22
|
+
get_lazy_instance = __MODULE.get_lazy_instance
|
23
|
+
inject = __MODULE.inject
|
24
|
+
injectable = __MODULE.injectable
|
25
|
+
scoped = __MODULE.scoped
|
26
|
+
set_constant = __MODULE.set_constant
|
27
|
+
should_be_injectable = __MODULE.should_be_injectable
|
28
|
+
singleton = __MODULE.singleton
|
38
29
|
|
30
|
+
@asynccontextmanager
|
31
|
+
def adefine_scope(name: str, *, shared: bool = ...) -> AsyncIterator[None]: ...
|
32
|
+
@contextmanager
|
33
|
+
def define_scope(name: str, *, shared: bool = ...) -> Iterator[None]: ...
|
39
34
|
def mod(name: str = ..., /) -> Module:
|
40
35
|
"""
|
41
36
|
Short syntax for `Module.from_name`.
|
42
37
|
"""
|
38
|
+
@runtime_checkable
|
39
|
+
class Injectable[T](Protocol):
|
40
|
+
@property
|
41
|
+
def is_locked(self) -> bool: ...
|
42
|
+
def unlock(self) -> None: ...
|
43
|
+
@abstractmethod
|
44
|
+
async def aget_instance(self) -> T: ...
|
45
|
+
@abstractmethod
|
46
|
+
def get_instance(self) -> T: ...
|
47
|
+
|
48
|
+
class LazyInstance[T]:
|
49
|
+
def __init__(
|
50
|
+
self,
|
51
|
+
cls: _InputType[T],
|
52
|
+
/,
|
53
|
+
default: T = ...,
|
54
|
+
module: Module = ...,
|
55
|
+
) -> None: ...
|
56
|
+
@overload
|
57
|
+
def __get__(self, instance: object, owner: type | None = ...) -> T: ...
|
58
|
+
@overload
|
59
|
+
def __get__(self, instance: None = ..., owner: type | None = ...) -> Self: ...
|
60
|
+
|
43
61
|
@final
|
44
62
|
class Module:
|
45
63
|
"""
|
@@ -94,6 +112,21 @@ class Module:
|
|
94
112
|
always be the same.
|
95
113
|
"""
|
96
114
|
|
115
|
+
def scoped[**P, T](
|
116
|
+
self,
|
117
|
+
scope_name: str,
|
118
|
+
/,
|
119
|
+
*,
|
120
|
+
inject: bool = ...,
|
121
|
+
on: _TypeInfo[T] = (),
|
122
|
+
mode: Mode | ModeStr = ...,
|
123
|
+
) -> Any:
|
124
|
+
"""
|
125
|
+
Decorator applicable to a class or function or generator function. It is used
|
126
|
+
to indicate how the scoped instance will be constructed. At injection time, the
|
127
|
+
injected instance is retrieved from the scope.
|
128
|
+
"""
|
129
|
+
|
97
130
|
def should_be_injectable[T](self, wrapped: type[T] = ..., /) -> Any:
|
98
131
|
"""
|
99
132
|
Decorator applicable to a class. It is used to specify whether an injectable
|
@@ -233,12 +266,13 @@ class Module:
|
|
233
266
|
Function to remove a module in use.
|
234
267
|
"""
|
235
268
|
|
269
|
+
@contextmanager
|
236
270
|
def use_temporarily(
|
237
271
|
self,
|
238
272
|
module: Module,
|
239
273
|
*,
|
240
274
|
priority: Priority | PriorityStr = ...,
|
241
|
-
) ->
|
275
|
+
) -> Iterator[None]:
|
242
276
|
"""
|
243
277
|
Context manager or decorator for temporary use of a module.
|
244
278
|
"""
|
@@ -275,30 +309,13 @@ class Module:
|
|
275
309
|
Class method for getting the default module.
|
276
310
|
"""
|
277
311
|
|
278
|
-
@final
|
279
|
-
class Priority(Enum):
|
280
|
-
LOW = ...
|
281
|
-
HIGH = ...
|
282
|
-
|
283
|
-
@runtime_checkable
|
284
|
-
class Injectable[T](Protocol):
|
285
|
-
@property
|
286
|
-
def is_locked(self) -> bool: ...
|
287
|
-
def unlock(self) -> None: ...
|
288
|
-
@abstractmethod
|
289
|
-
async def aget_instance(self) -> T: ...
|
290
|
-
@abstractmethod
|
291
|
-
def get_instance(self) -> T: ...
|
292
|
-
|
293
312
|
@final
|
294
313
|
class Mode(Enum):
|
295
314
|
FALLBACK = ...
|
296
315
|
NORMAL = ...
|
297
316
|
OVERRIDE = ...
|
298
317
|
|
299
|
-
|
300
|
-
|
301
|
-
|
302
|
-
|
303
|
-
@overload
|
304
|
-
def __get__(self, instance: None = ..., owner: type | None = ...) -> Self: ...
|
318
|
+
@final
|
319
|
+
class Priority(Enum):
|
320
|
+
LOW = ...
|
321
|
+
HIGH = ...
|
@@ -1,10 +1,19 @@
|
|
1
1
|
import asyncio
|
2
2
|
from abc import abstractmethod
|
3
|
-
from collections.abc import Awaitable, Callable, Generator
|
3
|
+
from collections.abc import Awaitable, Callable, Coroutine, Generator
|
4
4
|
from dataclasses import dataclass
|
5
5
|
from typing import Any, Protocol, runtime_checkable
|
6
6
|
|
7
7
|
|
8
|
+
def run_sync[T](coroutine: Coroutine[Any, Any, T]) -> T:
|
9
|
+
loop = asyncio.get_event_loop()
|
10
|
+
|
11
|
+
try:
|
12
|
+
return loop.run_until_complete(coroutine)
|
13
|
+
finally:
|
14
|
+
coroutine.close()
|
15
|
+
|
16
|
+
|
8
17
|
@dataclass(repr=False, eq=False, frozen=True, slots=True)
|
9
18
|
class SimpleAwaitable[T](Awaitable[T]):
|
10
19
|
callable: Callable[..., Awaitable[T]]
|
@@ -28,21 +37,13 @@ class Caller[**P, T](Protocol):
|
|
28
37
|
|
29
38
|
@dataclass(repr=False, eq=False, frozen=True, slots=True)
|
30
39
|
class AsyncCaller[**P, T](Caller[P, T]):
|
31
|
-
callable: Callable[P,
|
40
|
+
callable: Callable[P, Coroutine[Any, Any, T]]
|
32
41
|
|
33
42
|
async def acall(self, /, *args: P.args, **kwargs: P.kwargs) -> T:
|
34
43
|
return await self.callable(*args, **kwargs)
|
35
44
|
|
36
45
|
def call(self, /, *args: P.args, **kwargs: P.kwargs) -> T:
|
37
|
-
|
38
|
-
|
39
|
-
if loop.is_running():
|
40
|
-
raise RuntimeError(
|
41
|
-
"Can't call an asynchronous function in a synchronous context."
|
42
|
-
)
|
43
|
-
|
44
|
-
coroutine = self.callable(*args, **kwargs)
|
45
|
-
return loop.run_until_complete(coroutine)
|
46
|
+
return run_sync(self.callable(*args, **kwargs))
|
46
47
|
|
47
48
|
|
48
49
|
@dataclass(repr=False, eq=False, frozen=True, slots=True)
|
injection/_core/common/event.py
CHANGED
@@ -25,7 +25,7 @@ class EventChannel:
|
|
25
25
|
@contextmanager
|
26
26
|
def dispatch(self, event: Event) -> Iterator[None]:
|
27
27
|
with ExitStack() as stack:
|
28
|
-
for listener in
|
28
|
+
for listener in self.__listeners:
|
29
29
|
context_manager = listener.on_event(event)
|
30
30
|
|
31
31
|
if context_manager is None:
|
injection/_core/common/type.py
CHANGED
@@ -1,4 +1,12 @@
|
|
1
|
-
from collections.abc import
|
1
|
+
from collections.abc import (
|
2
|
+
AsyncGenerator,
|
3
|
+
AsyncIterable,
|
4
|
+
AsyncIterator,
|
5
|
+
Callable,
|
6
|
+
Generator,
|
7
|
+
Iterable,
|
8
|
+
Iterator,
|
9
|
+
)
|
2
10
|
from inspect import isfunction
|
3
11
|
from types import GenericAlias, UnionType
|
4
12
|
from typing import (
|
@@ -23,7 +31,7 @@ def get_return_types(*args: TypeInfo[Any]) -> Iterator[InputType[Any]]:
|
|
23
31
|
):
|
24
32
|
inner_args = arg
|
25
33
|
|
26
|
-
elif isfunction(arg) and (return_type :=
|
34
|
+
elif isfunction(arg) and (return_type := get_return_hint(arg)):
|
27
35
|
inner_args = (return_type,)
|
28
36
|
|
29
37
|
else:
|
@@ -33,6 +41,29 @@ def get_return_types(*args: TypeInfo[Any]) -> Iterator[InputType[Any]]:
|
|
33
41
|
yield from get_return_types(*inner_args)
|
34
42
|
|
35
43
|
|
44
|
+
def get_return_hint[T](function: Callable[..., T]) -> InputType[T] | None:
|
45
|
+
return get_type_hints(function).get("return")
|
46
|
+
|
47
|
+
|
48
|
+
def get_yield_hint[T](
|
49
|
+
function: Callable[..., Iterator[T]] | Callable[..., AsyncIterator[T]],
|
50
|
+
) -> InputType[T] | None:
|
51
|
+
return_type = get_return_hint(function)
|
52
|
+
|
53
|
+
if get_origin(return_type) not in {
|
54
|
+
AsyncGenerator,
|
55
|
+
AsyncIterable,
|
56
|
+
AsyncIterator,
|
57
|
+
Generator,
|
58
|
+
Iterable,
|
59
|
+
Iterator,
|
60
|
+
}:
|
61
|
+
return None
|
62
|
+
|
63
|
+
args = get_args(return_type)
|
64
|
+
return next(iter(args), None)
|
65
|
+
|
66
|
+
|
36
67
|
def standardize_types(
|
37
68
|
*types: InputType[Any],
|
38
69
|
with_origin: bool = False,
|
injection/_core/descriptors.py
CHANGED
@@ -10,9 +10,15 @@ class LazyInstance[T]:
|
|
10
10
|
|
11
11
|
__value: Invertible[T]
|
12
12
|
|
13
|
-
def __init__(
|
13
|
+
def __init__(
|
14
|
+
self,
|
15
|
+
cls: InputType[T],
|
16
|
+
/,
|
17
|
+
default: T = NotImplemented,
|
18
|
+
module: Module | None = None,
|
19
|
+
) -> None:
|
14
20
|
module = module or mod()
|
15
|
-
self.__value = module.get_lazy_instance(cls, default
|
21
|
+
self.__value = module.get_lazy_instance(cls, default)
|
16
22
|
|
17
23
|
def __get__(
|
18
24
|
self,
|
injection/_core/injectables.py
CHANGED
@@ -2,10 +2,18 @@ from abc import ABC, abstractmethod
|
|
2
2
|
from collections.abc import MutableMapping
|
3
3
|
from contextlib import suppress
|
4
4
|
from dataclasses import dataclass
|
5
|
-
from typing import
|
6
|
-
|
7
|
-
|
8
|
-
|
5
|
+
from typing import (
|
6
|
+
Any,
|
7
|
+
AsyncContextManager,
|
8
|
+
ClassVar,
|
9
|
+
ContextManager,
|
10
|
+
NoReturn,
|
11
|
+
Protocol,
|
12
|
+
runtime_checkable,
|
13
|
+
)
|
14
|
+
|
15
|
+
from injection._core.common.asynchronous import Caller, run_sync
|
16
|
+
from injection._core.scope import Scope, get_active_scopes, get_scope
|
9
17
|
from injection.exceptions import InjectionError
|
10
18
|
|
11
19
|
|
@@ -50,41 +58,114 @@ class SingletonInjectable[T](BaseInjectable[T]):
|
|
50
58
|
__key: ClassVar[str] = "$instance"
|
51
59
|
|
52
60
|
@property
|
53
|
-
def
|
61
|
+
def is_locked(self) -> bool:
|
62
|
+
return self.__key in self.__cache
|
63
|
+
|
64
|
+
@property
|
65
|
+
def __cache(self) -> MutableMapping[str, Any]:
|
54
66
|
return self.__dict__
|
55
67
|
|
68
|
+
async def aget_instance(self) -> T:
|
69
|
+
cache = self.__cache
|
70
|
+
|
71
|
+
with suppress(KeyError):
|
72
|
+
return cache[self.__key]
|
73
|
+
|
74
|
+
instance = await self.factory.acall()
|
75
|
+
cache[self.__key] = instance
|
76
|
+
return instance
|
77
|
+
|
78
|
+
def get_instance(self) -> T:
|
79
|
+
cache = self.__cache
|
80
|
+
|
81
|
+
with suppress(KeyError):
|
82
|
+
return cache[self.__key]
|
83
|
+
|
84
|
+
instance = self.factory.call()
|
85
|
+
cache[self.__key] = instance
|
86
|
+
return instance
|
87
|
+
|
88
|
+
def unlock(self) -> None:
|
89
|
+
self.__cache.pop(self.__key, None)
|
90
|
+
|
91
|
+
|
92
|
+
@dataclass(repr=False, eq=False, frozen=True, slots=True)
|
93
|
+
class ScopedInjectable[R, T](Injectable[T], ABC):
|
94
|
+
factory: Caller[..., R]
|
95
|
+
scope_name: str
|
96
|
+
|
56
97
|
@property
|
57
98
|
def is_locked(self) -> bool:
|
58
|
-
return self.
|
99
|
+
return any(self in scope.cache for scope in get_active_scopes(self.scope_name))
|
59
100
|
|
60
|
-
|
61
|
-
|
101
|
+
@abstractmethod
|
102
|
+
async def abuild(self, scope: Scope) -> T:
|
103
|
+
raise NotImplementedError
|
104
|
+
|
105
|
+
@abstractmethod
|
106
|
+
def build(self, scope: Scope) -> T:
|
107
|
+
raise NotImplementedError
|
62
108
|
|
63
109
|
async def aget_instance(self) -> T:
|
64
|
-
|
65
|
-
return self.__check_instance()
|
110
|
+
scope = get_scope(self.scope_name)
|
66
111
|
|
67
|
-
with
|
68
|
-
|
69
|
-
self.__set_instance(instance)
|
112
|
+
with suppress(KeyError):
|
113
|
+
return scope.cache[self]
|
70
114
|
|
115
|
+
instance = await self.abuild(scope)
|
116
|
+
scope.cache[self] = instance
|
71
117
|
return instance
|
72
118
|
|
73
119
|
def get_instance(self) -> T:
|
74
|
-
|
75
|
-
return self.__check_instance()
|
120
|
+
scope = get_scope(self.scope_name)
|
76
121
|
|
77
|
-
with
|
78
|
-
|
79
|
-
self.__set_instance(instance)
|
122
|
+
with suppress(KeyError):
|
123
|
+
return scope.cache[self]
|
80
124
|
|
125
|
+
instance = self.build(scope)
|
126
|
+
scope.cache[self] = instance
|
81
127
|
return instance
|
82
128
|
|
83
|
-
def
|
84
|
-
|
129
|
+
def unlock(self) -> None:
|
130
|
+
if self.is_locked:
|
131
|
+
raise RuntimeError(f"To unlock, close the `{self.scope_name}` scope.")
|
132
|
+
|
133
|
+
|
134
|
+
class AsyncCMScopedInjectable[T](ScopedInjectable[AsyncContextManager[T], T]):
|
135
|
+
__slots__ = ()
|
136
|
+
|
137
|
+
async def abuild(self, scope: Scope) -> T:
|
138
|
+
cm = await self.factory.acall()
|
139
|
+
return await scope.aenter(cm)
|
140
|
+
|
141
|
+
def build(self, scope: Scope) -> T:
|
142
|
+
return run_sync(self.abuild(scope))
|
143
|
+
|
85
144
|
|
86
|
-
|
87
|
-
|
145
|
+
class CMScopedInjectable[T](ScopedInjectable[ContextManager[T], T]):
|
146
|
+
__slots__ = ()
|
147
|
+
|
148
|
+
async def abuild(self, scope: Scope) -> T:
|
149
|
+
cm = await self.factory.acall()
|
150
|
+
return scope.enter(cm)
|
151
|
+
|
152
|
+
def build(self, scope: Scope) -> T:
|
153
|
+
cm = self.factory.call()
|
154
|
+
return scope.enter(cm)
|
155
|
+
|
156
|
+
|
157
|
+
class SimpleScopedInjectable[T](ScopedInjectable[T, T]):
|
158
|
+
__slots__ = ()
|
159
|
+
|
160
|
+
async def abuild(self, scope: Scope) -> T:
|
161
|
+
return await self.factory.acall()
|
162
|
+
|
163
|
+
def build(self, scope: Scope) -> T:
|
164
|
+
return self.factory.call()
|
165
|
+
|
166
|
+
def unlock(self) -> None:
|
167
|
+
for scope in get_active_scopes(self.scope_name):
|
168
|
+
scope.cache.pop(self, None)
|
88
169
|
|
89
170
|
|
90
171
|
@dataclass(repr=False, frozen=True, slots=True)
|
injection/_core/module.py
CHANGED
@@ -12,17 +12,25 @@ from collections.abc import (
|
|
12
12
|
Iterator,
|
13
13
|
Mapping,
|
14
14
|
)
|
15
|
-
from contextlib import contextmanager, suppress
|
15
|
+
from contextlib import asynccontextmanager, contextmanager, suppress
|
16
16
|
from dataclasses import dataclass, field
|
17
17
|
from enum import StrEnum
|
18
18
|
from functools import partialmethod, singledispatchmethod, update_wrapper
|
19
|
-
from inspect import
|
19
|
+
from inspect import (
|
20
|
+
Signature,
|
21
|
+
isasyncgenfunction,
|
22
|
+
isclass,
|
23
|
+
iscoroutinefunction,
|
24
|
+
isgeneratorfunction,
|
25
|
+
markcoroutinefunction,
|
26
|
+
)
|
20
27
|
from inspect import signature as inspect_signature
|
21
28
|
from logging import Logger, getLogger
|
22
29
|
from queue import Empty, Queue
|
23
30
|
from types import MethodType
|
24
31
|
from typing import (
|
25
32
|
Any,
|
33
|
+
AsyncContextManager,
|
26
34
|
ClassVar,
|
27
35
|
ContextManager,
|
28
36
|
Literal,
|
@@ -32,7 +40,6 @@ from typing import (
|
|
32
40
|
overload,
|
33
41
|
runtime_checkable,
|
34
42
|
)
|
35
|
-
from uuid import uuid4
|
36
43
|
|
37
44
|
from injection._core.common.asynchronous import (
|
38
45
|
AsyncCaller,
|
@@ -42,14 +49,22 @@ from injection._core.common.asynchronous import (
|
|
42
49
|
)
|
43
50
|
from injection._core.common.event import Event, EventChannel, EventListener
|
44
51
|
from injection._core.common.invertible import Invertible, SimpleInvertible
|
52
|
+
from injection._core.common.key import new_short_key
|
45
53
|
from injection._core.common.lazy import Lazy, LazyMapping
|
46
|
-
from injection._core.common.
|
47
|
-
|
54
|
+
from injection._core.common.type import (
|
55
|
+
InputType,
|
56
|
+
TypeInfo,
|
57
|
+
get_return_types,
|
58
|
+
get_yield_hint,
|
59
|
+
)
|
48
60
|
from injection._core.hook import Hook, apply_hooks
|
49
61
|
from injection._core.injectables import (
|
62
|
+
AsyncCMScopedInjectable,
|
63
|
+
CMScopedInjectable,
|
50
64
|
Injectable,
|
51
65
|
ShouldBeInjectable,
|
52
66
|
SimpleInjectable,
|
67
|
+
SimpleScopedInjectable,
|
53
68
|
SingletonInjectable,
|
54
69
|
)
|
55
70
|
from injection.exceptions import (
|
@@ -198,14 +213,12 @@ class Record[T](NamedTuple):
|
|
198
213
|
|
199
214
|
@dataclass(repr=False, eq=False, kw_only=True, slots=True)
|
200
215
|
class Updater[T]:
|
201
|
-
factory: Caller[..., T]
|
202
216
|
classes: Iterable[InputType[T]]
|
203
|
-
|
217
|
+
injectable: Injectable[T]
|
204
218
|
mode: Mode
|
205
219
|
|
206
220
|
def make_record(self) -> Record[T]:
|
207
|
-
|
208
|
-
return Record(injectable, self.mode)
|
221
|
+
return Record(self.injectable, self.mode)
|
209
222
|
|
210
223
|
|
211
224
|
@dataclass(repr=False, eq=False, frozen=True, slots=True)
|
@@ -262,7 +275,6 @@ class Locator(Broker):
|
|
262
275
|
def __injectables(self) -> frozenset[Injectable[Any]]:
|
263
276
|
return frozenset(record.injectable for record in self.__records.values())
|
264
277
|
|
265
|
-
@synchronized()
|
266
278
|
def update[T](self, updater: Updater[T]) -> Self:
|
267
279
|
updater = self.__update_preprocessing(updater)
|
268
280
|
record = updater.make_record()
|
@@ -276,7 +288,6 @@ class Locator(Broker):
|
|
276
288
|
|
277
289
|
return self
|
278
290
|
|
279
|
-
@synchronized()
|
280
291
|
def unlock(self) -> Self:
|
281
292
|
for injectable in self.__injectables:
|
282
293
|
injectable.unlock()
|
@@ -350,7 +361,7 @@ type PriorityStr = Literal["low", "high"]
|
|
350
361
|
|
351
362
|
@dataclass(eq=False, frozen=True, slots=True)
|
352
363
|
class Module(Broker, EventListener):
|
353
|
-
name: str = field(default_factory=lambda: f"anonymous@{
|
364
|
+
name: str = field(default_factory=lambda: f"anonymous@{new_short_key()}")
|
354
365
|
__channel: EventChannel = field(
|
355
366
|
default_factory=EventChannel,
|
356
367
|
init=False,
|
@@ -393,7 +404,7 @@ class Module(Broker, EventListener):
|
|
393
404
|
|
394
405
|
@property
|
395
406
|
def __brokers(self) -> Iterator[Broker]:
|
396
|
-
yield from
|
407
|
+
yield from self.__modules
|
397
408
|
yield self.__locator
|
398
409
|
|
399
410
|
def injectable[**P, T](
|
@@ -402,6 +413,7 @@ class Module(Broker, EventListener):
|
|
402
413
|
/,
|
403
414
|
*,
|
404
415
|
cls: InjectableFactory[T] = SimpleInjectable,
|
416
|
+
ignore_type_hint: bool = False,
|
405
417
|
inject: bool = True,
|
406
418
|
on: TypeInfo[T] = (),
|
407
419
|
mode: Mode | ModeStr = Mode.get_default(),
|
@@ -409,12 +421,11 @@ class Module(Broker, EventListener):
|
|
409
421
|
def decorator(
|
410
422
|
wp: Callable[P, T] | Callable[P, Awaitable[T]],
|
411
423
|
) -> Callable[P, T] | Callable[P, Awaitable[T]]:
|
412
|
-
factory =
|
413
|
-
|
424
|
+
factory = extract_caller(self.make_injected_function(wp) if inject else wp)
|
425
|
+
hints = on if ignore_type_hint else (wp, on)
|
414
426
|
updater = Updater(
|
415
|
-
|
416
|
-
|
417
|
-
injectable_factory=cls,
|
427
|
+
classes=get_return_types(hints),
|
428
|
+
injectable=cls(factory), # type: ignore[arg-type]
|
418
429
|
mode=Mode(mode),
|
419
430
|
)
|
420
431
|
self.update(updater)
|
@@ -424,12 +435,66 @@ class Module(Broker, EventListener):
|
|
424
435
|
|
425
436
|
singleton = partialmethod(injectable, cls=SingletonInjectable)
|
426
437
|
|
438
|
+
def scoped[**P, T](
|
439
|
+
self,
|
440
|
+
scope_name: str,
|
441
|
+
/,
|
442
|
+
*,
|
443
|
+
inject: bool = True,
|
444
|
+
on: TypeInfo[T] = (),
|
445
|
+
mode: Mode | ModeStr = Mode.get_default(),
|
446
|
+
) -> Any:
|
447
|
+
def decorator(
|
448
|
+
wrapped: Callable[P, T]
|
449
|
+
| Callable[P, Awaitable[T]]
|
450
|
+
| Callable[P, Iterator[T]]
|
451
|
+
| Callable[P, AsyncIterator[T]],
|
452
|
+
) -> (
|
453
|
+
Callable[P, T]
|
454
|
+
| Callable[P, Awaitable[T]]
|
455
|
+
| Callable[P, Iterator[T]]
|
456
|
+
| Callable[P, AsyncIterator[T]]
|
457
|
+
):
|
458
|
+
injectable_class: Callable[[Caller[P, Any], str], Injectable[T]]
|
459
|
+
wrapper: (
|
460
|
+
Callable[P, T]
|
461
|
+
| Callable[P, Awaitable[T]]
|
462
|
+
| Callable[P, ContextManager[T]]
|
463
|
+
| Callable[P, AsyncContextManager[T]]
|
464
|
+
)
|
465
|
+
|
466
|
+
if isasyncgenfunction(wrapped):
|
467
|
+
hint = get_yield_hint(wrapped)
|
468
|
+
injectable_class = AsyncCMScopedInjectable
|
469
|
+
wrapper = asynccontextmanager(wrapped)
|
470
|
+
|
471
|
+
elif isgeneratorfunction(wrapped):
|
472
|
+
hint = get_yield_hint(wrapped)
|
473
|
+
injectable_class = CMScopedInjectable
|
474
|
+
wrapper = contextmanager(wrapped)
|
475
|
+
|
476
|
+
else:
|
477
|
+
injectable_class = SimpleScopedInjectable
|
478
|
+
hint = wrapper = wrapped # type: ignore[assignment]
|
479
|
+
|
480
|
+
hints = on if hint is None else (hint, on)
|
481
|
+
self.injectable(
|
482
|
+
wrapper,
|
483
|
+
cls=lambda factory: injectable_class(factory, scope_name),
|
484
|
+
ignore_type_hint=True,
|
485
|
+
inject=inject,
|
486
|
+
on=hints,
|
487
|
+
mode=mode,
|
488
|
+
)
|
489
|
+
return wrapped
|
490
|
+
|
491
|
+
return decorator
|
492
|
+
|
427
493
|
def should_be_injectable[T](self, wrapped: type[T] | None = None, /) -> Any:
|
428
494
|
def decorator(wp: type[T]) -> type[T]:
|
429
495
|
updater = Updater(
|
430
|
-
factory=SyncCaller(wp),
|
431
496
|
classes=(wp,),
|
432
|
-
|
497
|
+
injectable=ShouldBeInjectable(wp),
|
433
498
|
mode=Mode.FALLBACK,
|
434
499
|
)
|
435
500
|
self.update(updater)
|
@@ -449,6 +514,7 @@ class Module(Broker, EventListener):
|
|
449
514
|
lazy_instance = Lazy(wp)
|
450
515
|
self.injectable(
|
451
516
|
lambda: ~lazy_instance,
|
517
|
+
ignore_type_hint=True,
|
452
518
|
inject=False,
|
453
519
|
on=(wp, on),
|
454
520
|
mode=mode,
|
@@ -465,14 +531,12 @@ class Module(Broker, EventListener):
|
|
465
531
|
alias: bool = False,
|
466
532
|
mode: Mode | ModeStr = Mode.get_default(),
|
467
533
|
) -> Self:
|
468
|
-
if
|
469
|
-
cls = type(instance)
|
470
|
-
on = (cls, on)
|
471
|
-
|
534
|
+
hints = on if alias else (type(instance), on)
|
472
535
|
self.injectable(
|
473
536
|
lambda: instance,
|
537
|
+
ignore_type_hint=True,
|
474
538
|
inject=False,
|
475
|
-
on=
|
539
|
+
on=hints,
|
476
540
|
mode=mode,
|
477
541
|
)
|
478
542
|
return self
|
@@ -670,8 +734,11 @@ class Module(Broker, EventListener):
|
|
670
734
|
priority: Priority | PriorityStr = Priority.get_default(),
|
671
735
|
) -> Iterator[None]:
|
672
736
|
self.use(module, priority=priority)
|
673
|
-
|
674
|
-
|
737
|
+
|
738
|
+
try:
|
739
|
+
yield
|
740
|
+
finally:
|
741
|
+
self.stop_using(module)
|
675
742
|
|
676
743
|
def change_priority(self, module: Module, priority: Priority | PriorityStr) -> Self:
|
677
744
|
priority = Priority(priority)
|
@@ -682,7 +749,6 @@ class Module(Broker, EventListener):
|
|
682
749
|
|
683
750
|
return self
|
684
751
|
|
685
|
-
@synchronized()
|
686
752
|
def unlock(self) -> Self:
|
687
753
|
for broker in self.__brokers:
|
688
754
|
broker.unlock()
|
@@ -714,12 +780,14 @@ class Module(Broker, EventListener):
|
|
714
780
|
self.__check_locking()
|
715
781
|
|
716
782
|
with self.__channel.dispatch(event):
|
717
|
-
|
718
|
-
|
719
|
-
|
783
|
+
try:
|
784
|
+
yield
|
785
|
+
finally:
|
786
|
+
message = str(event)
|
787
|
+
self.__debug(message)
|
720
788
|
|
721
789
|
def __debug(self, message: object) -> None:
|
722
|
-
for logger in
|
790
|
+
for logger in self.__loggers:
|
723
791
|
logger.debug(message)
|
724
792
|
|
725
793
|
def __check_locking(self) -> None:
|
@@ -741,10 +809,8 @@ class Module(Broker, EventListener):
|
|
741
809
|
with suppress(KeyError):
|
742
810
|
return cls.__instances[name]
|
743
811
|
|
744
|
-
|
745
|
-
|
746
|
-
cls.__instances[name] = instance
|
747
|
-
|
812
|
+
instance = cls(name)
|
813
|
+
cls.__instances[name] = instance
|
748
814
|
return instance
|
749
815
|
|
750
816
|
@classmethod
|
@@ -873,10 +939,8 @@ class InjectMetadata[**P, T](Caller[P, T], EventListener):
|
|
873
939
|
with suppress(AttributeError):
|
874
940
|
return self.__signature
|
875
941
|
|
876
|
-
|
877
|
-
|
878
|
-
self.__signature = signature
|
879
|
-
|
942
|
+
signature = inspect_signature(self.wrapped, eval_str=True)
|
943
|
+
self.__signature = signature
|
880
944
|
return signature
|
881
945
|
|
882
946
|
@property
|
@@ -921,7 +985,6 @@ class InjectMetadata[**P, T](Caller[P, T], EventListener):
|
|
921
985
|
self.__owner = owner
|
922
986
|
return self
|
923
987
|
|
924
|
-
@synchronized()
|
925
988
|
def update(self, module: Module) -> Self:
|
926
989
|
self.__dependencies = Dependencies.resolve(self.signature, module, self.__owner)
|
927
990
|
return self
|
@@ -968,9 +1031,7 @@ class InjectMetadata[**P, T](Caller[P, T], EventListener):
|
|
968
1031
|
self.__setup_queue = None
|
969
1032
|
|
970
1033
|
def __setup(self) -> None:
|
971
|
-
queue
|
972
|
-
|
973
|
-
if queue is None:
|
1034
|
+
if (queue := self.__setup_queue) is None:
|
974
1035
|
return
|
975
1036
|
|
976
1037
|
while True:
|
@@ -1037,11 +1098,13 @@ class SyncInjectedFunction[**P, T](InjectedFunction[P, T]):
|
|
1037
1098
|
return self.__inject_metadata__.call(*args, **kwargs)
|
1038
1099
|
|
1039
1100
|
|
1040
|
-
def
|
1101
|
+
def extract_caller[**P, T](
|
1102
|
+
function: Callable[P, T] | Callable[P, Awaitable[T]],
|
1103
|
+
) -> Caller[P, T]:
|
1041
1104
|
if iscoroutinefunction(function):
|
1042
1105
|
return AsyncCaller(function)
|
1043
1106
|
|
1044
1107
|
elif isinstance(function, InjectedFunction):
|
1045
1108
|
return function.__inject_metadata__
|
1046
1109
|
|
1047
|
-
return SyncCaller(function)
|
1110
|
+
return SyncCaller(function) # type: ignore[arg-type]
|
injection/_core/scope.py
ADDED
@@ -0,0 +1,209 @@
|
|
1
|
+
from __future__ import annotations
|
2
|
+
|
3
|
+
from abc import ABC, abstractmethod
|
4
|
+
from collections import defaultdict
|
5
|
+
from collections.abc import AsyncIterator, Iterator, MutableMapping
|
6
|
+
from contextlib import AsyncExitStack, ExitStack, asynccontextmanager, contextmanager
|
7
|
+
from contextvars import ContextVar
|
8
|
+
from dataclasses import dataclass, field
|
9
|
+
from types import TracebackType
|
10
|
+
from typing import (
|
11
|
+
Any,
|
12
|
+
AsyncContextManager,
|
13
|
+
ContextManager,
|
14
|
+
Final,
|
15
|
+
NoReturn,
|
16
|
+
Protocol,
|
17
|
+
Self,
|
18
|
+
runtime_checkable,
|
19
|
+
)
|
20
|
+
|
21
|
+
from injection._core.common.key import new_short_key
|
22
|
+
from injection.exceptions import (
|
23
|
+
ScopeAlreadyDefinedError,
|
24
|
+
ScopeError,
|
25
|
+
ScopeUndefinedError,
|
26
|
+
)
|
27
|
+
|
28
|
+
|
29
|
+
@dataclass(repr=False, slots=True)
|
30
|
+
class _ScopeState:
|
31
|
+
# Shouldn't be instantiated outside `__SCOPES`.
|
32
|
+
|
33
|
+
__context_var: ContextVar[Scope] = field(
|
34
|
+
default_factory=lambda: ContextVar(f"scope@{new_short_key()}"),
|
35
|
+
init=False,
|
36
|
+
)
|
37
|
+
__references: set[Scope] = field(
|
38
|
+
default_factory=set,
|
39
|
+
init=False,
|
40
|
+
)
|
41
|
+
__shared_value: Scope | None = field(
|
42
|
+
default=None,
|
43
|
+
init=False,
|
44
|
+
)
|
45
|
+
|
46
|
+
@property
|
47
|
+
def active_scopes(self) -> Iterator[Scope]:
|
48
|
+
yield from self.__references
|
49
|
+
|
50
|
+
if shared_value := self.__shared_value:
|
51
|
+
yield shared_value
|
52
|
+
|
53
|
+
@contextmanager
|
54
|
+
def bind_contextual_scope(self, scope: Scope) -> Iterator[None]:
|
55
|
+
self.__references.add(scope)
|
56
|
+
token = self.__context_var.set(scope)
|
57
|
+
|
58
|
+
try:
|
59
|
+
yield
|
60
|
+
finally:
|
61
|
+
self.__context_var.reset(token)
|
62
|
+
self.__references.remove(scope)
|
63
|
+
|
64
|
+
@contextmanager
|
65
|
+
def bind_shared_scope(self, scope: Scope) -> Iterator[None]:
|
66
|
+
if next(self.active_scopes, None):
|
67
|
+
raise ScopeError(
|
68
|
+
"A shared scope can't be defined when one or more contextual scopes "
|
69
|
+
"are defined on the same name."
|
70
|
+
)
|
71
|
+
|
72
|
+
self.__shared_value = scope
|
73
|
+
|
74
|
+
try:
|
75
|
+
yield
|
76
|
+
finally:
|
77
|
+
self.__shared_value = None
|
78
|
+
|
79
|
+
def get_scope(self) -> Scope | None:
|
80
|
+
return self.__context_var.get(self.__shared_value)
|
81
|
+
|
82
|
+
|
83
|
+
__SCOPES: Final[defaultdict[str, _ScopeState]] = defaultdict(_ScopeState)
|
84
|
+
|
85
|
+
|
86
|
+
@asynccontextmanager
|
87
|
+
async def adefine_scope(name: str, *, shared: bool = False) -> AsyncIterator[None]:
|
88
|
+
async with AsyncScope() as scope:
|
89
|
+
scope.enter(_bind_scope(name, scope, shared))
|
90
|
+
yield
|
91
|
+
|
92
|
+
|
93
|
+
@contextmanager
|
94
|
+
def define_scope(name: str, *, shared: bool = False) -> Iterator[None]:
|
95
|
+
with SyncScope() as scope:
|
96
|
+
scope.enter(_bind_scope(name, scope, shared))
|
97
|
+
yield
|
98
|
+
|
99
|
+
|
100
|
+
def get_active_scopes(name: str) -> tuple[Scope, ...]:
|
101
|
+
return tuple(__SCOPES[name].active_scopes)
|
102
|
+
|
103
|
+
|
104
|
+
def get_scope(name: str) -> Scope:
|
105
|
+
scope = __SCOPES[name].get_scope()
|
106
|
+
|
107
|
+
if scope is None:
|
108
|
+
raise ScopeUndefinedError(
|
109
|
+
f"Scope `{name}` isn't defined in the current context."
|
110
|
+
)
|
111
|
+
|
112
|
+
return scope
|
113
|
+
|
114
|
+
|
115
|
+
@contextmanager
|
116
|
+
def _bind_scope(name: str, scope: Scope, shared: bool) -> Iterator[None]:
|
117
|
+
state = __SCOPES[name]
|
118
|
+
|
119
|
+
if state.get_scope():
|
120
|
+
raise ScopeAlreadyDefinedError(
|
121
|
+
f"Scope `{name}` is already defined in the current context."
|
122
|
+
)
|
123
|
+
|
124
|
+
strategy = (
|
125
|
+
state.bind_shared_scope(scope) if shared else state.bind_contextual_scope(scope)
|
126
|
+
)
|
127
|
+
|
128
|
+
try:
|
129
|
+
with strategy:
|
130
|
+
yield
|
131
|
+
finally:
|
132
|
+
scope.cache.clear()
|
133
|
+
|
134
|
+
|
135
|
+
@runtime_checkable
|
136
|
+
class Scope(Protocol):
|
137
|
+
__slots__ = ()
|
138
|
+
|
139
|
+
cache: MutableMapping[Any, Any]
|
140
|
+
|
141
|
+
@abstractmethod
|
142
|
+
async def aenter[T](self, context_manager: AsyncContextManager[T]) -> T:
|
143
|
+
raise NotImplementedError
|
144
|
+
|
145
|
+
@abstractmethod
|
146
|
+
def enter[T](self, context_manager: ContextManager[T]) -> T:
|
147
|
+
raise NotImplementedError
|
148
|
+
|
149
|
+
|
150
|
+
@dataclass(repr=False, frozen=True, slots=True)
|
151
|
+
class BaseScope[T](Scope, ABC):
|
152
|
+
delegate: T
|
153
|
+
cache: MutableMapping[Any, Any] = field(
|
154
|
+
default_factory=dict,
|
155
|
+
init=False,
|
156
|
+
hash=False,
|
157
|
+
)
|
158
|
+
|
159
|
+
|
160
|
+
class AsyncScope(BaseScope[AsyncExitStack]):
|
161
|
+
__slots__ = ()
|
162
|
+
|
163
|
+
def __init__(self) -> None:
|
164
|
+
super().__init__(delegate=AsyncExitStack())
|
165
|
+
|
166
|
+
async def __aenter__(self) -> Self:
|
167
|
+
await self.delegate.__aenter__()
|
168
|
+
return self
|
169
|
+
|
170
|
+
async def __aexit__(
|
171
|
+
self,
|
172
|
+
exc_type: type[BaseException] | None,
|
173
|
+
exc_value: BaseException | None,
|
174
|
+
traceback: TracebackType | None,
|
175
|
+
) -> Any:
|
176
|
+
return await self.delegate.__aexit__(exc_type, exc_value, traceback)
|
177
|
+
|
178
|
+
async def aenter[T](self, context_manager: AsyncContextManager[T]) -> T:
|
179
|
+
return await self.delegate.enter_async_context(context_manager)
|
180
|
+
|
181
|
+
def enter[T](self, context_manager: ContextManager[T]) -> T:
|
182
|
+
return self.delegate.enter_context(context_manager)
|
183
|
+
|
184
|
+
|
185
|
+
class SyncScope(BaseScope[ExitStack]):
|
186
|
+
__slots__ = ()
|
187
|
+
|
188
|
+
def __init__(self) -> None:
|
189
|
+
super().__init__(delegate=ExitStack())
|
190
|
+
|
191
|
+
def __enter__(self) -> Self:
|
192
|
+
self.delegate.__enter__()
|
193
|
+
return self
|
194
|
+
|
195
|
+
def __exit__(
|
196
|
+
self,
|
197
|
+
exc_type: type[BaseException] | None,
|
198
|
+
exc_value: BaseException | None,
|
199
|
+
traceback: TracebackType | None,
|
200
|
+
) -> Any:
|
201
|
+
return self.delegate.__exit__(exc_type, exc_value, traceback)
|
202
|
+
|
203
|
+
async def aenter[T](self, context_manager: AsyncContextManager[T]) -> NoReturn:
|
204
|
+
raise ScopeError(
|
205
|
+
"Synchronous scope doesn't support asynchronous context manager."
|
206
|
+
)
|
207
|
+
|
208
|
+
def enter[T](self, context_manager: ContextManager[T]) -> T:
|
209
|
+
return self.delegate.enter_context(context_manager)
|
injection/exceptions.py
CHANGED
@@ -7,6 +7,9 @@ __all__ = (
|
|
7
7
|
"ModuleLockError",
|
8
8
|
"ModuleNotUsedError",
|
9
9
|
"NoInjectable",
|
10
|
+
"ScopeAlreadyDefinedError",
|
11
|
+
"ScopeError",
|
12
|
+
"ScopeUndefinedError",
|
10
13
|
)
|
11
14
|
|
12
15
|
|
@@ -36,4 +39,13 @@ class ModuleLockError(ModuleError): ...
|
|
36
39
|
class ModuleNotUsedError(KeyError, ModuleError): ...
|
37
40
|
|
38
41
|
|
42
|
+
class ScopeError(InjectionError): ...
|
43
|
+
|
44
|
+
|
45
|
+
class ScopeUndefinedError(LookupError, ScopeError): ...
|
46
|
+
|
47
|
+
|
48
|
+
class ScopeAlreadyDefinedError(ScopeError): ...
|
49
|
+
|
50
|
+
|
39
51
|
class HookError(InjectionError): ...
|
@@ -1,11 +1,9 @@
|
|
1
|
-
from collections.abc import Awaitable
|
2
1
|
from types import GenericAlias
|
3
2
|
from typing import Any, TypeAliasType
|
4
3
|
|
5
4
|
from fastapi import Depends
|
6
5
|
|
7
6
|
from injection import Module, mod
|
8
|
-
from injection.exceptions import InjectionError
|
9
7
|
|
10
8
|
__all__ = ("Inject",)
|
11
9
|
|
@@ -13,51 +11,17 @@ __all__ = ("Inject",)
|
|
13
11
|
def Inject[T]( # noqa: N802
|
14
12
|
cls: type[T] | TypeAliasType | GenericAlias,
|
15
13
|
/,
|
14
|
+
default: T = NotImplemented,
|
16
15
|
module: Module | None = None,
|
17
|
-
*,
|
18
|
-
scoped: bool = True,
|
19
16
|
) -> Any:
|
20
17
|
"""
|
21
18
|
Declare a FastAPI dependency with `python-injection`.
|
22
19
|
"""
|
23
20
|
|
24
|
-
|
25
|
-
|
21
|
+
module = module or mod()
|
22
|
+
lazy_instance = module.aget_lazy_instance(cls, default)
|
26
23
|
|
24
|
+
async def getter() -> T:
|
25
|
+
return await lazy_instance
|
27
26
|
|
28
|
-
|
29
|
-
__slots__ = ("__class", "__lazy_instance", "__module")
|
30
|
-
|
31
|
-
__class: type[T] | TypeAliasType | GenericAlias
|
32
|
-
__lazy_instance: Awaitable[T]
|
33
|
-
__module: Module
|
34
|
-
|
35
|
-
def __init__(
|
36
|
-
self,
|
37
|
-
cls: type[T] | TypeAliasType | GenericAlias,
|
38
|
-
module: Module,
|
39
|
-
) -> None:
|
40
|
-
self.__class = cls
|
41
|
-
self.__lazy_instance = module.aget_lazy_instance(cls, default=NotImplemented)
|
42
|
-
self.__module = module
|
43
|
-
|
44
|
-
async def __call__(self) -> T:
|
45
|
-
instance = await self.__lazy_instance
|
46
|
-
|
47
|
-
if instance is NotImplemented:
|
48
|
-
raise InjectionError(f"`{self.__class}` is an unknown dependency.")
|
49
|
-
|
50
|
-
return instance
|
51
|
-
|
52
|
-
def __eq__(self, other: Any) -> bool:
|
53
|
-
if isinstance(other, type(self)):
|
54
|
-
return self.__key == other.__key
|
55
|
-
|
56
|
-
return NotImplemented
|
57
|
-
|
58
|
-
def __hash__(self) -> int:
|
59
|
-
return hash(self.__key)
|
60
|
-
|
61
|
-
@property
|
62
|
-
def __key(self) -> tuple[type[T] | TypeAliasType | GenericAlias, Module]:
|
63
|
-
return self.__class, self.__module
|
27
|
+
return Depends(getter, use_cache=False)
|
injection/testing/__init__.py
CHANGED
@@ -9,6 +9,7 @@ __all__ = (
|
|
9
9
|
"should_be_test_injectable",
|
10
10
|
"test_constant",
|
11
11
|
"test_injectable",
|
12
|
+
"test_scoped",
|
12
13
|
"test_singleton",
|
13
14
|
)
|
14
15
|
|
@@ -18,6 +19,7 @@ set_test_constant = mod(_TEST_PROFILE_NAME).set_constant
|
|
18
19
|
should_be_test_injectable = mod(_TEST_PROFILE_NAME).should_be_injectable
|
19
20
|
test_constant = mod(_TEST_PROFILE_NAME).constant
|
20
21
|
test_injectable = mod(_TEST_PROFILE_NAME).injectable
|
22
|
+
test_scoped = mod(_TEST_PROFILE_NAME).scoped
|
21
23
|
test_singleton = mod(_TEST_PROFILE_NAME).singleton
|
22
24
|
|
23
25
|
|
injection/testing/__init__.pyi
CHANGED
@@ -1,12 +1,15 @@
|
|
1
|
-
from typing import ContextManager
|
1
|
+
from typing import ContextManager, Final
|
2
2
|
|
3
|
-
|
3
|
+
from injection import Module
|
4
4
|
|
5
|
-
|
6
|
-
|
7
|
-
|
8
|
-
|
9
|
-
|
5
|
+
__MODULE: Final[Module] = ...
|
6
|
+
|
7
|
+
set_test_constant = __MODULE.set_constant
|
8
|
+
should_be_test_injectable = __MODULE.should_be_injectable
|
9
|
+
test_constant = __MODULE.constant
|
10
|
+
test_injectable = __MODULE.injectable
|
11
|
+
test_scoped = __MODULE.scoped
|
12
|
+
test_singleton = __MODULE.singleton
|
10
13
|
|
11
14
|
def load_test_profile(*names: str) -> ContextManager[None]:
|
12
15
|
"""
|
injection/utils.py
CHANGED
@@ -42,12 +42,17 @@ def load_modules_with_keywords(
|
|
42
42
|
"""
|
43
43
|
Function to import modules from a Python package if one of the keywords is contained in the Python script.
|
44
44
|
The default keywords are:
|
45
|
-
- `from injection`
|
45
|
+
- `from injection `
|
46
|
+
- `from injection.`
|
46
47
|
- `import injection`
|
47
48
|
"""
|
48
49
|
|
49
50
|
if keywords is None:
|
50
|
-
keywords =
|
51
|
+
keywords = (
|
52
|
+
f"from {injection_package_name} ",
|
53
|
+
f"from {injection_package_name}.",
|
54
|
+
f"import {injection_package_name}",
|
55
|
+
)
|
51
56
|
|
52
57
|
b_keywords = tuple(keyword.encode() for keyword in keywords)
|
53
58
|
|
@@ -1,31 +1,30 @@
|
|
1
|
-
Metadata-Version: 2.
|
1
|
+
Metadata-Version: 2.4
|
2
2
|
Name: python-injection
|
3
|
-
Version: 0.
|
3
|
+
Version: 0.13.1
|
4
4
|
Summary: Fast and easy dependency injection framework.
|
5
|
-
|
5
|
+
Project-URL: Repository, https://github.com/100nm/python-injection
|
6
|
+
Author: remimd
|
6
7
|
License: MIT
|
7
8
|
Keywords: dependencies,dependency,inject,injection
|
8
|
-
Author: remimd
|
9
|
-
Requires-Python: >=3.12, <4
|
10
9
|
Classifier: Development Status :: 4 - Beta
|
10
|
+
Classifier: Intended Audience :: Developers
|
11
|
+
Classifier: Natural Language :: English
|
12
|
+
Classifier: Operating System :: OS Independent
|
13
|
+
Classifier: Programming Language :: Python
|
14
|
+
Classifier: Programming Language :: Python :: 3
|
15
|
+
Classifier: Programming Language :: Python :: 3 :: Only
|
11
16
|
Classifier: Topic :: Software Development :: Libraries
|
12
17
|
Classifier: Topic :: Software Development :: Libraries :: Application Frameworks
|
13
18
|
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
14
19
|
Classifier: Topic :: Software Development :: Testing
|
15
|
-
Classifier: Programming Language :: Python
|
16
|
-
Classifier: Programming Language :: Python :: 3
|
17
|
-
Classifier: Programming Language :: Python :: 3 :: Only
|
18
|
-
Classifier: Operating System :: OS Independent
|
19
|
-
Classifier: Intended Audience :: Developers
|
20
|
-
Classifier: Natural Language :: English
|
21
20
|
Classifier: Typing :: Typed
|
22
|
-
|
21
|
+
Requires-Python: <4,>=3.12
|
23
22
|
Description-Content-Type: text/markdown
|
24
23
|
|
25
24
|
# python-injection
|
26
25
|
|
27
26
|
[](https://github.com/100nm/python-injection)
|
28
|
-
[](https://pypi.org/project/python-injection
|
27
|
+
[](https://pypi.org/project/python-injection)
|
29
28
|
[](https://github.com/astral-sh/ruff)
|
30
29
|
|
31
30
|
Fast and easy dependency injection framework.
|
@@ -80,9 +79,9 @@ if __name__ == "__main__":
|
|
80
79
|
## Resources
|
81
80
|
|
82
81
|
* [**Basic usage**](https://github.com/100nm/python-injection/tree/prod/documentation/basic-usage.md)
|
82
|
+
* [**Scoped dependencies**](https://github.com/100nm/python-injection/tree/prod/documentation/scoped-dependencies.md)
|
83
83
|
* [**Testing**](https://github.com/100nm/python-injection/tree/prod/documentation/testing.md)
|
84
84
|
* [**Advanced usage**](https://github.com/100nm/python-injection/tree/prod/documentation/advanced-usage.md)
|
85
85
|
* [**Utils**](https://github.com/100nm/python-injection/tree/prod/documentation/utils.md)
|
86
86
|
* [**Integrations**](https://github.com/100nm/python-injection/tree/prod/documentation/integrations.md)
|
87
|
-
* [**Concrete example**](https://github.com/100nm/python-injection
|
88
|
-
|
87
|
+
* [**Concrete example**](https://github.com/100nm/python-injection-example)
|
@@ -0,0 +1,25 @@
|
|
1
|
+
injection/__init__.py,sha256=X0vIAoN4MDlhR7YIkup1qHgqbOkwZ3PWgdSi7h-udaM,1049
|
2
|
+
injection/__init__.pyi,sha256=LXBL7_ihiw6NyU94xWGdXkWwxvzRBoLYBSOv88PV2WM,9640
|
3
|
+
injection/exceptions.py,sha256=T__732aXxWWUz0sKc39ySteyolCS5tpqQC0oCnzUF2E,917
|
4
|
+
injection/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
5
|
+
injection/utils.py,sha256=SZBzmVAwRL-kilS2eRt9MCeNAL4zeaj_7Rm_2NLBzg0,3175
|
6
|
+
injection/_core/__init__.py,sha256=XERocCxCZBxPGIaOR37yeiQOZyvjHQ6a4rgRmlkUSuU,1367
|
7
|
+
injection/_core/descriptors.py,sha256=7fSHlgAqmgR_Uta8KocBapOt1Xyj2dI7RY9ZdoStTzw,726
|
8
|
+
injection/_core/hook.py,sha256=Qv505pr3kjOE6UitftlLh9JKX9OCNqZBRtHbFha1gqM,3130
|
9
|
+
injection/_core/injectables.py,sha256=SApYnP6vG3b1qi_KJx6hkJyPoyTmZfooxqPpOsxWSVI,4482
|
10
|
+
injection/_core/module.py,sha256=wOoHW_bImPSwoxi3i8_rFXP_HWFJ_S78UuY_5EIilFQ,30595
|
11
|
+
injection/_core/scope.py,sha256=TKOoxCmi2FIoXis0irReVNayDTte4KjLrTq8ZDcrOSk,5583
|
12
|
+
injection/_core/common/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
13
|
+
injection/_core/common/asynchronous.py,sha256=LlYMde_55osS3r8Sc3Fh5lgPp5UWmIfvyBzUeIDmMiM,1698
|
14
|
+
injection/_core/common/event.py,sha256=XjzV8gxtGlGvzZs_ykvoC60qmdpd3RN08Eiqz5QUwes,1236
|
15
|
+
injection/_core/common/invertible.py,sha256=YZlAdh6bNJgf1-74TRjwJTm8xrlgY95ZhOUGLSJ4XcY,482
|
16
|
+
injection/_core/common/key.py,sha256=ghkZD-Y8Moz6SEPNgMh3xgsZUjDVq-XYAmXaCu5VuCA,80
|
17
|
+
injection/_core/common/lazy.py,sha256=ZKx2O9CCFsF9F0SLM4zb7jSLksJUv-FBLCPlWltMN5k,1398
|
18
|
+
injection/_core/common/type.py,sha256=QbBBhJp7i1p6gLzWX0TgofvfG7yDH-gHfEQcssVZeHo,2186
|
19
|
+
injection/integrations/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
20
|
+
injection/integrations/fastapi.py,sha256=YHSs85_3m6TUVtOwUcV157b3UZJQIw_aXWAg199a-YE,594
|
21
|
+
injection/testing/__init__.py,sha256=ALcKuDYNdslmpgqotZzSWrXAW0kNNFUs9nzfO1ZgIGc,783
|
22
|
+
injection/testing/__init__.pyi,sha256=6rv5NOYHEaiKMd82E1IIc8lFlLV9ttY57DLiMqGTXt8,482
|
23
|
+
python_injection-0.13.1.dist-info/METADATA,sha256=y4ls59BE2tC-G8V8chV5dMl4um6zcLtYOejDr03Z3jQ,2996
|
24
|
+
python_injection-0.13.1.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
|
25
|
+
python_injection-0.13.1.dist-info/RECORD,,
|
@@ -1,24 +0,0 @@
|
|
1
|
-
injection/__init__.py,sha256=1MI9kXuQfsqLGQK_5-Zm4JjecqopjGNlhsAW8iihn9M,919
|
2
|
-
injection/__init__.pyi,sha256=Gwtvo6sJVizqz0pTKNaG-Jz_NW0gSqEsoZF7T6p3V1s,8820
|
3
|
-
injection/_core/__init__.py,sha256=XERocCxCZBxPGIaOR37yeiQOZyvjHQ6a4rgRmlkUSuU,1367
|
4
|
-
injection/_core/common/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
5
|
-
injection/_core/common/asynchronous.py,sha256=Sd7r5y42WSNXRtC4NvYRn5y2Jb6nNapj0aAOvkS4WIU,1726
|
6
|
-
injection/_core/common/event.py,sha256=kev8WUESAXzMn11EJsbTcICaCvYiD556E6BOLvQ54s4,1243
|
7
|
-
injection/_core/common/invertible.py,sha256=YZlAdh6bNJgf1-74TRjwJTm8xrlgY95ZhOUGLSJ4XcY,482
|
8
|
-
injection/_core/common/lazy.py,sha256=ZKx2O9CCFsF9F0SLM4zb7jSLksJUv-FBLCPlWltMN5k,1398
|
9
|
-
injection/_core/common/threading.py,sha256=OXm7L3p8c7O7eSkU-RTR7cobqIGMhuo-7gpDXsWKDNQ,214
|
10
|
-
injection/_core/common/type.py,sha256=c4QfvbkMfYMlNxqt-vq6QJ83ubMnw6AIVI2Rp-tV1PI,1550
|
11
|
-
injection/_core/descriptors.py,sha256=y1rFTQdCDIMLVQfuQE8ZkTPlVZKgzvwZ2Y20Si05DwM,662
|
12
|
-
injection/_core/hook.py,sha256=Qv505pr3kjOE6UitftlLh9JKX9OCNqZBRtHbFha1gqM,3130
|
13
|
-
injection/_core/injectables.py,sha256=rBcrTYRpZ69LkHSGm6bve6TGPwf66XkuK2XVACwcRNc,2427
|
14
|
-
injection/_core/module.py,sha256=e0FVvhplVRbOGrAT-RCNKIR2pTng5qEAxCjuurm0cEA,28675
|
15
|
-
injection/exceptions.py,sha256=-5Shs7R5rctQXhpMLfcjiMBCzrtFWxC88qETUIHz57s,692
|
16
|
-
injection/integrations/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
17
|
-
injection/integrations/fastapi.py,sha256=tI9ohXOm_WucqkS-DZJceIVb_mE9LOY_UpIcr6hfRVM,1668
|
18
|
-
injection/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
19
|
-
injection/testing/__init__.py,sha256=Wd9sq43CJCV7GjYFSIaikIf4hJ5lfVNS7GP3iI8a1X8,719
|
20
|
-
injection/testing/__init__.pyi,sha256=Dyf1LjSj3LrjghXjM2xkMiINg-OPWQMtjCEnnfWzaDY,372
|
21
|
-
injection/utils.py,sha256=a9y95B08Fr9IgDiBffeQdcawQcvAs5gwBDt9cCYX0uE,3061
|
22
|
-
python_injection-0.12.3.dist-info/METADATA,sha256=8AyuGaX3P5JfCRxQBcnMrwkssXYeKP52UC64xJ5XE-o,2958
|
23
|
-
python_injection-0.12.3.dist-info/WHEEL,sha256=RaoafKOydTQ7I_I3JTrPCg6kUmTgtm4BornzOqyEfJ8,88
|
24
|
-
python_injection-0.12.3.dist-info/RECORD,,
|