python-injection 0.14.5__py3-none-any.whl → 0.14.6.post0__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.
@@ -1,7 +1,7 @@
1
1
  from abc import abstractmethod
2
2
  from collections.abc import Awaitable, Callable, Generator
3
3
  from dataclasses import dataclass
4
- from typing import Any, NoReturn, Protocol, runtime_checkable
4
+ from typing import Any, AsyncContextManager, NoReturn, Protocol, runtime_checkable
5
5
 
6
6
 
7
7
  @dataclass(repr=False, eq=False, frozen=True, slots=True)
@@ -45,3 +45,15 @@ class SyncCaller[**P, T](Caller[P, T]):
45
45
 
46
46
  def call(self, /, *args: P.args, **kwargs: P.kwargs) -> T:
47
47
  return self.callable(*args, **kwargs)
48
+
49
+
50
+ try:
51
+ import anyio
52
+
53
+ def create_semaphore(value: int) -> AsyncContextManager[Any]:
54
+ return anyio.Semaphore(value)
55
+ except ImportError: # pragma: no cover
56
+ import asyncio
57
+
58
+ def create_semaphore(value: int) -> AsyncContextManager[Any]:
59
+ return asyncio.Semaphore(value)
@@ -1,7 +1,8 @@
1
1
  from abc import ABC, abstractmethod
2
- from collections.abc import MutableMapping
2
+ from collections.abc import Awaitable, Callable, MutableMapping
3
3
  from contextlib import suppress
4
- from dataclasses import dataclass
4
+ from dataclasses import dataclass, field
5
+ from functools import partial
5
6
  from typing import (
6
7
  Any,
7
8
  AsyncContextManager,
@@ -13,6 +14,9 @@ from typing import (
13
14
  )
14
15
 
15
16
  from injection._core.common.asynchronous import Caller
17
+ from injection._core.common.asynchronous import (
18
+ create_semaphore as _create_async_semaphore,
19
+ )
16
20
  from injection._core.scope import Scope, get_active_scopes, get_scope
17
21
  from injection.exceptions import InjectionError
18
22
 
@@ -37,14 +41,10 @@ class Injectable[T](Protocol):
37
41
  raise NotImplementedError
38
42
 
39
43
 
40
- @dataclass(repr=False, frozen=True, slots=True)
41
- class BaseInjectable[T](Injectable[T], ABC):
44
+ @dataclass(repr=False, eq=False, frozen=True, slots=True)
45
+ class SimpleInjectable[T](Injectable[T]):
42
46
  factory: Caller[..., T]
43
47
 
44
-
45
- class SimpleInjectable[T](BaseInjectable[T]):
46
- __slots__ = ()
47
-
48
48
  async def aget_instance(self) -> T:
49
49
  return await self.factory.acall()
50
50
 
@@ -52,47 +52,74 @@ class SimpleInjectable[T](BaseInjectable[T]):
52
52
  return self.factory.call()
53
53
 
54
54
 
55
- class SingletonInjectable[T](BaseInjectable[T]):
56
- __slots__ = ("__dict__",)
55
+ class CacheLogic[T]:
56
+ __slots__ = ("__semaphore",)
57
57
 
58
- __key: ClassVar[str] = "$instance"
58
+ __semaphore: AsyncContextManager[Any]
59
59
 
60
- @property
61
- def is_locked(self) -> bool:
62
- return self.__key in self.__cache
60
+ def __init__(self) -> None:
61
+ self.__semaphore = _create_async_semaphore(1)
63
62
 
64
- @property
65
- def __cache(self) -> MutableMapping[str, Any]:
66
- return self.__dict__
63
+ async def aget_or_create[K](
64
+ self,
65
+ cache: MutableMapping[K, T],
66
+ key: K,
67
+ factory: Callable[..., Awaitable[T]],
68
+ ) -> T:
69
+ async with self.__semaphore:
70
+ with suppress(KeyError):
71
+ return cache[key]
67
72
 
68
- async def aget_instance(self) -> T:
69
- cache = self.__cache
73
+ instance = await factory()
74
+ cache[key] = instance
70
75
 
76
+ return instance
77
+
78
+ def get_or_create[K](
79
+ self,
80
+ cache: MutableMapping[K, T],
81
+ key: K,
82
+ factory: Callable[..., T],
83
+ ) -> T:
71
84
  with suppress(KeyError):
72
- return cache[self.__key]
85
+ return cache[key]
73
86
 
74
- instance = await self.factory.acall()
75
- cache[self.__key] = instance
87
+ instance = factory()
88
+ cache[key] = instance
76
89
  return instance
77
90
 
78
- def get_instance(self) -> T:
79
- cache = self.__cache
80
91
 
81
- with suppress(KeyError):
82
- return cache[self.__key]
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)
83
97
 
84
- instance = self.factory.call()
85
- cache[self.__key] = instance
86
- return instance
98
+ __key: ClassVar[str] = "$instance"
99
+
100
+ @property
101
+ def is_locked(self) -> bool:
102
+ return self.__key in self.cache
103
+
104
+ async def aget_instance(self) -> T:
105
+ return await self.logic.aget_or_create(
106
+ self.cache,
107
+ self.__key,
108
+ self.factory.acall,
109
+ )
110
+
111
+ def get_instance(self) -> T:
112
+ return self.logic.get_or_create(self.cache, self.__key, self.factory.call)
87
113
 
88
114
  def unlock(self) -> None:
89
- self.__cache.pop(self.__key, None)
115
+ self.cache.pop(self.__key, None)
90
116
 
91
117
 
92
118
  @dataclass(repr=False, eq=False, frozen=True, slots=True)
93
119
  class ScopedInjectable[R, T](Injectable[T], ABC):
94
120
  factory: Caller[..., R]
95
121
  scope_name: str
122
+ logic: CacheLogic[T] = field(default_factory=CacheLogic)
96
123
 
97
124
  @property
98
125
  def is_locked(self) -> bool:
@@ -107,35 +134,26 @@ class ScopedInjectable[R, T](Injectable[T], ABC):
107
134
  raise NotImplementedError
108
135
 
109
136
  async def aget_instance(self) -> T:
110
- scope = self.get_scope()
111
-
112
- with suppress(KeyError):
113
- return scope.cache[self]
114
-
115
- instance = await self.abuild(scope)
116
- self.set_instance(instance, scope)
117
- return instance
137
+ scope = self.__get_scope()
138
+ factory = partial(self.abuild, scope)
139
+ return await self.logic.aget_or_create(scope.cache, self, factory)
118
140
 
119
141
  def get_instance(self) -> T:
120
- scope = self.get_scope()
142
+ scope = self.__get_scope()
143
+ factory = partial(self.build, scope)
144
+ return self.logic.get_or_create(scope.cache, self, factory)
121
145
 
122
- with suppress(KeyError):
123
- return scope.cache[self]
124
-
125
- instance = self.build(scope)
126
- self.set_instance(instance, scope)
127
- return instance
128
-
129
- def get_scope(self) -> Scope:
130
- return get_scope(self.scope_name)
131
-
132
- def set_instance(self, instance: T, scope: Scope) -> None:
133
- scope.cache[self] = instance
146
+ def setdefault(self, instance: T) -> T:
147
+ scope = self.__get_scope()
148
+ return self.logic.get_or_create(scope.cache, self, lambda: instance)
134
149
 
135
150
  def unlock(self) -> None:
136
151
  if self.is_locked:
137
152
  raise RuntimeError(f"To unlock, close the `{self.scope_name}` scope.")
138
153
 
154
+ def __get_scope(self) -> Scope:
155
+ return get_scope(self.scope_name)
156
+
139
157
 
140
158
  class AsyncCMScopedInjectable[T](ScopedInjectable[AsyncContextManager[T], T]):
141
159
  __slots__ = ()
@@ -174,7 +192,7 @@ class SimpleScopedInjectable[T](ScopedInjectable[T, T]):
174
192
  scope.cache.pop(self, None)
175
193
 
176
194
 
177
- @dataclass(repr=False, frozen=True, slots=True)
195
+ @dataclass(repr=False, eq=False, frozen=True, slots=True)
178
196
  class ShouldBeInjectable[T](Injectable[T]):
179
197
  cls: type[T]
180
198
 
injection/_core/module.py CHANGED
@@ -300,7 +300,8 @@ class Locator(Broker):
300
300
  if injectable.is_locked:
301
301
  continue
302
302
 
303
- await injectable.aget_instance()
303
+ with suppress(SkipInjectable):
304
+ await injectable.aget_instance()
304
305
 
305
306
  def add_listener(self, listener: EventListener) -> Self:
306
307
  self.__channel.add_listener(listener)
injection/_core/scope.py CHANGED
@@ -98,13 +98,18 @@ def define_scope(name: str, *, shared: bool = False) -> Iterator[None]:
98
98
 
99
99
 
100
100
  def get_active_scopes(name: str) -> tuple[Scope, ...]:
101
- return tuple(__SCOPES[name].active_scopes)
101
+ state = __SCOPES.get(name)
102
+
103
+ if state is None:
104
+ return ()
105
+
106
+ return tuple(state.active_scopes)
102
107
 
103
108
 
104
109
  def get_scope(name: str) -> Scope:
105
- scope = __SCOPES[name].get_scope()
110
+ state = __SCOPES.get(name)
106
111
 
107
- if scope is None:
112
+ if state is None or (scope := state.get_scope()) is None:
108
113
  raise ScopeUndefinedError(
109
114
  f"Scope `{name}` isn't defined in the current context."
110
115
  )
injection/_core/slots.py CHANGED
@@ -3,6 +3,7 @@ from dataclasses import dataclass
3
3
  from typing import Any, Protocol, runtime_checkable
4
4
 
5
5
  from injection._core.injectables import ScopedInjectable
6
+ from injection.exceptions import InjectionError
6
7
 
7
8
 
8
9
  @runtime_checkable
@@ -19,6 +20,5 @@ class ScopedSlot[T](Slot[T]):
19
20
  injectable: ScopedInjectable[Any, T]
20
21
 
21
22
  def set(self, instance: T, /) -> None:
22
- injectable = self.injectable
23
- scope = injectable.get_scope()
24
- injectable.set_instance(instance, scope)
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.14.5
3
+ Version: 0.14.6.post0
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,12 +5,12 @@ 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=SKP9uKDO8U7ATw_pG-SjP8KIfPDYAC1nzZdONNv1blI,4682
9
- injection/_core/module.py,sha256=64M6bGtKUA3mmekBiaA8nwBSwBy_N9CohLt6YIU6qss,31767
10
- injection/_core/scope.py,sha256=pwOqVKOUM_ZteNScgp-hLqyOHg5Ew75jWlfWLLD2PU8,5437
11
- injection/_core/slots.py,sha256=O2OGLI6Zp5rVzort8ln5j4X54DJVjeIH5V5lj8lskX8,638
8
+ injection/_core/injectables.py,sha256=idNkQZZ29vd73G_lE-eS5C7zGeVe_ALNkUt8M6YjZrk,5519
9
+ injection/_core/module.py,sha256=DLw0pD3HDXfNhzbWM0yeCDKg-Mwg8JAzqZq43tFAXik,31814
10
+ injection/_core/scope.py,sha256=SnjfYnZ62BkxEUh3wXKHl7ivCHRrPFiTa5GMxC-8ACM,5533
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
- injection/_core/common/asynchronous.py,sha256=9bQDVRE6eqo9K0d5H9RzyFalf0WGoGP7cDrKDGbvZPI,1500
13
+ injection/_core/common/asynchronous.py,sha256=QeS2Lc4gEBFvTA_snOWfme5mTL4BFZWqZ8EzJwOdVos,1816
14
14
  injection/_core/common/event.py,sha256=XjzV8gxtGlGvzZs_ykvoC60qmdpd3RN08Eiqz5QUwes,1236
15
15
  injection/_core/common/invertible.py,sha256=YZlAdh6bNJgf1-74TRjwJTm8xrlgY95ZhOUGLSJ4XcY,482
16
16
  injection/_core/common/key.py,sha256=ghkZD-Y8Moz6SEPNgMh3xgsZUjDVq-XYAmXaCu5VuCA,80
@@ -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.5.dist-info/METADATA,sha256=jJ_lLiZ8gkIubQ9KXRRYiRwUcu18QXgQX5Ys04A2FU0,3199
24
- python_injection-0.14.5.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
25
- python_injection-0.14.5.dist-info/RECORD,,
23
+ python_injection-0.14.6.post0.dist-info/METADATA,sha256=sRxRmH0mUX84dvnm-e2Vw6dyZrSOqNqaoVdn5A82U_4,3205
24
+ python_injection-0.14.6.post0.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
25
+ python_injection-0.14.6.post0.dist-info/RECORD,,