python-injection 0.15.0__py3-none-any.whl → 0.16.0__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 CHANGED
@@ -1,8 +1,9 @@
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
5
- from ._core.slots import Slot
4
+ from ._core.scope import ScopeFacade as Scope
5
+ from ._core.scope import ScopeKind, adefine_scope, define_scope
6
+ from ._core.slots import SlotKey
6
7
 
7
8
  __all__ = (
8
9
  "Injectable",
@@ -10,7 +11,9 @@ __all__ = (
10
11
  "Mode",
11
12
  "Module",
12
13
  "Priority",
13
- "Slot",
14
+ "Scope",
15
+ "ScopeKind",
16
+ "SlotKey",
14
17
  "adefine_scope",
15
18
  "afind_instance",
16
19
  "aget_instance",
injection/__init__.pyi CHANGED
@@ -1,5 +1,5 @@
1
1
  from abc import abstractmethod
2
- from collections.abc import AsyncIterator, Awaitable, Callable, Iterator
2
+ from collections.abc import AsyncIterator, Awaitable, Callable, Iterator, Mapping
3
3
  from contextlib import asynccontextmanager, contextmanager
4
4
  from enum import Enum
5
5
  from logging import Logger
@@ -11,6 +11,7 @@ from ._core.common.type import TypeInfo as _TypeInfo
11
11
  from ._core.module import InjectableFactory as _InjectableFactory
12
12
  from ._core.module import ModeStr, PriorityStr
13
13
  from ._core.module import Recipe as _Recipe
14
+ from ._core.scope import ScopeKindStr
14
15
 
15
16
  __MODULE: Final[Module] = ...
16
17
 
@@ -30,9 +31,17 @@ should_be_injectable = __MODULE.should_be_injectable
30
31
  singleton = __MODULE.singleton
31
32
 
32
33
  @asynccontextmanager
33
- def adefine_scope(name: str, *, shared: bool = ...) -> AsyncIterator[None]: ...
34
+ def adefine_scope(
35
+ name: str,
36
+ /,
37
+ kind: ScopeKind | ScopeKindStr = ...,
38
+ ) -> AsyncIterator[Scope]: ...
34
39
  @contextmanager
35
- def define_scope(name: str, *, shared: bool = ...) -> Iterator[None]: ...
40
+ def define_scope(
41
+ name: str,
42
+ /,
43
+ kind: ScopeKind | ScopeKindStr = ...,
44
+ ) -> Iterator[Scope]: ...
36
45
  def mod(name: str = ..., /) -> Module:
37
46
  """
38
47
  Short syntax for `Module.from_name`.
@@ -47,10 +56,19 @@ class Injectable[T](Protocol):
47
56
  @abstractmethod
48
57
  def get_instance(self) -> T: ...
49
58
 
59
+ @final
60
+ class ScopeKind(Enum):
61
+ CONTEXTUAL = ...
62
+ SHARED = ...
63
+
50
64
  @runtime_checkable
51
- class Slot[T](Protocol):
65
+ class Scope(Protocol):
52
66
  @abstractmethod
53
- def set(self, instance: T, /) -> None: ...
67
+ def set_slot[T](self, key: SlotKey[T], value: T) -> Self: ...
68
+ @abstractmethod
69
+ def slot_map(self, mapping: Mapping[SlotKey[Any], Any], /) -> Self: ...
70
+
71
+ class SlotKey[T]: ...
54
72
 
55
73
  class LazyInstance[T]:
56
74
  def __init__(
@@ -179,12 +197,12 @@ class Module:
179
197
 
180
198
  def reserve_scoped_slot[T](
181
199
  self,
182
- on: _TypeInfo[T],
200
+ cls: type[T],
183
201
  /,
184
202
  scope_name: str,
185
203
  *,
186
204
  mode: Mode | ModeStr = ...,
187
- ) -> Slot[T]: ...
205
+ ) -> SlotKey[T]: ...
188
206
  def make_injected_function[**P, T](
189
207
  self,
190
208
  wrapped: Callable[P, T],
@@ -17,8 +17,14 @@ from injection._core.common.asynchronous import Caller
17
17
  from injection._core.common.asynchronous import (
18
18
  create_semaphore as _create_async_semaphore,
19
19
  )
20
- from injection._core.scope import Scope, get_active_scopes, get_scope
21
- from injection.exceptions import InjectionError
20
+ from injection._core.scope import (
21
+ Scope,
22
+ get_scope,
23
+ in_scope_cache,
24
+ remove_scoped_values,
25
+ )
26
+ from injection._core.slots import SlotKey
27
+ from injection.exceptions import EmptySlotError, InjectionError
22
28
 
23
29
 
24
30
  @runtime_checkable
@@ -123,7 +129,7 @@ class ScopedInjectable[R, T](Injectable[T], ABC):
123
129
 
124
130
  @property
125
131
  def is_locked(self) -> bool:
126
- return any(self in scope.cache for scope in get_active_scopes(self.scope_name))
132
+ return in_scope_cache(self, self.scope_name)
127
133
 
128
134
  @abstractmethod
129
135
  async def abuild(self, scope: Scope) -> T:
@@ -143,10 +149,6 @@ class ScopedInjectable[R, T](Injectable[T], ABC):
143
149
  factory = partial(self.build, scope)
144
150
  return self.logic.get_or_create(scope.cache, self, factory)
145
151
 
146
- def setdefault(self, instance: T) -> T:
147
- scope = self.__get_scope()
148
- return self.logic.get_or_create(scope.cache, self, lambda: instance)
149
-
150
152
  def unlock(self) -> None:
151
153
  if self.is_locked:
152
154
  raise RuntimeError(f"To unlock, close the `{self.scope_name}` scope.")
@@ -188,8 +190,35 @@ class SimpleScopedInjectable[T](ScopedInjectable[T, T]):
188
190
  return self.factory.call()
189
191
 
190
192
  def unlock(self) -> None:
191
- for scope in get_active_scopes(self.scope_name):
192
- scope.cache.pop(self, None)
193
+ remove_scoped_values(self, self.scope_name)
194
+
195
+
196
+ @dataclass(repr=False, eq=False, frozen=True, slots=True)
197
+ class ScopedSlotInjectable[T](Injectable[T]):
198
+ cls: type[T]
199
+ scope_name: str
200
+ key: SlotKey[T] = field(default_factory=SlotKey)
201
+
202
+ @property
203
+ def is_locked(self) -> bool:
204
+ return in_scope_cache(self.key, self.scope_name)
205
+
206
+ async def aget_instance(self) -> T:
207
+ return self.get_instance()
208
+
209
+ def get_instance(self) -> T:
210
+ scope_name = self.scope_name
211
+ scope = get_scope(scope_name)
212
+
213
+ try:
214
+ return scope.cache[self.key]
215
+ except KeyError as exc:
216
+ raise EmptySlotError(
217
+ f"The slot for `{self.cls}` isn't set in the current `{scope_name}` scope."
218
+ ) from exc
219
+
220
+ def unlock(self) -> None:
221
+ remove_scoped_values(self.key, self.scope_name)
193
222
 
194
223
 
195
224
  @dataclass(repr=False, eq=False, frozen=True, slots=True)
injection/_core/module.py CHANGED
@@ -64,14 +64,14 @@ from injection._core.injectables import (
64
64
  CMScopedInjectable,
65
65
  Injectable,
66
66
  ScopedInjectable,
67
+ ScopedSlotInjectable,
67
68
  ShouldBeInjectable,
68
69
  SimpleInjectable,
69
70
  SimpleScopedInjectable,
70
71
  SingletonInjectable,
71
72
  )
72
- from injection._core.slots import ScopedSlot, Slot
73
+ from injection._core.slots import SlotKey
73
74
  from injection.exceptions import (
74
- EmptySlotError,
75
75
  ModuleError,
76
76
  ModuleLockError,
77
77
  ModuleNotUsedError,
@@ -492,11 +492,8 @@ class Module(Broker, EventListener):
492
492
 
493
493
  def should_be_injectable[T](self, wrapped: type[T] | None = None, /) -> Any:
494
494
  def decorator(wp: type[T]) -> type[T]:
495
- updater = Updater(
496
- classes=(wp,),
497
- injectable=ShouldBeInjectable(wp),
498
- mode=Mode.FALLBACK,
499
- )
495
+ injectable = ShouldBeInjectable(wp)
496
+ updater = Updater.with_basics(wp, injectable, Mode.FALLBACK)
500
497
  self.update(updater)
501
498
  return wp
502
499
 
@@ -543,21 +540,16 @@ class Module(Broker, EventListener):
543
540
 
544
541
  def reserve_scoped_slot[T](
545
542
  self,
546
- on: TypeInfo[T],
543
+ cls: type[T],
547
544
  /,
548
545
  scope_name: str,
549
546
  *,
550
547
  mode: Mode | ModeStr = Mode.get_default(),
551
- ) -> Slot[T]:
552
- def when_empty() -> T:
553
- raise EmptySlotError(
554
- f"The slot for `{on}` isn't set in the current `{scope_name}` scope."
555
- )
556
-
557
- injectable = SimpleScopedInjectable(SyncCaller(when_empty), scope_name)
558
- updater = Updater.with_basics(on, injectable, mode)
548
+ ) -> SlotKey[T]:
549
+ injectable = ScopedSlotInjectable(cls, scope_name)
550
+ updater = Updater.with_basics(cls, injectable, mode)
559
551
  self.update(updater)
560
- return ScopedSlot(injectable)
552
+ return injectable.key
561
553
 
562
554
  def inject[**P, T](
563
555
  self,
injection/_core/scope.py CHANGED
@@ -7,12 +7,14 @@ from collections.abc import AsyncIterator, 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
10
+ from enum import StrEnum
10
11
  from types import EllipsisType, TracebackType
11
12
  from typing import (
12
13
  Any,
13
14
  AsyncContextManager,
14
15
  ContextManager,
15
16
  Final,
17
+ Literal,
16
18
  NoReturn,
17
19
  Protocol,
18
20
  Self,
@@ -21,13 +23,27 @@ from typing import (
21
23
  )
22
24
 
23
25
  from injection._core.common.key import new_short_key
26
+ from injection._core.slots import SlotKey
24
27
  from injection.exceptions import (
28
+ InjectionError,
25
29
  ScopeAlreadyDefinedError,
26
30
  ScopeError,
27
31
  ScopeUndefinedError,
28
32
  )
29
33
 
30
34
 
35
+ class ScopeKind(StrEnum):
36
+ CONTEXTUAL = "contextual"
37
+ SHARED = "shared"
38
+
39
+ @classmethod
40
+ def get_default(cls) -> ScopeKind:
41
+ return cls.CONTEXTUAL
42
+
43
+
44
+ type ScopeKindStr = Literal["contextual", "shared"]
45
+
46
+
31
47
  @runtime_checkable
32
48
  class ScopeState(Protocol):
33
49
  __slots__ = ()
@@ -109,17 +125,25 @@ __SHARED_SCOPES: Final[Mapping[str, ScopeState]] = defaultdict(
109
125
 
110
126
 
111
127
  @asynccontextmanager
112
- async def adefine_scope(name: str, *, shared: bool = False) -> AsyncIterator[None]:
128
+ async def adefine_scope(
129
+ name: str,
130
+ /,
131
+ kind: ScopeKind | ScopeKindStr = ScopeKind.get_default(),
132
+ ) -> AsyncIterator[ScopeFacade]:
113
133
  async with AsyncScope() as scope:
114
- scope.enter(_bind_scope(name, scope, shared))
115
- yield
134
+ with _bind_scope(name, scope, kind) as facade:
135
+ yield facade
116
136
 
117
137
 
118
138
  @contextmanager
119
- def define_scope(name: str, *, shared: bool = False) -> Iterator[None]:
139
+ def define_scope(
140
+ name: str,
141
+ /,
142
+ kind: ScopeKind | ScopeKindStr = ScopeKind.get_default(),
143
+ ) -> Iterator[ScopeFacade]:
120
144
  with SyncScope() as scope:
121
- scope.enter(_bind_scope(name, scope, shared))
122
- yield
145
+ with _bind_scope(name, scope, kind) as facade:
146
+ yield facade
123
147
 
124
148
 
125
149
  def get_active_scopes(name: str) -> tuple[Scope, ...]:
@@ -153,15 +177,32 @@ def get_scope(name, default=...): # type: ignore[no-untyped-def]
153
177
  return default
154
178
 
155
179
 
156
- @contextmanager
157
- def _bind_scope(name: str, scope: Scope, shared: bool) -> Iterator[None]:
158
- if shared:
159
- is_already_defined = bool(get_active_scopes(name))
160
- states = __SHARED_SCOPES
180
+ def in_scope_cache(key: Any, scope_name: str) -> bool:
181
+ return any(key in scope.cache for scope in get_active_scopes(scope_name))
182
+
161
183
 
162
- else:
163
- is_already_defined = bool(get_scope(name, default=None))
164
- states = __CONTEXTUAL_SCOPES
184
+ def remove_scoped_values(key: Any, scope_name: str) -> None:
185
+ for scope in get_active_scopes(scope_name):
186
+ scope.cache.pop(key, None)
187
+
188
+
189
+ @contextmanager
190
+ def _bind_scope(
191
+ name: str,
192
+ scope: Scope,
193
+ kind: ScopeKind | ScopeKindStr,
194
+ ) -> Iterator[ScopeFacade]:
195
+ match ScopeKind(kind):
196
+ case ScopeKind.CONTEXTUAL:
197
+ is_already_defined = bool(get_scope(name, default=None))
198
+ states = __CONTEXTUAL_SCOPES
199
+
200
+ case ScopeKind.SHARED:
201
+ is_already_defined = bool(get_active_scopes(name))
202
+ states = __SHARED_SCOPES
203
+
204
+ case _:
205
+ raise NotImplementedError
165
206
 
166
207
  if is_already_defined:
167
208
  raise ScopeAlreadyDefinedError(
@@ -169,7 +210,7 @@ def _bind_scope(name: str, scope: Scope, shared: bool) -> Iterator[None]:
169
210
  )
170
211
 
171
212
  with states[name].bind(scope):
172
- yield
213
+ yield _UserScope(scope)
173
214
 
174
215
 
175
216
  @runtime_checkable
@@ -245,3 +286,34 @@ class SyncScope(BaseScope[ExitStack]):
245
286
 
246
287
  def enter[T](self, context_manager: ContextManager[T]) -> T:
247
288
  return self.delegate.enter_context(context_manager)
289
+
290
+
291
+ @runtime_checkable
292
+ class ScopeFacade(Protocol):
293
+ __slots__ = ()
294
+
295
+ @abstractmethod
296
+ def set_slot[T](self, key: SlotKey[T], value: T) -> Self:
297
+ raise NotImplementedError
298
+
299
+ @abstractmethod
300
+ def slot_map(self, mapping: Mapping[SlotKey[Any], Any], /) -> Self:
301
+ raise NotImplementedError
302
+
303
+
304
+ @dataclass(repr=False, frozen=True, slots=True)
305
+ class _UserScope(ScopeFacade):
306
+ scope: Scope
307
+
308
+ def set_slot[T](self, key: SlotKey[T], value: T) -> Self:
309
+ return self.slot_map({key: value})
310
+
311
+ def slot_map(self, mapping: Mapping[SlotKey[Any], Any], /) -> Self:
312
+ cache = self.scope.cache
313
+
314
+ for slot_key in mapping:
315
+ if slot_key in cache:
316
+ raise InjectionError("Slot already set.")
317
+
318
+ cache.update(mapping)
319
+ return self
injection/_core/slots.py CHANGED
@@ -1,24 +1,2 @@
1
- from abc import abstractmethod
2
- from dataclasses import dataclass
3
- from typing import Any, Protocol, runtime_checkable
4
-
5
- from injection._core.injectables import ScopedInjectable
6
- from injection.exceptions import InjectionError
7
-
8
-
9
- @runtime_checkable
10
- class Slot[T](Protocol):
1
+ class SlotKey[T]:
11
2
  __slots__ = ()
12
-
13
- @abstractmethod
14
- def set(self, instance: T, /) -> None:
15
- raise NotImplementedError
16
-
17
-
18
- @dataclass(repr=False, eq=False, frozen=True, slots=True)
19
- class ScopedSlot[T](Slot[T]):
20
- injectable: ScopedInjectable[Any, T]
21
-
22
- def set(self, instance: T, /) -> None:
23
- if self.injectable.setdefault(instance) is not instance:
24
- raise InjectionError("Slot already set.")
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: python-injection
3
- Version: 0.15.0
3
+ Version: 0.16.0
4
4
  Summary: Fast and easy dependency injection framework.
5
5
  Project-URL: Repository, https://github.com/100nm/python-injection
6
6
  Author: remimd
@@ -1,14 +1,14 @@
1
- injection/__init__.py,sha256=a-rBAlBTiH6TzZ-11NGK6diovfibyKmmnf99PZUog1A,1166
2
- injection/__init__.pyi,sha256=rOl1kA9BkIeXDr7OdBMifAg9xWwvme447yjDZooir18,10398
1
+ injection/__init__.py,sha256=7ZRUlO5EEPWO7IlbYHD-8DOX-cg4Np4nYq5fpw-U56o,1259
2
+ injection/__init__.pyi,sha256=O2W6ZWcwH58JnDmJNkJ6cdoaO0D2NE7PEVFgfJNTwB0,10717
3
3
  injection/exceptions.py,sha256=v57yMujiq6H_zwwn30A8UYEZX9R9k-bY8FnsdaimPM4,1025
4
4
  injection/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
5
5
  injection/utils.py,sha256=bLIVA_3N3mTEQ3kGV4YzrWEnokHxUGqWYNKPggOOnpg,4065
6
6
  injection/_core/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
7
7
  injection/_core/descriptors.py,sha256=7fSHlgAqmgR_Uta8KocBapOt1Xyj2dI7RY9ZdoStTzw,726
8
- injection/_core/injectables.py,sha256=idNkQZZ29vd73G_lE-eS5C7zGeVe_ALNkUt8M6YjZrk,5519
9
- injection/_core/module.py,sha256=KrEr66q1lazwz7F7jYscpr-ExX8aOwYHEho1IotXa54,31814
10
- injection/_core/scope.py,sha256=LGT_Sk0b2FXB_ScXfLhU2BWz_bEE0vdJOV5p-ijCDq4,6552
11
- injection/_core/slots.py,sha256=6LoG0XtaRnIGDSG8s-FfUIw_50gL0bl4X3Fo_n-hdak,680
8
+ injection/_core/injectables.py,sha256=yeW_AHCZSsxyiN1oT_E9MLc1DcYA37XlmQGAkUc4eR8,6189
9
+ injection/_core/module.py,sha256=GNDnYbD7oXVCS_CH_w7Io-rbWZxMConoOMNy1XJKotY,31575
10
+ injection/_core/scope.py,sha256=J6O0fgaz7IstYDx1hjRPaZUNIzIS_cHc29fp-qIrUA0,8291
11
+ injection/_core/slots.py,sha256=g9TG6CbqRzCsjg01iPyfRtTTUCJnnJOwcj9mJabH0dc,37
12
12
  injection/_core/common/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
13
13
  injection/_core/common/asynchronous.py,sha256=QeS2Lc4gEBFvTA_snOWfme5mTL4BFZWqZ8EzJwOdVos,1816
14
14
  injection/_core/common/event.py,sha256=XjzV8gxtGlGvzZs_ykvoC60qmdpd3RN08Eiqz5QUwes,1236
@@ -20,6 +20,6 @@ injection/integrations/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3
20
20
  injection/integrations/fastapi.py,sha256=YHSs85_3m6TUVtOwUcV157b3UZJQIw_aXWAg199a-YE,594
21
21
  injection/testing/__init__.py,sha256=SiImXDd0-DO1a8S5nbUQRtgDX8iaU_nHcp8DdqwtD2M,896
22
22
  injection/testing/__init__.pyi,sha256=iOii0i9F5n7znltGeGQYI2KXC_if9SAogLh1h03yx-0,540
23
- python_injection-0.15.0.dist-info/METADATA,sha256=LA4sTrPDIUAk-dwOH1zKj1Rjk-feJwq1r2eYHpLIJTw,3199
24
- python_injection-0.15.0.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
25
- python_injection-0.15.0.dist-info/RECORD,,
23
+ python_injection-0.16.0.dist-info/METADATA,sha256=g8st7-_QqRHhi8eS_vCYHWx8AppVvczjhAb3Qw0owRE,3199
24
+ python_injection-0.16.0.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
25
+ python_injection-0.16.0.dist-info/RECORD,,