python-injection 0.14.6__tar.gz → 0.14.6.post1__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.14.6 → python_injection-0.14.6.post1}/PKG-INFO +1 -1
- {python_injection-0.14.6 → python_injection-0.14.6.post1}/injection/_core/injectables.py +38 -34
- {python_injection-0.14.6 → python_injection-0.14.6.post1}/injection/_core/scope.py +78 -37
- {python_injection-0.14.6 → python_injection-0.14.6.post1}/pyproject.toml +1 -1
- {python_injection-0.14.6 → python_injection-0.14.6.post1}/.gitignore +0 -0
- {python_injection-0.14.6 → python_injection-0.14.6.post1}/README.md +0 -0
- {python_injection-0.14.6 → python_injection-0.14.6.post1}/injection/__init__.py +0 -0
- {python_injection-0.14.6 → python_injection-0.14.6.post1}/injection/__init__.pyi +0 -0
- {python_injection-0.14.6 → python_injection-0.14.6.post1}/injection/_core/__init__.py +0 -0
- {python_injection-0.14.6 → python_injection-0.14.6.post1}/injection/_core/common/__init__.py +0 -0
- {python_injection-0.14.6 → python_injection-0.14.6.post1}/injection/_core/common/asynchronous.py +0 -0
- {python_injection-0.14.6 → python_injection-0.14.6.post1}/injection/_core/common/event.py +0 -0
- {python_injection-0.14.6 → python_injection-0.14.6.post1}/injection/_core/common/invertible.py +0 -0
- {python_injection-0.14.6 → python_injection-0.14.6.post1}/injection/_core/common/key.py +0 -0
- {python_injection-0.14.6 → python_injection-0.14.6.post1}/injection/_core/common/lazy.py +0 -0
- {python_injection-0.14.6 → python_injection-0.14.6.post1}/injection/_core/common/type.py +0 -0
- {python_injection-0.14.6 → python_injection-0.14.6.post1}/injection/_core/descriptors.py +0 -0
- {python_injection-0.14.6 → python_injection-0.14.6.post1}/injection/_core/module.py +0 -0
- {python_injection-0.14.6 → python_injection-0.14.6.post1}/injection/_core/slots.py +0 -0
- {python_injection-0.14.6 → python_injection-0.14.6.post1}/injection/exceptions.py +0 -0
- {python_injection-0.14.6 → python_injection-0.14.6.post1}/injection/integrations/__init__.py +0 -0
- {python_injection-0.14.6 → python_injection-0.14.6.post1}/injection/integrations/fastapi.py +0 -0
- {python_injection-0.14.6 → python_injection-0.14.6.post1}/injection/py.typed +0 -0
- {python_injection-0.14.6 → python_injection-0.14.6.post1}/injection/testing/__init__.py +0 -0
- {python_injection-0.14.6 → python_injection-0.14.6.post1}/injection/testing/__init__.pyi +0 -0
- {python_injection-0.14.6 → python_injection-0.14.6.post1}/injection/utils.py +0 -0
@@ -13,7 +13,10 @@ from typing import (
|
|
13
13
|
runtime_checkable,
|
14
14
|
)
|
15
15
|
|
16
|
-
from injection._core.common.asynchronous import Caller
|
16
|
+
from injection._core.common.asynchronous import Caller
|
17
|
+
from injection._core.common.asynchronous import (
|
18
|
+
create_semaphore as _create_async_semaphore,
|
19
|
+
)
|
17
20
|
from injection._core.scope import Scope, get_active_scopes, get_scope
|
18
21
|
from injection.exceptions import InjectionError
|
19
22
|
|
@@ -39,12 +42,8 @@ class Injectable[T](Protocol):
|
|
39
42
|
|
40
43
|
|
41
44
|
@dataclass(repr=False, eq=False, frozen=True, slots=True)
|
42
|
-
class
|
43
|
-
factory: Caller[...,
|
44
|
-
|
45
|
-
|
46
|
-
class SimpleInjectable[T](BaseInjectable[T, T]):
|
47
|
-
__slots__ = ()
|
45
|
+
class SimpleInjectable[T](Injectable[T]):
|
46
|
+
factory: Caller[..., T]
|
48
47
|
|
49
48
|
async def aget_instance(self) -> T:
|
50
49
|
return await self.factory.acall()
|
@@ -53,13 +52,13 @@ class SimpleInjectable[T](BaseInjectable[T, T]):
|
|
53
52
|
return self.factory.call()
|
54
53
|
|
55
54
|
|
56
|
-
|
57
|
-
|
58
|
-
|
59
|
-
|
60
|
-
|
61
|
-
|
62
|
-
|
55
|
+
class CacheLogic[T]:
|
56
|
+
__slots__ = ("__semaphore",)
|
57
|
+
|
58
|
+
__semaphore: AsyncContextManager[Any]
|
59
|
+
|
60
|
+
def __init__(self) -> None:
|
61
|
+
self.__semaphore = _create_async_semaphore(1)
|
63
62
|
|
64
63
|
async def aget_or_create[K](
|
65
64
|
self,
|
@@ -90,32 +89,37 @@ class CachedInjectable[R, T](BaseInjectable[R, T], ABC):
|
|
90
89
|
return instance
|
91
90
|
|
92
91
|
|
93
|
-
|
94
|
-
|
92
|
+
@dataclass(repr=False, eq=False, frozen=True, slots=True)
|
93
|
+
class SingletonInjectable[T](Injectable[T]):
|
94
|
+
factory: Caller[..., T]
|
95
|
+
cache: MutableMapping[str, T] = field(default_factory=dict)
|
96
|
+
logic: CacheLogic[T] = field(default_factory=CacheLogic)
|
95
97
|
|
96
98
|
__key: ClassVar[str] = "$instance"
|
97
99
|
|
98
100
|
@property
|
99
101
|
def is_locked(self) -> bool:
|
100
|
-
return self.__key in self.
|
101
|
-
|
102
|
-
@property
|
103
|
-
def __cache(self) -> MutableMapping[str, Any]:
|
104
|
-
return self.__dict__
|
102
|
+
return self.__key in self.cache
|
105
103
|
|
106
104
|
async def aget_instance(self) -> T:
|
107
|
-
return await self.aget_or_create(
|
105
|
+
return await self.logic.aget_or_create(
|
106
|
+
self.cache,
|
107
|
+
self.__key,
|
108
|
+
self.factory.acall,
|
109
|
+
)
|
108
110
|
|
109
111
|
def get_instance(self) -> T:
|
110
|
-
return self.get_or_create(self.
|
112
|
+
return self.logic.get_or_create(self.cache, self.__key, self.factory.call)
|
111
113
|
|
112
114
|
def unlock(self) -> None:
|
113
|
-
self.
|
115
|
+
self.cache.pop(self.__key, None)
|
114
116
|
|
115
117
|
|
116
118
|
@dataclass(repr=False, eq=False, frozen=True, slots=True)
|
117
|
-
class ScopedInjectable[R, T](
|
119
|
+
class ScopedInjectable[R, T](Injectable[T], ABC):
|
120
|
+
factory: Caller[..., R]
|
118
121
|
scope_name: str
|
122
|
+
logic: CacheLogic[T] = field(default_factory=CacheLogic)
|
119
123
|
|
120
124
|
@property
|
121
125
|
def is_locked(self) -> bool:
|
@@ -130,26 +134,26 @@ class ScopedInjectable[R, T](CachedInjectable[R, T], ABC):
|
|
130
134
|
raise NotImplementedError
|
131
135
|
|
132
136
|
async def aget_instance(self) -> T:
|
133
|
-
scope = self.
|
137
|
+
scope = self.__get_scope()
|
134
138
|
factory = partial(self.abuild, scope)
|
135
|
-
return await self.aget_or_create(scope.cache, self, factory)
|
139
|
+
return await self.logic.aget_or_create(scope.cache, self, factory)
|
136
140
|
|
137
141
|
def get_instance(self) -> T:
|
138
|
-
scope = self.
|
142
|
+
scope = self.__get_scope()
|
139
143
|
factory = partial(self.build, scope)
|
140
|
-
return self.get_or_create(scope.cache, self, factory)
|
141
|
-
|
142
|
-
def get_scope(self) -> Scope:
|
143
|
-
return get_scope(self.scope_name)
|
144
|
+
return self.logic.get_or_create(scope.cache, self, factory)
|
144
145
|
|
145
146
|
def setdefault(self, instance: T) -> T:
|
146
|
-
scope = self.
|
147
|
-
return self.get_or_create(scope.cache, self, lambda: instance)
|
147
|
+
scope = self.__get_scope()
|
148
|
+
return self.logic.get_or_create(scope.cache, self, lambda: instance)
|
148
149
|
|
149
150
|
def unlock(self) -> None:
|
150
151
|
if self.is_locked:
|
151
152
|
raise RuntimeError(f"To unlock, close the `{self.scope_name}` scope.")
|
152
153
|
|
154
|
+
def __get_scope(self) -> Scope:
|
155
|
+
return get_scope(self.scope_name)
|
156
|
+
|
153
157
|
|
154
158
|
class AsyncCMScopedInjectable[T](ScopedInjectable[AsyncContextManager[T], T]):
|
155
159
|
__slots__ = ()
|
@@ -1,12 +1,13 @@
|
|
1
1
|
from __future__ import annotations
|
2
2
|
|
3
|
+
import itertools
|
3
4
|
from abc import ABC, abstractmethod
|
4
5
|
from collections import defaultdict
|
5
|
-
from collections.abc import AsyncIterator, Iterator, MutableMapping
|
6
|
+
from collections.abc import AsyncIterator, Iterator, Mapping, MutableMapping
|
6
7
|
from contextlib import AsyncExitStack, ExitStack, asynccontextmanager, contextmanager
|
7
8
|
from contextvars import ContextVar
|
8
9
|
from dataclasses import dataclass, field
|
9
|
-
from types import TracebackType
|
10
|
+
from types import EllipsisType, TracebackType
|
10
11
|
from typing import (
|
11
12
|
Any,
|
12
13
|
AsyncContextManager,
|
@@ -15,6 +16,7 @@ from typing import (
|
|
15
16
|
NoReturn,
|
16
17
|
Protocol,
|
17
18
|
Self,
|
19
|
+
overload,
|
18
20
|
runtime_checkable,
|
19
21
|
)
|
20
22
|
|
@@ -26,18 +28,32 @@ from injection.exceptions import (
|
|
26
28
|
)
|
27
29
|
|
28
30
|
|
29
|
-
@
|
30
|
-
class
|
31
|
-
|
31
|
+
@runtime_checkable
|
32
|
+
class ScopeState(Protocol):
|
33
|
+
__slots__ = ()
|
34
|
+
|
35
|
+
@property
|
36
|
+
@abstractmethod
|
37
|
+
def active_scopes(self) -> Iterator[Scope]:
|
38
|
+
raise NotImplementedError
|
39
|
+
|
40
|
+
@abstractmethod
|
41
|
+
def bind(self, scope: Scope) -> ContextManager[None]:
|
42
|
+
raise NotImplementedError
|
43
|
+
|
44
|
+
@abstractmethod
|
45
|
+
def get_scope(self) -> Scope | None:
|
46
|
+
raise NotImplementedError
|
47
|
+
|
48
|
+
|
49
|
+
@dataclass(repr=False, frozen=True, slots=True)
|
50
|
+
class _ContextualScopeState(ScopeState):
|
51
|
+
# Shouldn't be instantiated outside `__CONTEXTUAL_SCOPES`.
|
32
52
|
|
33
53
|
__context_var: ContextVar[Scope] = field(
|
34
54
|
default_factory=lambda: ContextVar(f"scope@{new_short_key()}"),
|
35
55
|
init=False,
|
36
56
|
)
|
37
|
-
__default: Scope | None = field(
|
38
|
-
default=None,
|
39
|
-
init=False,
|
40
|
-
)
|
41
57
|
__references: set[Scope] = field(
|
42
58
|
default_factory=set,
|
43
59
|
init=False,
|
@@ -45,13 +61,10 @@ class _ScopeState:
|
|
45
61
|
|
46
62
|
@property
|
47
63
|
def active_scopes(self) -> Iterator[Scope]:
|
48
|
-
|
49
|
-
|
50
|
-
if default := self.__default:
|
51
|
-
yield default
|
64
|
+
return iter(self.__references)
|
52
65
|
|
53
66
|
@contextmanager
|
54
|
-
def
|
67
|
+
def bind(self, scope: Scope) -> Iterator[None]:
|
55
68
|
self.__references.add(scope)
|
56
69
|
token = self.__context_var.set(scope)
|
57
70
|
|
@@ -61,26 +74,38 @@ class _ScopeState:
|
|
61
74
|
self.__context_var.reset(token)
|
62
75
|
self.__references.remove(scope)
|
63
76
|
|
64
|
-
|
65
|
-
|
66
|
-
|
67
|
-
|
68
|
-
|
69
|
-
|
70
|
-
|
77
|
+
def get_scope(self) -> Scope | None:
|
78
|
+
return self.__context_var.get(None)
|
79
|
+
|
80
|
+
|
81
|
+
@dataclass(repr=False, slots=True)
|
82
|
+
class _SharedScopeState(ScopeState):
|
83
|
+
__scope: Scope | None = field(default=None)
|
84
|
+
|
85
|
+
@property
|
86
|
+
def active_scopes(self) -> Iterator[Scope]:
|
87
|
+
if scope := self.__scope:
|
88
|
+
yield scope
|
71
89
|
|
72
|
-
|
90
|
+
@contextmanager
|
91
|
+
def bind(self, scope: Scope) -> Iterator[None]:
|
92
|
+
self.__scope = scope
|
73
93
|
|
74
94
|
try:
|
75
95
|
yield
|
76
96
|
finally:
|
77
|
-
self.
|
97
|
+
self.__scope = None
|
78
98
|
|
79
99
|
def get_scope(self) -> Scope | None:
|
80
|
-
return self.
|
100
|
+
return self.__scope
|
81
101
|
|
82
102
|
|
83
|
-
|
103
|
+
__CONTEXTUAL_SCOPES: Final[Mapping[str, ScopeState]] = defaultdict(
|
104
|
+
_ContextualScopeState,
|
105
|
+
)
|
106
|
+
__SHARED_SCOPES: Final[Mapping[str, ScopeState]] = defaultdict(
|
107
|
+
_SharedScopeState,
|
108
|
+
)
|
84
109
|
|
85
110
|
|
86
111
|
@asynccontextmanager
|
@@ -98,36 +123,52 @@ def define_scope(name: str, *, shared: bool = False) -> Iterator[None]:
|
|
98
123
|
|
99
124
|
|
100
125
|
def get_active_scopes(name: str) -> tuple[Scope, ...]:
|
101
|
-
|
126
|
+
active_scopes = (
|
127
|
+
state.active_scopes
|
128
|
+
for states in (__CONTEXTUAL_SCOPES, __SHARED_SCOPES)
|
129
|
+
if (state := states.get(name))
|
130
|
+
)
|
131
|
+
return tuple(itertools.chain.from_iterable(active_scopes))
|
132
|
+
|
102
133
|
|
103
|
-
|
104
|
-
|
134
|
+
@overload
|
135
|
+
def get_scope(name: str, default: EllipsisType = ...) -> Scope: ...
|
105
136
|
|
106
|
-
return tuple(state.active_scopes)
|
107
137
|
|
138
|
+
@overload
|
139
|
+
def get_scope[T](name: str, default: T) -> Scope | T: ...
|
108
140
|
|
109
|
-
def get_scope(name: str) -> Scope:
|
110
|
-
state = __SCOPES.get(name)
|
111
141
|
|
112
|
-
|
142
|
+
def get_scope(name, default=...): # type: ignore[no-untyped-def]
|
143
|
+
for states in (__CONTEXTUAL_SCOPES, __SHARED_SCOPES):
|
144
|
+
state = states.get(name)
|
145
|
+
if state and (scope := state.get_scope()):
|
146
|
+
return scope
|
147
|
+
|
148
|
+
if default is Ellipsis:
|
113
149
|
raise ScopeUndefinedError(
|
114
150
|
f"Scope `{name}` isn't defined in the current context."
|
115
151
|
)
|
116
152
|
|
117
|
-
return
|
153
|
+
return default
|
118
154
|
|
119
155
|
|
120
156
|
@contextmanager
|
121
157
|
def _bind_scope(name: str, scope: Scope, shared: bool) -> Iterator[None]:
|
122
|
-
|
158
|
+
if shared:
|
159
|
+
is_already_defined = bool(get_active_scopes(name))
|
160
|
+
state = __SHARED_SCOPES[name]
|
161
|
+
|
162
|
+
else:
|
163
|
+
is_already_defined = bool(get_scope(name, default=None))
|
164
|
+
state = __CONTEXTUAL_SCOPES[name]
|
123
165
|
|
124
|
-
if
|
166
|
+
if is_already_defined:
|
125
167
|
raise ScopeAlreadyDefinedError(
|
126
168
|
f"Scope `{name}` is already defined in the current context."
|
127
169
|
)
|
128
170
|
|
129
|
-
|
130
|
-
with strategy(scope):
|
171
|
+
with state.bind(scope):
|
131
172
|
yield
|
132
173
|
|
133
174
|
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
{python_injection-0.14.6 → python_injection-0.14.6.post1}/injection/_core/common/__init__.py
RENAMED
File without changes
|
{python_injection-0.14.6 → python_injection-0.14.6.post1}/injection/_core/common/asynchronous.py
RENAMED
File without changes
|
File without changes
|
{python_injection-0.14.6 → python_injection-0.14.6.post1}/injection/_core/common/invertible.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
|
{python_injection-0.14.6 → python_injection-0.14.6.post1}/injection/integrations/__init__.py
RENAMED
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|