python-injection 0.14.6__py3-none-any.whl → 0.14.6.post1__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.
@@ -13,7 +13,10 @@ from typing import (
13
13
  runtime_checkable,
14
14
  )
15
15
 
16
- from injection._core.common.asynchronous import Caller, create_semaphore
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 BaseInjectable[R, T](Injectable[T], ABC):
43
- factory: Caller[..., R]
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
- @dataclass(repr=False, eq=False, frozen=True, slots=True)
57
- class CachedInjectable[R, T](BaseInjectable[R, T], ABC):
58
- __semaphore: AsyncContextManager[Any] = field(
59
- default_factory=partial(create_semaphore, 1),
60
- init=False,
61
- hash=False,
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
- class SingletonInjectable[T](CachedInjectable[T, T]):
94
- __slots__ = ("__dict__",)
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.__cache
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(self.__cache, self.__key, self.factory.acall)
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.__cache, self.__key, self.factory.call)
112
+ return self.logic.get_or_create(self.cache, self.__key, self.factory.call)
111
113
 
112
114
  def unlock(self) -> None:
113
- self.__cache.pop(self.__key, None)
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](CachedInjectable[R, T], ABC):
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.get_scope()
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.get_scope()
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.get_scope()
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__ = ()
injection/_core/scope.py CHANGED
@@ -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
- @dataclass(repr=False, slots=True)
30
- class _ScopeState:
31
- # Shouldn't be instantiated outside `__SCOPES`.
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
- yield from self.__references
49
-
50
- if default := self.__default:
51
- yield default
64
+ return iter(self.__references)
52
65
 
53
66
  @contextmanager
54
- def bind_contextual_scope(self, scope: Scope) -> Iterator[None]:
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
- @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
- )
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
- self.__default = scope
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.__default = None
97
+ self.__scope = None
78
98
 
79
99
  def get_scope(self) -> Scope | None:
80
- return self.__context_var.get(self.__default)
100
+ return self.__scope
81
101
 
82
102
 
83
- __SCOPES: Final[defaultdict[str, _ScopeState]] = defaultdict(_ScopeState)
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
- state = __SCOPES.get(name)
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
- if state is None:
104
- return ()
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
- if state is None or (scope := state.get_scope()) is None:
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 scope
153
+ return default
118
154
 
119
155
 
120
156
  @contextmanager
121
157
  def _bind_scope(name: str, scope: Scope, shared: bool) -> Iterator[None]:
122
- state = __SCOPES[name]
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 state.get_scope():
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
- strategy = state.bind_shared_scope if shared else state.bind_contextual_scope
130
- with strategy(scope):
171
+ with state.bind(scope):
131
172
  yield
132
173
 
133
174
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: python-injection
3
- Version: 0.14.6
3
+ Version: 0.14.6.post1
4
4
  Summary: Fast and easy dependency injection framework.
5
5
  Project-URL: Repository, https://github.com/100nm/python-injection
6
6
  Author: remimd
@@ -5,9 +5,9 @@ injection/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
5
5
  injection/utils.py,sha256=EuHMrix6gx2YnnUAn2_BPsDkvucGS5-pFhM3596oBK4,2796
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=w23JjZyNZBtWSTCtKHEje-YC6m5qf_LQ1By0qQDh6zY,5360
8
+ injection/_core/injectables.py,sha256=idNkQZZ29vd73G_lE-eS5C7zGeVe_ALNkUt8M6YjZrk,5519
9
9
  injection/_core/module.py,sha256=DLw0pD3HDXfNhzbWM0yeCDKg-Mwg8JAzqZq43tFAXik,31814
10
- injection/_core/scope.py,sha256=SnjfYnZ62BkxEUh3wXKHl7ivCHRrPFiTa5GMxC-8ACM,5533
10
+ injection/_core/scope.py,sha256=fZ6zvuP9RO_9wZvMcM13_elSoztMaYKen1MgTP3s8t4,6555
11
11
  injection/_core/slots.py,sha256=6LoG0XtaRnIGDSG8s-FfUIw_50gL0bl4X3Fo_n-hdak,680
12
12
  injection/_core/common/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
13
13
  injection/_core/common/asynchronous.py,sha256=QeS2Lc4gEBFvTA_snOWfme5mTL4BFZWqZ8EzJwOdVos,1816
@@ -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.14.6.dist-info/METADATA,sha256=uRWPRk__XYHBvfjfwrMCJMxYHdIX9EOlE8mh_qWcsDQ,3199
24
- python_injection-0.14.6.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
25
- python_injection-0.14.6.dist-info/RECORD,,
23
+ python_injection-0.14.6.post1.dist-info/METADATA,sha256=g-UJxN15zRFae7U8t5Fv2KU-naXLksipOjwefxrmBhs,3205
24
+ python_injection-0.14.6.post1.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
25
+ python_injection-0.14.6.post1.dist-info/RECORD,,