python-injection 0.25.10__tar.gz → 0.25.12__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.25.10 → python_injection-0.25.12}/PKG-INFO +1 -1
- python_injection-0.25.12/injection/_core/common/threading.py +13 -0
- {python_injection-0.25.10 → python_injection-0.25.12}/injection/_core/injectables.py +27 -14
- {python_injection-0.25.10 → python_injection-0.25.12}/injection/_core/locator.py +5 -4
- {python_injection-0.25.10 → python_injection-0.25.12}/injection/_core/scope.py +28 -47
- {python_injection-0.25.10 → python_injection-0.25.12}/pyproject.toml +1 -1
- python_injection-0.25.10/injection/_core/common/threading.py +0 -11
- {python_injection-0.25.10 → python_injection-0.25.12}/.gitignore +0 -0
- {python_injection-0.25.10 → python_injection-0.25.12}/LICENSE +0 -0
- {python_injection-0.25.10 → python_injection-0.25.12}/docs/index.md +0 -0
- {python_injection-0.25.10 → python_injection-0.25.12}/injection/__init__.py +0 -0
- {python_injection-0.25.10 → python_injection-0.25.12}/injection/__init__.pyi +0 -0
- {python_injection-0.25.10 → python_injection-0.25.12}/injection/_core/__init__.py +0 -0
- {python_injection-0.25.10 → python_injection-0.25.12}/injection/_core/asfunction.py +0 -0
- {python_injection-0.25.10 → python_injection-0.25.12}/injection/_core/common/__init__.py +0 -0
- {python_injection-0.25.10 → python_injection-0.25.12}/injection/_core/common/asynchronous.py +0 -0
- {python_injection-0.25.10 → python_injection-0.25.12}/injection/_core/common/event.py +0 -0
- {python_injection-0.25.10 → python_injection-0.25.12}/injection/_core/common/invertible.py +0 -0
- {python_injection-0.25.10 → python_injection-0.25.12}/injection/_core/common/key.py +0 -0
- {python_injection-0.25.10 → python_injection-0.25.12}/injection/_core/common/lazy.py +0 -0
- {python_injection-0.25.10 → python_injection-0.25.12}/injection/_core/common/type.py +0 -0
- {python_injection-0.25.10 → python_injection-0.25.12}/injection/_core/descriptors.py +0 -0
- {python_injection-0.25.10 → python_injection-0.25.12}/injection/_core/module.py +0 -0
- {python_injection-0.25.10 → python_injection-0.25.12}/injection/_core/slots.py +0 -0
- {python_injection-0.25.10 → python_injection-0.25.12}/injection/entrypoint.py +0 -0
- {python_injection-0.25.10 → python_injection-0.25.12}/injection/exceptions.py +0 -0
- {python_injection-0.25.10 → python_injection-0.25.12}/injection/ext/__init__.py +0 -0
- {python_injection-0.25.10 → python_injection-0.25.12}/injection/ext/fastapi.py +0 -0
- {python_injection-0.25.10 → python_injection-0.25.12}/injection/ext/fastapi.pyi +0 -0
- {python_injection-0.25.10 → python_injection-0.25.12}/injection/loaders.py +0 -0
- {python_injection-0.25.10 → python_injection-0.25.12}/injection/py.typed +0 -0
- {python_injection-0.25.10 → python_injection-0.25.12}/injection/testing/__init__.py +0 -0
- {python_injection-0.25.10 → python_injection-0.25.12}/injection/testing/__init__.pyi +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: python-injection
|
|
3
|
-
Version: 0.25.
|
|
3
|
+
Version: 0.25.12
|
|
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
|
|
@@ -0,0 +1,13 @@
|
|
|
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()
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
from abc import ABC, abstractmethod
|
|
2
|
-
from collections.abc import Awaitable, Callable, MutableMapping
|
|
3
|
-
from contextlib import suppress
|
|
2
|
+
from collections.abc import Awaitable, Callable, Iterator, MutableMapping
|
|
3
|
+
from contextlib import contextmanager, suppress
|
|
4
4
|
from dataclasses import dataclass, field
|
|
5
5
|
from functools import partial
|
|
6
6
|
from typing import (
|
|
@@ -16,12 +16,7 @@ from typing import (
|
|
|
16
16
|
|
|
17
17
|
from injection._core.common.asynchronous import AsyncSemaphore, Caller
|
|
18
18
|
from injection._core.common.type import InputType
|
|
19
|
-
from injection._core.scope import
|
|
20
|
-
Scope,
|
|
21
|
-
get_scope,
|
|
22
|
-
in_scope_cache,
|
|
23
|
-
remove_scoped_values,
|
|
24
|
-
)
|
|
19
|
+
from injection._core.scope import Scope, get_scope, in_scope_cache
|
|
25
20
|
from injection._core.slots import SlotKey
|
|
26
21
|
from injection.exceptions import EmptySlotError, InjectionError
|
|
27
22
|
|
|
@@ -58,11 +53,13 @@ class TransientInjectable[T](Injectable[T]):
|
|
|
58
53
|
|
|
59
54
|
|
|
60
55
|
class CacheLogic[T]:
|
|
61
|
-
__slots__ = ("__semaphore"
|
|
56
|
+
__slots__ = ("__is_instantiating", "__semaphore")
|
|
62
57
|
|
|
58
|
+
__is_instantiating: bool
|
|
63
59
|
__semaphore: AsyncContextManager[Any]
|
|
64
60
|
|
|
65
61
|
def __init__(self) -> None:
|
|
62
|
+
self.__is_instantiating = False
|
|
66
63
|
self.__semaphore = AsyncSemaphore(1)
|
|
67
64
|
|
|
68
65
|
async def aget_or_create[K](
|
|
@@ -71,11 +68,14 @@ class CacheLogic[T]:
|
|
|
71
68
|
key: K,
|
|
72
69
|
factory: Callable[..., Awaitable[T]],
|
|
73
70
|
) -> T:
|
|
71
|
+
self.__fail_if_instantiating()
|
|
74
72
|
async with self.__semaphore:
|
|
75
73
|
with suppress(KeyError):
|
|
76
74
|
return cache[key]
|
|
77
75
|
|
|
78
|
-
|
|
76
|
+
with self.__instantiating():
|
|
77
|
+
instance = await factory()
|
|
78
|
+
|
|
79
79
|
cache[key] = instance
|
|
80
80
|
|
|
81
81
|
return instance
|
|
@@ -86,13 +86,29 @@ class CacheLogic[T]:
|
|
|
86
86
|
key: K,
|
|
87
87
|
factory: Callable[..., T],
|
|
88
88
|
) -> T:
|
|
89
|
+
self.__fail_if_instantiating()
|
|
89
90
|
with suppress(KeyError):
|
|
90
91
|
return cache[key]
|
|
91
92
|
|
|
92
|
-
|
|
93
|
+
with self.__instantiating():
|
|
94
|
+
instance = factory()
|
|
95
|
+
|
|
93
96
|
cache[key] = instance
|
|
94
97
|
return instance
|
|
95
98
|
|
|
99
|
+
def __fail_if_instantiating(self) -> None:
|
|
100
|
+
if self.__is_instantiating:
|
|
101
|
+
raise RecursionError("Recursive call detected during instantiation.")
|
|
102
|
+
|
|
103
|
+
@contextmanager
|
|
104
|
+
def __instantiating(self) -> Iterator[None]:
|
|
105
|
+
self.__is_instantiating = True
|
|
106
|
+
|
|
107
|
+
try:
|
|
108
|
+
yield
|
|
109
|
+
finally:
|
|
110
|
+
self.__is_instantiating = False
|
|
111
|
+
|
|
96
112
|
|
|
97
113
|
@dataclass(repr=False, eq=False, frozen=True, slots=True)
|
|
98
114
|
class SingletonInjectable[T](Injectable[T]):
|
|
@@ -204,9 +220,6 @@ class SimpleScopedInjectable[T](ScopedInjectable[T, T]):
|
|
|
204
220
|
def build(self, scope: Scope) -> T:
|
|
205
221
|
return self.factory.call()
|
|
206
222
|
|
|
207
|
-
def unlock(self) -> None:
|
|
208
|
-
remove_scoped_values(self.key, self.scope_name)
|
|
209
|
-
|
|
210
223
|
|
|
211
224
|
@dataclass(repr=False, eq=False, frozen=True, slots=True)
|
|
212
225
|
class ScopedSlotInjectable[T](Injectable[T]):
|
|
@@ -61,6 +61,7 @@ class InjectionProvider(ABC):
|
|
|
61
61
|
self,
|
|
62
62
|
wrapped: Callable[P, T],
|
|
63
63
|
/,
|
|
64
|
+
threadsafe: bool | None = ...,
|
|
64
65
|
) -> Callable[P, T]:
|
|
65
66
|
raise NotImplementedError
|
|
66
67
|
|
|
@@ -108,7 +109,7 @@ class DynamicInjectableBroker[T](InjectableBroker[T]):
|
|
|
108
109
|
|
|
109
110
|
injectable = _make_injectable(
|
|
110
111
|
self.factory,
|
|
111
|
-
provider.make_injected_function(self.recipe), # type: ignore[misc]
|
|
112
|
+
provider.make_injected_function(self.recipe, threadsafe=False), # type: ignore[misc]
|
|
112
113
|
)
|
|
113
114
|
self.injectables[provider] = injectable
|
|
114
115
|
return injectable
|
|
@@ -116,16 +117,16 @@ class DynamicInjectableBroker[T](InjectableBroker[T]):
|
|
|
116
117
|
|
|
117
118
|
@dataclass(repr=False, eq=False, frozen=True, slots=True)
|
|
118
119
|
class StaticInjectableBroker[T](InjectableBroker[T]):
|
|
119
|
-
|
|
120
|
+
injectable: Injectable[T]
|
|
120
121
|
|
|
121
122
|
def get(self, provider: InjectionProvider) -> Injectable[T] | None:
|
|
122
|
-
return self.
|
|
123
|
+
return self.injectable
|
|
123
124
|
|
|
124
125
|
def is_locked(self, provider: InjectionProvider) -> bool:
|
|
125
126
|
return False
|
|
126
127
|
|
|
127
128
|
def request(self, provider: InjectionProvider) -> Injectable[T]:
|
|
128
|
-
return self.
|
|
129
|
+
return self.injectable
|
|
129
130
|
|
|
130
131
|
@classmethod
|
|
131
132
|
def from_factory(
|
|
@@ -3,7 +3,7 @@ from __future__ import annotations
|
|
|
3
3
|
import itertools
|
|
4
4
|
from abc import ABC, abstractmethod
|
|
5
5
|
from collections import defaultdict
|
|
6
|
-
from collections.abc import AsyncIterator, Iterator, Mapping, MutableMapping
|
|
6
|
+
from collections.abc import AsyncIterator, Collection, Iterator, Mapping, MutableMapping
|
|
7
7
|
from contextlib import AsyncExitStack, ExitStack, asynccontextmanager, contextmanager
|
|
8
8
|
from contextvars import ContextVar
|
|
9
9
|
from dataclasses import dataclass, field
|
|
@@ -47,12 +47,12 @@ type ScopeKindStr = Literal["contextual", "shared"]
|
|
|
47
47
|
|
|
48
48
|
|
|
49
49
|
@runtime_checkable
|
|
50
|
-
class
|
|
50
|
+
class ScopeResolver(Protocol):
|
|
51
51
|
__slots__ = ()
|
|
52
52
|
|
|
53
53
|
@property
|
|
54
54
|
@abstractmethod
|
|
55
|
-
def active_scopes(self) ->
|
|
55
|
+
def active_scopes(self) -> Collection[Scope]:
|
|
56
56
|
raise NotImplementedError
|
|
57
57
|
|
|
58
58
|
@abstractmethod
|
|
@@ -65,8 +65,8 @@ class ScopeState(Protocol):
|
|
|
65
65
|
|
|
66
66
|
|
|
67
67
|
@dataclass(repr=False, frozen=True, slots=True)
|
|
68
|
-
class
|
|
69
|
-
# Shouldn't be instantiated outside `
|
|
68
|
+
class _ContextualScopeResolver(ScopeResolver):
|
|
69
|
+
# Shouldn't be instantiated outside `__scope_resolvers`.
|
|
70
70
|
|
|
71
71
|
__context_var: ContextVar[Scope] = field(
|
|
72
72
|
default_factory=lambda: ContextVar(f"scope@{new_short_key()}"),
|
|
@@ -78,8 +78,8 @@ class _ContextualScopeState(ScopeState):
|
|
|
78
78
|
)
|
|
79
79
|
|
|
80
80
|
@property
|
|
81
|
-
def active_scopes(self) ->
|
|
82
|
-
return
|
|
81
|
+
def active_scopes(self) -> Collection[Scope]:
|
|
82
|
+
return self.__references
|
|
83
83
|
|
|
84
84
|
@contextmanager
|
|
85
85
|
def bind(self, scope: Scope) -> Iterator[None]:
|
|
@@ -97,13 +97,13 @@ class _ContextualScopeState(ScopeState):
|
|
|
97
97
|
|
|
98
98
|
|
|
99
99
|
@dataclass(repr=False, slots=True)
|
|
100
|
-
class
|
|
100
|
+
class _SharedScopeResolver(ScopeResolver):
|
|
101
101
|
__scope: Scope | None = field(default=None)
|
|
102
102
|
|
|
103
103
|
@property
|
|
104
|
-
def active_scopes(self) ->
|
|
105
|
-
|
|
106
|
-
|
|
104
|
+
def active_scopes(self) -> Collection[Scope]:
|
|
105
|
+
scope = self.__scope
|
|
106
|
+
return () if scope is None else (scope,)
|
|
107
107
|
|
|
108
108
|
@contextmanager
|
|
109
109
|
def bind(self, scope: Scope) -> Iterator[None]:
|
|
@@ -118,12 +118,10 @@ class _SharedScopeState(ScopeState):
|
|
|
118
118
|
return self.__scope
|
|
119
119
|
|
|
120
120
|
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
)
|
|
124
|
-
|
|
125
|
-
_SharedScopeState,
|
|
126
|
-
)
|
|
121
|
+
__scope_resolvers: Final[Mapping[str, Mapping[str, ScopeResolver]]] = {
|
|
122
|
+
ScopeKind.CONTEXTUAL: defaultdict(_ContextualScopeResolver),
|
|
123
|
+
ScopeKind.SHARED: defaultdict(_SharedScopeResolver),
|
|
124
|
+
}
|
|
127
125
|
|
|
128
126
|
|
|
129
127
|
@asynccontextmanager
|
|
@@ -150,15 +148,6 @@ def define_scope(
|
|
|
150
148
|
yield facade
|
|
151
149
|
|
|
152
150
|
|
|
153
|
-
def get_active_scopes(name: str) -> tuple[Scope, ...]:
|
|
154
|
-
active_scopes = (
|
|
155
|
-
state.active_scopes
|
|
156
|
-
for states in (__CONTEXTUAL_SCOPES, __SHARED_SCOPES)
|
|
157
|
-
if (state := states.get(name))
|
|
158
|
-
)
|
|
159
|
-
return tuple(itertools.chain.from_iterable(active_scopes))
|
|
160
|
-
|
|
161
|
-
|
|
162
151
|
if TYPE_CHECKING: # pragma: no cover
|
|
163
152
|
|
|
164
153
|
@overload
|
|
@@ -169,9 +158,9 @@ if TYPE_CHECKING: # pragma: no cover
|
|
|
169
158
|
|
|
170
159
|
|
|
171
160
|
def get_scope[T](name: str, default: T | EllipsisType = ...) -> Scope | T:
|
|
172
|
-
for
|
|
173
|
-
|
|
174
|
-
if
|
|
161
|
+
for resolvers in __scope_resolvers.values():
|
|
162
|
+
resolver = resolvers.get(name)
|
|
163
|
+
if resolver and (scope := resolver.get_scope()):
|
|
175
164
|
return scope
|
|
176
165
|
|
|
177
166
|
if default is Ellipsis:
|
|
@@ -183,12 +172,16 @@ def get_scope[T](name: str, default: T | EllipsisType = ...) -> Scope | T:
|
|
|
183
172
|
|
|
184
173
|
|
|
185
174
|
def in_scope_cache(key: SlotKey[Any], scope_name: str) -> bool:
|
|
186
|
-
return any(key in scope.cache for scope in
|
|
175
|
+
return any(key in scope.cache for scope in iter_active_scopes(scope_name))
|
|
187
176
|
|
|
188
177
|
|
|
189
|
-
def
|
|
190
|
-
|
|
191
|
-
|
|
178
|
+
def iter_active_scopes(name: str) -> Iterator[Scope]:
|
|
179
|
+
active_scopes = (
|
|
180
|
+
resolver.active_scopes
|
|
181
|
+
for resolvers in __scope_resolvers.values()
|
|
182
|
+
if (resolver := resolvers.get(name))
|
|
183
|
+
)
|
|
184
|
+
return itertools.chain.from_iterable(active_scopes)
|
|
192
185
|
|
|
193
186
|
|
|
194
187
|
@contextmanager
|
|
@@ -201,25 +194,13 @@ def _bind_scope(
|
|
|
201
194
|
lock = get_lock(threadsafe)
|
|
202
195
|
|
|
203
196
|
with lock:
|
|
204
|
-
|
|
205
|
-
case ScopeKind.CONTEXTUAL:
|
|
206
|
-
is_already_defined = bool(get_scope(name, default=None))
|
|
207
|
-
states = __CONTEXTUAL_SCOPES
|
|
208
|
-
|
|
209
|
-
case ScopeKind.SHARED:
|
|
210
|
-
is_already_defined = bool(get_active_scopes(name))
|
|
211
|
-
states = __SHARED_SCOPES
|
|
212
|
-
|
|
213
|
-
case _:
|
|
214
|
-
raise NotImplementedError
|
|
215
|
-
|
|
216
|
-
if is_already_defined:
|
|
197
|
+
if get_scope(name, default=None):
|
|
217
198
|
raise ScopeAlreadyDefinedError(
|
|
218
199
|
f"Scope `{name}` is already defined in the current context."
|
|
219
200
|
)
|
|
220
201
|
|
|
221
202
|
stack = ExitStack()
|
|
222
|
-
stack.enter_context(
|
|
203
|
+
stack.enter_context(__scope_resolvers[kind][name].bind(scope))
|
|
223
204
|
|
|
224
205
|
try:
|
|
225
206
|
yield _UserScope(scope, lock)
|
|
@@ -1,11 +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(getenv("PYTHON_INJECTION_THREADSAFE"))
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
def get_lock(threadsafe: bool | None = None) -> ContextManager[Any]:
|
|
10
|
-
cond = _PYTHON_INJECTION_THREADSAFE if threadsafe is None else threadsafe
|
|
11
|
-
return RLock() if cond 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
|
{python_injection-0.25.10 → python_injection-0.25.12}/injection/_core/common/asynchronous.py
RENAMED
|
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
|