python-injection 0.26.2__tar.gz → 0.26.4__tar.gz
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.
- {python_injection-0.26.2 → python_injection-0.26.4}/PKG-INFO +9 -4
- {python_injection-0.26.2 → python_injection-0.26.4}/docs/index.md +6 -2
- {python_injection-0.26.2 → python_injection-0.26.4}/injection/_core/common/asynchronous.py +5 -10
- {python_injection-0.26.2 → python_injection-0.26.4}/injection/_core/common/event.py +3 -3
- {python_injection-0.26.2 → python_injection-0.26.4}/injection/_core/common/lazy.py +1 -1
- python_injection-0.26.4/injection/_core/common/threading.py +22 -0
- {python_injection-0.26.2 → python_injection-0.26.4}/injection/_core/descriptors.py +2 -2
- {python_injection-0.26.2 → python_injection-0.26.4}/injection/_core/injectables.py +8 -10
- {python_injection-0.26.2 → python_injection-0.26.4}/injection/_core/locator.py +2 -3
- {python_injection-0.26.2 → python_injection-0.26.4}/injection/_core/module.py +41 -34
- {python_injection-0.26.2 → python_injection-0.26.4}/injection/_core/scope.py +29 -19
- {python_injection-0.26.2 → python_injection-0.26.4}/injection/entrypoint.py +10 -10
- {python_injection-0.26.2 → python_injection-0.26.4}/pyproject.toml +5 -4
- python_injection-0.26.2/injection/_core/common/threading.py +0 -13
- {python_injection-0.26.2 → python_injection-0.26.4}/.gitignore +0 -0
- {python_injection-0.26.2 → python_injection-0.26.4}/LICENSE +0 -0
- {python_injection-0.26.2 → python_injection-0.26.4}/injection/__init__.py +0 -0
- {python_injection-0.26.2 → python_injection-0.26.4}/injection/__init__.pyi +0 -0
- {python_injection-0.26.2 → python_injection-0.26.4}/injection/_core/__init__.py +0 -0
- {python_injection-0.26.2 → python_injection-0.26.4}/injection/_core/asfunction.py +0 -0
- {python_injection-0.26.2 → python_injection-0.26.4}/injection/_core/common/__init__.py +0 -0
- {python_injection-0.26.2 → python_injection-0.26.4}/injection/_core/common/invertible.py +0 -0
- {python_injection-0.26.2 → python_injection-0.26.4}/injection/_core/common/type.py +0 -0
- {python_injection-0.26.2 → python_injection-0.26.4}/injection/_core/slots.py +0 -0
- {python_injection-0.26.2 → python_injection-0.26.4}/injection/exceptions.py +0 -0
- {python_injection-0.26.2 → python_injection-0.26.4}/injection/ext/__init__.py +0 -0
- {python_injection-0.26.2 → python_injection-0.26.4}/injection/ext/fastapi.py +0 -0
- {python_injection-0.26.2 → python_injection-0.26.4}/injection/ext/fastapi.pyi +0 -0
- {python_injection-0.26.2 → python_injection-0.26.4}/injection/loaders.py +0 -0
- {python_injection-0.26.2 → python_injection-0.26.4}/injection/py.typed +0 -0
- {python_injection-0.26.2 → python_injection-0.26.4}/injection/testing/__init__.py +0 -0
- {python_injection-0.26.2 → python_injection-0.26.4}/injection/testing/__init__.pyi +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
Metadata-Version: 2.
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
2
|
Name: python-injection
|
|
3
|
-
Version: 0.26.
|
|
3
|
+
Version: 0.26.4
|
|
4
4
|
Summary: Dead-simple dependency injection framework for Python.
|
|
5
5
|
Project-URL: Documentation, https://python-injection.remimd.dev
|
|
6
6
|
Project-URL: Repository, https://github.com/100nm/python-injection
|
|
@@ -18,6 +18,7 @@ Classifier: Programming Language :: Python :: 3 :: Only
|
|
|
18
18
|
Classifier: Programming Language :: Python :: 3.12
|
|
19
19
|
Classifier: Programming Language :: Python :: 3.13
|
|
20
20
|
Classifier: Programming Language :: Python :: 3.14
|
|
21
|
+
Classifier: Programming Language :: Python :: 3.15
|
|
21
22
|
Classifier: Topic :: Software Development :: Libraries
|
|
22
23
|
Classifier: Topic :: Software Development :: Libraries :: Application Frameworks
|
|
23
24
|
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
@@ -31,8 +32,8 @@ Description-Content-Type: text/markdown
|
|
|
31
32
|
|
|
32
33
|
# python-injection
|
|
33
34
|
|
|
34
|
-
[](https://pypi.org/project/python-injection)
|
|
36
|
+
[](https://pypistats.org/packages/python-injection)
|
|
36
37
|
|
|
37
38
|
## Project motivations
|
|
38
39
|
|
|
@@ -68,6 +69,7 @@ Simply apply the decorators and the package takes care of the rest.
|
|
|
68
69
|
```python
|
|
69
70
|
from injection import injectable, inject, singleton
|
|
70
71
|
|
|
72
|
+
|
|
71
73
|
@singleton
|
|
72
74
|
class Printer:
|
|
73
75
|
def __init__(self):
|
|
@@ -77,6 +79,7 @@ class Printer:
|
|
|
77
79
|
self.history.append(message)
|
|
78
80
|
print(message)
|
|
79
81
|
|
|
82
|
+
|
|
80
83
|
@injectable
|
|
81
84
|
class Service:
|
|
82
85
|
def __init__(self, printer: Printer):
|
|
@@ -85,10 +88,12 @@ class Service:
|
|
|
85
88
|
def hello(self):
|
|
86
89
|
self.printer.print("Hello world!")
|
|
87
90
|
|
|
91
|
+
|
|
88
92
|
@inject
|
|
89
93
|
def main(service: Service):
|
|
90
94
|
service.hello()
|
|
91
95
|
|
|
96
|
+
|
|
92
97
|
if __name__ == "__main__":
|
|
93
98
|
main()
|
|
94
99
|
```
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
# python-injection
|
|
2
2
|
|
|
3
|
-
[](https://pypi.org/project/python-injection)
|
|
4
|
+
[](https://pypistats.org/packages/python-injection)
|
|
5
5
|
|
|
6
6
|
## Project motivations
|
|
7
7
|
|
|
@@ -37,6 +37,7 @@ Simply apply the decorators and the package takes care of the rest.
|
|
|
37
37
|
```python
|
|
38
38
|
from injection import injectable, inject, singleton
|
|
39
39
|
|
|
40
|
+
|
|
40
41
|
@singleton
|
|
41
42
|
class Printer:
|
|
42
43
|
def __init__(self):
|
|
@@ -46,6 +47,7 @@ class Printer:
|
|
|
46
47
|
self.history.append(message)
|
|
47
48
|
print(message)
|
|
48
49
|
|
|
50
|
+
|
|
49
51
|
@injectable
|
|
50
52
|
class Service:
|
|
51
53
|
def __init__(self, printer: Printer):
|
|
@@ -54,10 +56,12 @@ class Service:
|
|
|
54
56
|
def hello(self):
|
|
55
57
|
self.printer.print("Hello world!")
|
|
56
58
|
|
|
59
|
+
|
|
57
60
|
@inject
|
|
58
61
|
def main(service: Service):
|
|
59
62
|
service.hello()
|
|
60
63
|
|
|
64
|
+
|
|
61
65
|
if __name__ == "__main__":
|
|
62
66
|
main()
|
|
63
67
|
```
|
|
@@ -1,15 +1,10 @@
|
|
|
1
1
|
from abc import abstractmethod
|
|
2
2
|
from collections.abc import Awaitable, Callable, Generator
|
|
3
|
+
from contextlib import AbstractAsyncContextManager
|
|
3
4
|
from dataclasses import dataclass
|
|
4
|
-
from typing import
|
|
5
|
-
Any,
|
|
6
|
-
AsyncContextManager,
|
|
7
|
-
NoReturn,
|
|
8
|
-
Protocol,
|
|
9
|
-
runtime_checkable,
|
|
10
|
-
)
|
|
5
|
+
from typing import Any, NoReturn, Protocol, runtime_checkable
|
|
11
6
|
|
|
12
|
-
|
|
7
|
+
AsyncLock: Callable[[], AbstractAsyncContextManager[Any]]
|
|
13
8
|
|
|
14
9
|
try:
|
|
15
10
|
import anyio
|
|
@@ -17,10 +12,10 @@ try:
|
|
|
17
12
|
except ImportError: # pragma: no cover
|
|
18
13
|
import asyncio
|
|
19
14
|
|
|
20
|
-
|
|
15
|
+
AsyncLock = asyncio.Lock
|
|
21
16
|
|
|
22
17
|
else:
|
|
23
|
-
|
|
18
|
+
AsyncLock = anyio.Lock
|
|
24
19
|
|
|
25
20
|
|
|
26
21
|
@dataclass(repr=False, eq=False, frozen=True, slots=True)
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
from abc import ABC, abstractmethod
|
|
2
2
|
from collections.abc import Iterator
|
|
3
|
-
from contextlib import ExitStack, contextmanager
|
|
3
|
+
from contextlib import AbstractContextManager, ExitStack, contextmanager
|
|
4
4
|
from dataclasses import dataclass, field
|
|
5
|
-
from typing import
|
|
5
|
+
from typing import Self
|
|
6
6
|
from weakref import WeakSet
|
|
7
7
|
|
|
8
8
|
|
|
@@ -14,7 +14,7 @@ class EventListener(ABC):
|
|
|
14
14
|
__slots__ = ("__weakref__",)
|
|
15
15
|
|
|
16
16
|
@abstractmethod
|
|
17
|
-
def on_event(self, event: Event, /) ->
|
|
17
|
+
def on_event(self, event: Event, /) -> AbstractContextManager[None] | None:
|
|
18
18
|
raise NotImplementedError
|
|
19
19
|
|
|
20
20
|
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from contextlib import AbstractContextManager, nullcontext
|
|
4
|
+
from os import getenv
|
|
5
|
+
from threading import Lock
|
|
6
|
+
from typing import Any, Final
|
|
7
|
+
|
|
8
|
+
_PYTHON_INJECTION_THREADSAFE: Final[bool] = bool(
|
|
9
|
+
int(getenv("PYTHON_INJECTION_THREADSAFE", "0"))
|
|
10
|
+
)
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def lock_if_threadsafe(
|
|
14
|
+
lock: AbstractContextManager[Any] | None = None,
|
|
15
|
+
threadsafe: bool | None = None,
|
|
16
|
+
) -> AbstractContextManager[Any]:
|
|
17
|
+
threadsafe = _PYTHON_INJECTION_THREADSAFE if threadsafe is None else threadsafe
|
|
18
|
+
|
|
19
|
+
if not threadsafe:
|
|
20
|
+
return nullcontext()
|
|
21
|
+
|
|
22
|
+
return lock or Lock()
|
|
@@ -73,7 +73,7 @@ class BoundMappedScope:
|
|
|
73
73
|
async def adefine(
|
|
74
74
|
self,
|
|
75
75
|
/,
|
|
76
|
-
kind: ScopeKind | ScopeKindStr = ScopeKind.get_default(),
|
|
76
|
+
kind: ScopeKind | ScopeKindStr = ScopeKind.get_default(), # noqa: B008
|
|
77
77
|
threadsafe: bool | None = None,
|
|
78
78
|
) -> AsyncIterator[None]:
|
|
79
79
|
async with adefine_scope(self.name, kind, threadsafe) as scope:
|
|
@@ -86,7 +86,7 @@ class BoundMappedScope:
|
|
|
86
86
|
def define(
|
|
87
87
|
self,
|
|
88
88
|
/,
|
|
89
|
-
kind: ScopeKind | ScopeKindStr = ScopeKind.get_default(),
|
|
89
|
+
kind: ScopeKind | ScopeKindStr = ScopeKind.get_default(), # noqa: B008
|
|
90
90
|
threadsafe: bool | None = None,
|
|
91
91
|
) -> Iterator[None]:
|
|
92
92
|
with define_scope(self.name, kind, threadsafe) as scope:
|
|
@@ -1,20 +1,18 @@
|
|
|
1
1
|
from abc import ABC, abstractmethod
|
|
2
2
|
from collections.abc import Awaitable, Callable, MutableMapping, Sequence
|
|
3
|
-
from contextlib import suppress
|
|
3
|
+
from contextlib import AbstractAsyncContextManager, AbstractContextManager, suppress
|
|
4
4
|
from dataclasses import dataclass, field
|
|
5
5
|
from functools import partial
|
|
6
6
|
from typing import (
|
|
7
7
|
Any,
|
|
8
|
-
AsyncContextManager,
|
|
9
8
|
ClassVar,
|
|
10
|
-
ContextManager,
|
|
11
9
|
NoReturn,
|
|
12
10
|
Protocol,
|
|
13
11
|
Self,
|
|
14
12
|
runtime_checkable,
|
|
15
13
|
)
|
|
16
14
|
|
|
17
|
-
from injection._core.common.asynchronous import
|
|
15
|
+
from injection._core.common.asynchronous import AsyncLock, Caller
|
|
18
16
|
from injection._core.common.type import InputType
|
|
19
17
|
from injection._core.scope import Scope, get_first_scope, in_scope_cache
|
|
20
18
|
from injection._core.slots import SlotKey
|
|
@@ -53,12 +51,12 @@ class TransientInjectable[T](Injectable[T]):
|
|
|
53
51
|
|
|
54
52
|
|
|
55
53
|
class CacheLogic[T]:
|
|
56
|
-
__slots__ = ("
|
|
54
|
+
__slots__ = ("__lock",)
|
|
57
55
|
|
|
58
|
-
|
|
56
|
+
__lock: AbstractAsyncContextManager[Any]
|
|
59
57
|
|
|
60
58
|
def __init__(self) -> None:
|
|
61
|
-
self.
|
|
59
|
+
self.__lock = AsyncLock()
|
|
62
60
|
|
|
63
61
|
async def aget_or_create[K](
|
|
64
62
|
self,
|
|
@@ -66,7 +64,7 @@ class CacheLogic[T]:
|
|
|
66
64
|
key: K,
|
|
67
65
|
factory: Callable[..., Awaitable[T]],
|
|
68
66
|
) -> T:
|
|
69
|
-
async with self.
|
|
67
|
+
async with self.__lock:
|
|
70
68
|
with suppress(KeyError):
|
|
71
69
|
return cache[key]
|
|
72
70
|
|
|
@@ -169,7 +167,7 @@ class ScopedInjectable[R, T](Injectable[T], ABC):
|
|
|
169
167
|
return partial(cls, scope_names=names)
|
|
170
168
|
|
|
171
169
|
|
|
172
|
-
class AsyncCMScopedInjectable[T](ScopedInjectable[
|
|
170
|
+
class AsyncCMScopedInjectable[T](ScopedInjectable[AbstractAsyncContextManager[T], T]):
|
|
173
171
|
__slots__ = ()
|
|
174
172
|
|
|
175
173
|
async def abuild(self, scope: Scope) -> T:
|
|
@@ -180,7 +178,7 @@ class AsyncCMScopedInjectable[T](ScopedInjectable[AsyncContextManager[T], T]):
|
|
|
180
178
|
raise RuntimeError("Can't use async context manager synchronously.")
|
|
181
179
|
|
|
182
180
|
|
|
183
|
-
class CMScopedInjectable[T](ScopedInjectable[
|
|
181
|
+
class CMScopedInjectable[T](ScopedInjectable[AbstractContextManager[T], T]):
|
|
184
182
|
__slots__ = ()
|
|
185
183
|
|
|
186
184
|
async def abuild(self, scope: Scope) -> T:
|
|
@@ -2,13 +2,12 @@ from __future__ import annotations
|
|
|
2
2
|
|
|
3
3
|
from abc import ABC, abstractmethod
|
|
4
4
|
from collections.abc import Awaitable, Callable, Collection, Iterable, Iterator
|
|
5
|
-
from contextlib import suppress
|
|
5
|
+
from contextlib import AbstractContextManager, suppress
|
|
6
6
|
from dataclasses import dataclass, field
|
|
7
7
|
from enum import StrEnum
|
|
8
8
|
from inspect import iscoroutinefunction
|
|
9
9
|
from typing import (
|
|
10
10
|
Any,
|
|
11
|
-
ContextManager,
|
|
12
11
|
Literal,
|
|
13
12
|
NamedTuple,
|
|
14
13
|
Protocol,
|
|
@@ -226,7 +225,7 @@ class Locator:
|
|
|
226
225
|
self.__channel.add_listener(listener)
|
|
227
226
|
return self
|
|
228
227
|
|
|
229
|
-
def dispatch(self, event: Event) ->
|
|
228
|
+
def dispatch(self, event: Event) -> AbstractContextManager[None]:
|
|
230
229
|
return self.__channel.dispatch(event)
|
|
231
230
|
|
|
232
231
|
def __iter_injectables(
|
|
@@ -14,7 +14,13 @@ from collections.abc import (
|
|
|
14
14
|
Iterator,
|
|
15
15
|
Mapping,
|
|
16
16
|
)
|
|
17
|
-
from contextlib import
|
|
17
|
+
from contextlib import (
|
|
18
|
+
AbstractAsyncContextManager,
|
|
19
|
+
AbstractContextManager,
|
|
20
|
+
asynccontextmanager,
|
|
21
|
+
contextmanager,
|
|
22
|
+
suppress,
|
|
23
|
+
)
|
|
18
24
|
from dataclasses import dataclass, field
|
|
19
25
|
from enum import StrEnum
|
|
20
26
|
from functools import partialmethod, singledispatchmethod, update_wrapper
|
|
@@ -29,18 +35,9 @@ from inspect import (
|
|
|
29
35
|
)
|
|
30
36
|
from inspect import signature as inspect_signature
|
|
31
37
|
from logging import Logger, getLogger
|
|
38
|
+
from threading import RLock
|
|
32
39
|
from types import MethodType
|
|
33
|
-
from typing import
|
|
34
|
-
TYPE_CHECKING,
|
|
35
|
-
Any,
|
|
36
|
-
AsyncContextManager,
|
|
37
|
-
ClassVar,
|
|
38
|
-
ContextManager,
|
|
39
|
-
Literal,
|
|
40
|
-
NamedTuple,
|
|
41
|
-
Self,
|
|
42
|
-
overload,
|
|
43
|
-
)
|
|
40
|
+
from typing import TYPE_CHECKING, Any, ClassVar, Literal, NamedTuple, Self, overload
|
|
44
41
|
|
|
45
42
|
from type_analyzer import MatchingTypesConfig, iter_matching_types, matching_types
|
|
46
43
|
|
|
@@ -48,7 +45,7 @@ from injection._core.common.asynchronous import Caller, SimpleAwaitable
|
|
|
48
45
|
from injection._core.common.event import Event, EventChannel, EventListener
|
|
49
46
|
from injection._core.common.invertible import Invertible, SimpleInvertible
|
|
50
47
|
from injection._core.common.lazy import Lazy
|
|
51
|
-
from injection._core.common.threading import
|
|
48
|
+
from injection._core.common.threading import lock_if_threadsafe
|
|
52
49
|
from injection._core.common.type import (
|
|
53
50
|
InputType,
|
|
54
51
|
TypeInfo,
|
|
@@ -157,7 +154,7 @@ class Priority(StrEnum):
|
|
|
157
154
|
type PriorityStr = Literal["low", "high"]
|
|
158
155
|
|
|
159
156
|
type ContextManagerRecipe[**P, T] = (
|
|
160
|
-
Callable[P,
|
|
157
|
+
Callable[P, AbstractContextManager[T]] | Callable[P, AbstractAsyncContextManager[T]]
|
|
161
158
|
)
|
|
162
159
|
type GeneratorRecipe[**P, T] = (
|
|
163
160
|
Callable[P, Generator[T, Any, Any]] | Callable[P, AsyncGenerator[T, Any]]
|
|
@@ -184,6 +181,11 @@ class Module(EventListener, InjectionProvider): # type: ignore[misc]
|
|
|
184
181
|
init=False,
|
|
185
182
|
repr=False,
|
|
186
183
|
)
|
|
184
|
+
__lock: RLock = field(
|
|
185
|
+
default_factory=RLock,
|
|
186
|
+
init=False,
|
|
187
|
+
repr=False,
|
|
188
|
+
)
|
|
187
189
|
__loggers: list[Logger] = field(
|
|
188
190
|
default_factory=lambda: [getLogger("python-injection")],
|
|
189
191
|
init=False,
|
|
@@ -231,7 +233,7 @@ class Module(EventListener, InjectionProvider): # type: ignore[misc]
|
|
|
231
233
|
ignore_type_hint: bool = False,
|
|
232
234
|
inject: bool = True,
|
|
233
235
|
on: TypeInfo[T] = (),
|
|
234
|
-
mode: Mode | ModeStr = Mode.get_default(),
|
|
236
|
+
mode: Mode | ModeStr = Mode.get_default(), # noqa: B008
|
|
235
237
|
) -> Any:
|
|
236
238
|
def decorator(wp: Recipe[P, T]) -> Recipe[P, T]:
|
|
237
239
|
hints = on if ignore_type_hint else (wp, on)
|
|
@@ -255,7 +257,7 @@ class Module(EventListener, InjectionProvider): # type: ignore[misc]
|
|
|
255
257
|
ignore_type_hint: bool = False,
|
|
256
258
|
inject: bool = True,
|
|
257
259
|
on: TypeInfo[T] = (),
|
|
258
|
-
mode: Mode | ModeStr = Mode.get_default(),
|
|
260
|
+
mode: Mode | ModeStr = Mode.get_default(), # noqa: B008
|
|
259
261
|
) -> Any:
|
|
260
262
|
def decorator(
|
|
261
263
|
wrapped: Recipe[P, T] | GeneratorRecipe[P, T],
|
|
@@ -307,7 +309,7 @@ class Module(EventListener, InjectionProvider): # type: ignore[misc]
|
|
|
307
309
|
on: TypeInfo[T] = (),
|
|
308
310
|
*,
|
|
309
311
|
alias: bool = False,
|
|
310
|
-
mode: Mode | ModeStr = Mode.get_default(),
|
|
312
|
+
mode: Mode | ModeStr = Mode.get_default(), # noqa: B008
|
|
311
313
|
) -> T:
|
|
312
314
|
if not alias:
|
|
313
315
|
on = (type(instance), on)
|
|
@@ -325,7 +327,7 @@ class Module(EventListener, InjectionProvider): # type: ignore[misc]
|
|
|
325
327
|
/,
|
|
326
328
|
scope_name: str,
|
|
327
329
|
*,
|
|
328
|
-
mode: Mode | ModeStr = Mode.get_default(),
|
|
330
|
+
mode: Mode | ModeStr = Mode.get_default(), # noqa: B008
|
|
329
331
|
) -> SlotKey[T]:
|
|
330
332
|
injectable = ScopedSlotInjectable(cls, scope_name)
|
|
331
333
|
broker = StaticInjectableBroker(injectable)
|
|
@@ -341,8 +343,8 @@ class Module(EventListener, InjectionProvider): # type: ignore[misc]
|
|
|
341
343
|
) -> Any:
|
|
342
344
|
def decorator(wp: Callable[P, T]) -> Callable[P, T]:
|
|
343
345
|
if isclass(wp):
|
|
344
|
-
wp.__init__ = self.inject(wp.__init__, threadsafe=threadsafe)
|
|
345
|
-
return wp
|
|
346
|
+
wp.__init__ = self.inject(wp.__init__, threadsafe=threadsafe) # type: ignore[method-assign]
|
|
347
|
+
return wp # type: ignore[return-value]
|
|
346
348
|
|
|
347
349
|
return self.make_injected_function(wp, threadsafe)
|
|
348
350
|
|
|
@@ -354,7 +356,7 @@ class Module(EventListener, InjectionProvider): # type: ignore[misc]
|
|
|
354
356
|
/,
|
|
355
357
|
threadsafe: bool | None = None,
|
|
356
358
|
) -> InjectMetadata[P, T]:
|
|
357
|
-
return InjectMetadata(wrapped, threadsafe).listen(self)
|
|
359
|
+
return InjectMetadata(wrapped, self.__lock, threadsafe).listen(self)
|
|
358
360
|
|
|
359
361
|
if TYPE_CHECKING: # pragma: no cover
|
|
360
362
|
|
|
@@ -401,7 +403,7 @@ class Module(EventListener, InjectionProvider): # type: ignore[misc]
|
|
|
401
403
|
*,
|
|
402
404
|
threadsafe: bool | None = None,
|
|
403
405
|
) -> T:
|
|
404
|
-
with
|
|
406
|
+
with lock_if_threadsafe(self.__lock, threadsafe):
|
|
405
407
|
injectable = self[cls]
|
|
406
408
|
return await injectable.aget_instance()
|
|
407
409
|
|
|
@@ -411,7 +413,7 @@ class Module(EventListener, InjectionProvider): # type: ignore[misc]
|
|
|
411
413
|
*,
|
|
412
414
|
threadsafe: bool | None = None,
|
|
413
415
|
) -> T:
|
|
414
|
-
with
|
|
416
|
+
with lock_if_threadsafe(self.__lock, threadsafe):
|
|
415
417
|
injectable = self[cls]
|
|
416
418
|
return injectable.get_instance()
|
|
417
419
|
|
|
@@ -583,7 +585,7 @@ class Module(EventListener, InjectionProvider): # type: ignore[misc]
|
|
|
583
585
|
self,
|
|
584
586
|
module: Module,
|
|
585
587
|
*,
|
|
586
|
-
priority: Priority | PriorityStr = Priority.get_default(),
|
|
588
|
+
priority: Priority | PriorityStr = Priority.get_default(), # noqa: B008
|
|
587
589
|
) -> Self:
|
|
588
590
|
if module is self:
|
|
589
591
|
raise ModuleError("Module can't be used by itself.")
|
|
@@ -602,9 +604,9 @@ class Module(EventListener, InjectionProvider): # type: ignore[misc]
|
|
|
602
604
|
return self
|
|
603
605
|
|
|
604
606
|
def stop_using(self, module: Module) -> Self:
|
|
605
|
-
|
|
607
|
+
if module in self.__modules:
|
|
608
|
+
event = ModuleRemoved(self, module)
|
|
606
609
|
|
|
607
|
-
with suppress(KeyError):
|
|
608
610
|
with self.dispatch(event):
|
|
609
611
|
self.__modules.pop(module)
|
|
610
612
|
module.remove_listener(self)
|
|
@@ -616,7 +618,7 @@ class Module(EventListener, InjectionProvider): # type: ignore[misc]
|
|
|
616
618
|
self,
|
|
617
619
|
module: Module,
|
|
618
620
|
*,
|
|
619
|
-
priority: Priority | PriorityStr = Priority.get_default(),
|
|
621
|
+
priority: Priority | PriorityStr = Priority.get_default(), # noqa: B008
|
|
620
622
|
unlock: bool = False,
|
|
621
623
|
) -> Iterator[Self]:
|
|
622
624
|
self.use(module, priority=priority)
|
|
@@ -660,7 +662,7 @@ class Module(EventListener, InjectionProvider): # type: ignore[misc]
|
|
|
660
662
|
self.__channel.remove_listener(listener)
|
|
661
663
|
return self
|
|
662
664
|
|
|
663
|
-
def on_event(self, event: Event, /) ->
|
|
665
|
+
def on_event(self, event: Event, /) -> AbstractContextManager[None]:
|
|
664
666
|
self_event = ModuleEventProxy(self, event)
|
|
665
667
|
return self.dispatch(self_event)
|
|
666
668
|
|
|
@@ -829,7 +831,7 @@ class InjectMetadata[**P, T](Caller[P, T], EventListener):
|
|
|
829
831
|
)
|
|
830
832
|
|
|
831
833
|
__dependencies: Dependencies
|
|
832
|
-
__lock:
|
|
834
|
+
__lock: AbstractContextManager[Any]
|
|
833
835
|
__owner: type | None
|
|
834
836
|
__signature: Signature
|
|
835
837
|
__tasks: deque[Callable[..., Any]]
|
|
@@ -838,11 +840,12 @@ class InjectMetadata[**P, T](Caller[P, T], EventListener):
|
|
|
838
840
|
def __init__(
|
|
839
841
|
self,
|
|
840
842
|
wrapped: Callable[P, T],
|
|
843
|
+
lock: RLock,
|
|
841
844
|
/,
|
|
842
845
|
threadsafe: bool | None = None,
|
|
843
846
|
) -> None:
|
|
844
847
|
self.__dependencies = Dependencies.empty()
|
|
845
|
-
self.__lock =
|
|
848
|
+
self.__lock = lock_if_threadsafe(lock, threadsafe)
|
|
846
849
|
self.__owner = None
|
|
847
850
|
self.__tasks = deque()
|
|
848
851
|
self.__wrapped = wrapped
|
|
@@ -904,8 +907,12 @@ class InjectMetadata[**P, T](Caller[P, T], EventListener):
|
|
|
904
907
|
self.__dependencies = Dependencies.resolve(self.signature, module, self.__owner)
|
|
905
908
|
return self
|
|
906
909
|
|
|
907
|
-
def task[**
|
|
908
|
-
|
|
910
|
+
def task[**TaskP, TaskR](
|
|
911
|
+
self,
|
|
912
|
+
wrapped: Callable[TaskP, TaskR] | None = None,
|
|
913
|
+
/,
|
|
914
|
+
) -> Any:
|
|
915
|
+
def decorator(wp: Callable[TaskP, TaskR]) -> Callable[TaskP, TaskR]:
|
|
909
916
|
self.__tasks.append(wp)
|
|
910
917
|
return wp
|
|
911
918
|
|
|
@@ -920,7 +927,7 @@ class InjectMetadata[**P, T](Caller[P, T], EventListener):
|
|
|
920
927
|
return self
|
|
921
928
|
|
|
922
929
|
@singledispatchmethod
|
|
923
|
-
def on_event(self, event: Event, /) ->
|
|
930
|
+
def on_event(self, event: Event, /) -> AbstractContextManager[None] | None:
|
|
924
931
|
return None
|
|
925
932
|
|
|
926
933
|
@on_event.register
|
|
@@ -941,7 +948,7 @@ class InjectMetadata[**P, T](Caller[P, T], EventListener):
|
|
|
941
948
|
arguments: dict[str, Any],
|
|
942
949
|
additional_arguments: dict[str, Any],
|
|
943
950
|
) -> Arguments:
|
|
944
|
-
bound = BoundArguments(self.signature, additional_arguments | arguments)
|
|
951
|
+
bound = BoundArguments(self.signature, additional_arguments | arguments)
|
|
945
952
|
return Arguments(bound.args, bound.kwargs)
|
|
946
953
|
|
|
947
954
|
def __run_tasks(self) -> None:
|
|
@@ -4,17 +4,23 @@ import itertools
|
|
|
4
4
|
from abc import ABC, abstractmethod
|
|
5
5
|
from collections import defaultdict
|
|
6
6
|
from collections.abc import AsyncIterator, Collection, Iterator, Mapping, MutableMapping
|
|
7
|
-
from contextlib import
|
|
7
|
+
from contextlib import (
|
|
8
|
+
AbstractAsyncContextManager,
|
|
9
|
+
AbstractContextManager,
|
|
10
|
+
AsyncExitStack,
|
|
11
|
+
ExitStack,
|
|
12
|
+
asynccontextmanager,
|
|
13
|
+
contextmanager,
|
|
14
|
+
)
|
|
8
15
|
from contextvars import ContextVar
|
|
9
16
|
from dataclasses import dataclass, field
|
|
10
17
|
from enum import StrEnum
|
|
11
18
|
from functools import partial
|
|
19
|
+
from threading import Lock
|
|
12
20
|
from types import EllipsisType, TracebackType
|
|
13
21
|
from typing import (
|
|
14
22
|
TYPE_CHECKING,
|
|
15
23
|
Any,
|
|
16
|
-
AsyncContextManager,
|
|
17
|
-
ContextManager,
|
|
18
24
|
Final,
|
|
19
25
|
Literal,
|
|
20
26
|
NoReturn,
|
|
@@ -24,7 +30,7 @@ from typing import (
|
|
|
24
30
|
runtime_checkable,
|
|
25
31
|
)
|
|
26
32
|
|
|
27
|
-
from injection._core.common.threading import
|
|
33
|
+
from injection._core.common.threading import lock_if_threadsafe
|
|
28
34
|
from injection._core.slots import SlotKey
|
|
29
35
|
from injection.exceptions import (
|
|
30
36
|
InjectionError,
|
|
@@ -33,6 +39,8 @@ from injection.exceptions import (
|
|
|
33
39
|
ScopeUndefinedError,
|
|
34
40
|
)
|
|
35
41
|
|
|
42
|
+
__thread_lock: Final[Lock] = Lock()
|
|
43
|
+
|
|
36
44
|
|
|
37
45
|
class ScopeKind(StrEnum):
|
|
38
46
|
CONTEXTUAL = "contextual"
|
|
@@ -56,7 +64,7 @@ class ScopeResolver(Protocol):
|
|
|
56
64
|
raise NotImplementedError
|
|
57
65
|
|
|
58
66
|
@abstractmethod
|
|
59
|
-
def bind(self, scope: Scope) ->
|
|
67
|
+
def bind(self, scope: Scope) -> AbstractContextManager[None]:
|
|
60
68
|
raise NotImplementedError
|
|
61
69
|
|
|
62
70
|
@abstractmethod
|
|
@@ -128,7 +136,7 @@ __scope_resolvers: Final[Mapping[str, Mapping[str, ScopeResolver]]] = {
|
|
|
128
136
|
async def adefine_scope(
|
|
129
137
|
name: str,
|
|
130
138
|
/,
|
|
131
|
-
kind: ScopeKind | ScopeKindStr = ScopeKind.get_default(),
|
|
139
|
+
kind: ScopeKind | ScopeKindStr = ScopeKind.get_default(), # noqa: B008
|
|
132
140
|
threadsafe: bool | None = None,
|
|
133
141
|
) -> AsyncIterator[ScopeFacade]:
|
|
134
142
|
async with AsyncScope() as scope:
|
|
@@ -140,12 +148,11 @@ async def adefine_scope(
|
|
|
140
148
|
def define_scope(
|
|
141
149
|
name: str,
|
|
142
150
|
/,
|
|
143
|
-
kind: ScopeKind | ScopeKindStr = ScopeKind.get_default(),
|
|
151
|
+
kind: ScopeKind | ScopeKindStr = ScopeKind.get_default(), # noqa: B008
|
|
144
152
|
threadsafe: bool | None = None,
|
|
145
153
|
) -> Iterator[ScopeFacade]:
|
|
146
|
-
with SyncScope() as scope:
|
|
147
|
-
|
|
148
|
-
yield facade
|
|
154
|
+
with SyncScope() as scope, _bind_scope(name, scope, kind, threadsafe) as facade:
|
|
155
|
+
yield facade
|
|
149
156
|
|
|
150
157
|
|
|
151
158
|
if TYPE_CHECKING: # pragma: no cover
|
|
@@ -193,7 +200,7 @@ def _bind_scope(
|
|
|
193
200
|
kind: ScopeKind | ScopeKindStr,
|
|
194
201
|
threadsafe: bool | None,
|
|
195
202
|
) -> Iterator[ScopeFacade]:
|
|
196
|
-
lock =
|
|
203
|
+
lock = lock_if_threadsafe(__thread_lock, threadsafe)
|
|
197
204
|
|
|
198
205
|
with lock:
|
|
199
206
|
if get_first_scope(name, default=None):
|
|
@@ -205,7 +212,7 @@ def _bind_scope(
|
|
|
205
212
|
stack.enter_context(__scope_resolvers[kind][name].bind(scope))
|
|
206
213
|
|
|
207
214
|
try:
|
|
208
|
-
yield _UserScope(scope,
|
|
215
|
+
yield _UserScope(scope, lock_if_threadsafe(threadsafe=threadsafe))
|
|
209
216
|
|
|
210
217
|
finally:
|
|
211
218
|
with lock:
|
|
@@ -234,11 +241,11 @@ class Scope(Protocol):
|
|
|
234
241
|
cache: MutableMapping[SlotKey[Any], Any]
|
|
235
242
|
|
|
236
243
|
@abstractmethod
|
|
237
|
-
async def aenter[T](self, context_manager:
|
|
244
|
+
async def aenter[T](self, context_manager: AbstractAsyncContextManager[T]) -> T:
|
|
238
245
|
raise NotImplementedError
|
|
239
246
|
|
|
240
247
|
@abstractmethod
|
|
241
|
-
def enter[T](self, context_manager:
|
|
248
|
+
def enter[T](self, context_manager: AbstractContextManager[T]) -> T:
|
|
242
249
|
raise NotImplementedError
|
|
243
250
|
|
|
244
251
|
|
|
@@ -270,10 +277,10 @@ class AsyncScope(BaseScope[AsyncExitStack]):
|
|
|
270
277
|
self.close()
|
|
271
278
|
return await self.delegate.__aexit__(exc_type, exc_value, traceback)
|
|
272
279
|
|
|
273
|
-
async def aenter[T](self, context_manager:
|
|
280
|
+
async def aenter[T](self, context_manager: AbstractAsyncContextManager[T]) -> T:
|
|
274
281
|
return await self.delegate.enter_async_context(context_manager)
|
|
275
282
|
|
|
276
|
-
def enter[T](self, context_manager:
|
|
283
|
+
def enter[T](self, context_manager: AbstractContextManager[T]) -> T:
|
|
277
284
|
return self.delegate.enter_context(context_manager)
|
|
278
285
|
|
|
279
286
|
|
|
@@ -296,10 +303,13 @@ class SyncScope(BaseScope[ExitStack]):
|
|
|
296
303
|
self.close()
|
|
297
304
|
return self.delegate.__exit__(exc_type, exc_value, traceback)
|
|
298
305
|
|
|
299
|
-
async def aenter[T](
|
|
306
|
+
async def aenter[T](
|
|
307
|
+
self,
|
|
308
|
+
context_manager: AbstractAsyncContextManager[T],
|
|
309
|
+
) -> NoReturn:
|
|
300
310
|
raise ScopeError("Synchronous scope doesn't support async context manager.")
|
|
301
311
|
|
|
302
|
-
def enter[T](self, context_manager:
|
|
312
|
+
def enter[T](self, context_manager: AbstractContextManager[T]) -> T:
|
|
303
313
|
return self.delegate.enter_context(context_manager)
|
|
304
314
|
|
|
305
315
|
|
|
@@ -319,7 +329,7 @@ class ScopeFacade(Protocol):
|
|
|
319
329
|
@dataclass(repr=False, frozen=True, slots=True)
|
|
320
330
|
class _UserScope(ScopeFacade):
|
|
321
331
|
scope: Scope
|
|
322
|
-
lock:
|
|
332
|
+
lock: AbstractContextManager[Any]
|
|
323
333
|
|
|
324
334
|
def set_slot[T](self, key: SlotKey[T], value: T) -> Self:
|
|
325
335
|
return self.slot_map({key: value})
|
|
@@ -194,18 +194,18 @@ class EntrypointBuilder[**P, T1, T2](_EntrypointDecorator[P, T1, T2]):
|
|
|
194
194
|
|
|
195
195
|
return decorator(wrapped) if wrapped else decorator
|
|
196
196
|
|
|
197
|
-
def async_to_sync[
|
|
198
|
-
self: EntrypointBuilder[P, T1, Awaitable[
|
|
199
|
-
run: Callable[[Coroutine[Any, Any,
|
|
197
|
+
def async_to_sync[T](
|
|
198
|
+
self: EntrypointBuilder[P, T1, Awaitable[T]],
|
|
199
|
+
run: Callable[[Coroutine[Any, Any, T]], T] = asyncio.run,
|
|
200
200
|
/,
|
|
201
|
-
) -> EntrypointBuilder[P, T1,
|
|
201
|
+
) -> EntrypointBuilder[P, T1, T]:
|
|
202
202
|
return self._add_rule(_AsyncToSyncRule(run)) # type: ignore[arg-type]
|
|
203
203
|
|
|
204
|
-
def decorate[
|
|
204
|
+
def decorate[T](
|
|
205
205
|
self,
|
|
206
|
-
decorator: Callable[[Callable[P, T2]], Callable[P,
|
|
206
|
+
decorator: Callable[[Callable[P, T2]], Callable[P, T]],
|
|
207
207
|
/,
|
|
208
|
-
) -> EntrypointBuilder[P, T1,
|
|
208
|
+
) -> EntrypointBuilder[P, T1, T]:
|
|
209
209
|
return self._add_rule(_DecorateRule(decorator))
|
|
210
210
|
|
|
211
211
|
def inject(self) -> Self:
|
|
@@ -224,10 +224,10 @@ class EntrypointBuilder[**P, T1, T2](_EntrypointDecorator[P, T1, T2]):
|
|
|
224
224
|
self._add_rule(_LoadProfileRule(self.profile_loader, name))
|
|
225
225
|
return self
|
|
226
226
|
|
|
227
|
-
def _add_rule[
|
|
227
|
+
def _add_rule[T](
|
|
228
228
|
self,
|
|
229
|
-
rule: Rule[P, T2,
|
|
230
|
-
) -> EntrypointBuilder[P, T1,
|
|
229
|
+
rule: Rule[P, T2, T],
|
|
230
|
+
) -> EntrypointBuilder[P, T1, T]:
|
|
231
231
|
self.__rules.append(rule)
|
|
232
232
|
return self # type: ignore[return-value]
|
|
233
233
|
|
|
@@ -10,12 +10,12 @@ dev = [
|
|
|
10
10
|
"ruff",
|
|
11
11
|
]
|
|
12
12
|
docs = [
|
|
13
|
-
"
|
|
14
|
-
"
|
|
13
|
+
"ghp-import",
|
|
14
|
+
"zensical",
|
|
15
15
|
]
|
|
16
16
|
test = [
|
|
17
17
|
"fastapi",
|
|
18
|
-
"
|
|
18
|
+
"httpx2",
|
|
19
19
|
"pydantic",
|
|
20
20
|
"pytest",
|
|
21
21
|
"pytest-asyncio",
|
|
@@ -24,7 +24,7 @@ test = [
|
|
|
24
24
|
|
|
25
25
|
[project]
|
|
26
26
|
name = "python-injection"
|
|
27
|
-
version = "0.26.
|
|
27
|
+
version = "0.26.4"
|
|
28
28
|
description = "Dead-simple dependency injection framework for Python."
|
|
29
29
|
license = "MIT"
|
|
30
30
|
license-files = ["LICENSE"]
|
|
@@ -44,6 +44,7 @@ classifiers = [
|
|
|
44
44
|
"Programming Language :: Python :: 3.12",
|
|
45
45
|
"Programming Language :: Python :: 3.13",
|
|
46
46
|
"Programming Language :: Python :: 3.14",
|
|
47
|
+
"Programming Language :: Python :: 3.15",
|
|
47
48
|
"Operating System :: OS Independent",
|
|
48
49
|
"Intended Audience :: Developers",
|
|
49
50
|
"Natural Language :: English",
|
|
@@ -1,13 +0,0 @@
|
|
|
1
|
-
from contextlib import nullcontext
|
|
2
|
-
from os import getenv
|
|
3
|
-
from threading import RLock
|
|
4
|
-
from typing import Any, ContextManager, Final
|
|
5
|
-
|
|
6
|
-
_PYTHON_INJECTION_THREADSAFE: Final[bool] = bool(
|
|
7
|
-
int(getenv("PYTHON_INJECTION_THREADSAFE", 0))
|
|
8
|
-
)
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
def get_lock(threadsafe: bool | None = None) -> ContextManager[Any]:
|
|
12
|
-
threadsafe = _PYTHON_INJECTION_THREADSAFE if threadsafe is None else threadsafe
|
|
13
|
-
return RLock() if threadsafe else nullcontext()
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|