python-injection 0.25.14__tar.gz → 0.26.0__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.
Files changed (31) hide show
  1. {python_injection-0.25.14 → python_injection-0.26.0}/PKG-INFO +1 -1
  2. {python_injection-0.25.14 → python_injection-0.26.0}/injection/__init__.pyi +2 -2
  3. {python_injection-0.25.14 → python_injection-0.26.0}/injection/_core/injectables.py +15 -34
  4. {python_injection-0.25.14 → python_injection-0.26.0}/injection/_core/module.py +2 -3
  5. {python_injection-0.25.14 → python_injection-0.26.0}/injection/_core/scope.py +13 -11
  6. {python_injection-0.25.14 → python_injection-0.26.0}/pyproject.toml +1 -1
  7. {python_injection-0.25.14 → python_injection-0.26.0}/.gitignore +0 -0
  8. {python_injection-0.25.14 → python_injection-0.26.0}/LICENSE +0 -0
  9. {python_injection-0.25.14 → python_injection-0.26.0}/docs/index.md +0 -0
  10. {python_injection-0.25.14 → python_injection-0.26.0}/injection/__init__.py +0 -0
  11. {python_injection-0.25.14 → python_injection-0.26.0}/injection/_core/__init__.py +0 -0
  12. {python_injection-0.25.14 → python_injection-0.26.0}/injection/_core/asfunction.py +0 -0
  13. {python_injection-0.25.14 → python_injection-0.26.0}/injection/_core/common/__init__.py +0 -0
  14. {python_injection-0.25.14 → python_injection-0.26.0}/injection/_core/common/asynchronous.py +0 -0
  15. {python_injection-0.25.14 → python_injection-0.26.0}/injection/_core/common/event.py +0 -0
  16. {python_injection-0.25.14 → python_injection-0.26.0}/injection/_core/common/invertible.py +0 -0
  17. {python_injection-0.25.14 → python_injection-0.26.0}/injection/_core/common/lazy.py +0 -0
  18. {python_injection-0.25.14 → python_injection-0.26.0}/injection/_core/common/threading.py +0 -0
  19. {python_injection-0.25.14 → python_injection-0.26.0}/injection/_core/common/type.py +0 -0
  20. {python_injection-0.25.14 → python_injection-0.26.0}/injection/_core/descriptors.py +0 -0
  21. {python_injection-0.25.14 → python_injection-0.26.0}/injection/_core/locator.py +0 -0
  22. {python_injection-0.25.14 → python_injection-0.26.0}/injection/_core/slots.py +0 -0
  23. {python_injection-0.25.14 → python_injection-0.26.0}/injection/entrypoint.py +0 -0
  24. {python_injection-0.25.14 → python_injection-0.26.0}/injection/exceptions.py +0 -0
  25. {python_injection-0.25.14 → python_injection-0.26.0}/injection/ext/__init__.py +0 -0
  26. {python_injection-0.25.14 → python_injection-0.26.0}/injection/ext/fastapi.py +0 -0
  27. {python_injection-0.25.14 → python_injection-0.26.0}/injection/ext/fastapi.pyi +0 -0
  28. {python_injection-0.25.14 → python_injection-0.26.0}/injection/loaders.py +0 -0
  29. {python_injection-0.25.14 → python_injection-0.26.0}/injection/py.typed +0 -0
  30. {python_injection-0.25.14 → python_injection-0.26.0}/injection/testing/__init__.py +0 -0
  31. {python_injection-0.25.14 → python_injection-0.26.0}/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.14
3
+ Version: 0.26.0
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
@@ -68,6 +68,7 @@ def mod(name: str = ..., /) -> Module:
68
68
  """
69
69
  Short syntax for `Module.from_name`.
70
70
  """
71
+
71
72
  @runtime_checkable
72
73
  class Injectable[T](Protocol):
73
74
  @property
@@ -234,9 +235,8 @@ class Module:
234
235
  ) -> _Decorator: ...
235
236
  def scoped(
236
237
  self,
237
- scope_name: str,
238
238
  /,
239
- *,
239
+ *scope_names: str,
240
240
  ignore_type_hint: bool = ...,
241
241
  inject: bool = ...,
242
242
  on: _TypeInfo[Any] = ...,
@@ -1,6 +1,6 @@
1
1
  from abc import ABC, abstractmethod
2
- from collections.abc import Awaitable, Callable, Iterator, MutableMapping
3
- from contextlib import contextmanager, suppress
2
+ from collections.abc import Awaitable, Callable, MutableMapping, Sequence
3
+ from contextlib import suppress
4
4
  from dataclasses import dataclass, field
5
5
  from functools import partial
6
6
  from typing import (
@@ -16,7 +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 Scope, get_scope, in_scope_cache
19
+ from injection._core.scope import Scope, get_first_scope, in_scope_cache
20
20
  from injection._core.slots import SlotKey
21
21
  from injection.exceptions import EmptySlotError, InjectionError
22
22
 
@@ -53,13 +53,11 @@ class TransientInjectable[T](Injectable[T]):
53
53
 
54
54
 
55
55
  class CacheLogic[T]:
56
- __slots__ = ("__is_instantiating", "__semaphore")
56
+ __slots__ = ("__semaphore",)
57
57
 
58
- __is_instantiating: bool
59
58
  __semaphore: AsyncContextManager[Any]
60
59
 
61
60
  def __init__(self) -> None:
62
- self.__is_instantiating = False
63
61
  self.__semaphore = AsyncSemaphore(1)
64
62
 
65
63
  async def aget_or_create[K](
@@ -68,14 +66,11 @@ class CacheLogic[T]:
68
66
  key: K,
69
67
  factory: Callable[..., Awaitable[T]],
70
68
  ) -> T:
71
- self.__fail_if_instantiating()
72
69
  async with self.__semaphore:
73
70
  with suppress(KeyError):
74
71
  return cache[key]
75
72
 
76
- with self.__instantiating():
77
- instance = await factory()
78
-
73
+ instance = await factory()
79
74
  cache[key] = instance
80
75
 
81
76
  return instance
@@ -86,29 +81,13 @@ class CacheLogic[T]:
86
81
  key: K,
87
82
  factory: Callable[..., T],
88
83
  ) -> T:
89
- self.__fail_if_instantiating()
90
84
  with suppress(KeyError):
91
85
  return cache[key]
92
86
 
93
- with self.__instantiating():
94
- instance = factory()
95
-
87
+ instance = factory()
96
88
  cache[key] = instance
97
89
  return instance
98
90
 
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
-
112
91
 
113
92
  @dataclass(repr=False, eq=False, frozen=True, slots=True)
114
93
  class SingletonInjectable[T](Injectable[T]):
@@ -150,13 +129,13 @@ class ConstantInjectable[T](Injectable[T]):
150
129
  @dataclass(repr=False, eq=False, frozen=True, slots=True)
151
130
  class ScopedInjectable[R, T](Injectable[T], ABC):
152
131
  factory: Caller[..., R]
153
- scope_name: str
132
+ scope_names: Sequence[str]
154
133
  key: SlotKey[T] = field(default_factory=SlotKey)
155
134
  logic: CacheLogic[T] = field(default_factory=CacheLogic)
156
135
 
157
136
  @property
158
137
  def is_locked(self) -> bool:
159
- return in_scope_cache(self.key, self.scope_name)
138
+ return in_scope_cache(self.key, *self.scope_names)
160
139
 
161
140
  @abstractmethod
162
141
  async def abuild(self, scope: Scope) -> T:
@@ -178,14 +157,16 @@ class ScopedInjectable[R, T](Injectable[T], ABC):
178
157
 
179
158
  def unlock(self) -> None:
180
159
  if self.is_locked:
181
- raise RuntimeError(f"To unlock, close the `{self.scope_name}` scope.")
160
+ raise RuntimeError(
161
+ f"To unlock, close all open scopes in [{', '.join(f'`{name}`' for name in self.scope_names)}]."
162
+ )
182
163
 
183
164
  def __get_scope(self) -> Scope:
184
- return get_scope(self.scope_name)
165
+ return get_first_scope(*self.scope_names)
185
166
 
186
167
  @classmethod
187
- def bind_scope_name(cls, name: str) -> Callable[[Caller[..., R]], Self]:
188
- return partial(cls, scope_name=name)
168
+ def bind_scope_names(cls, names: Sequence[str]) -> Callable[[Caller[..., R]], Self]:
169
+ return partial(cls, scope_names=names)
189
170
 
190
171
 
191
172
  class AsyncCMScopedInjectable[T](ScopedInjectable[AsyncContextManager[T], T]):
@@ -232,7 +213,7 @@ class ScopedSlotInjectable[T](Injectable[T]):
232
213
 
233
214
  def get_instance(self) -> T:
234
215
  scope_name = self.scope_name
235
- scope = get_scope(scope_name)
216
+ scope = get_first_scope(scope_name)
236
217
 
237
218
  try:
238
219
  return scope.cache[self.key]
@@ -250,9 +250,8 @@ class Module(EventListener, InjectionProvider): # type: ignore[misc]
250
250
 
251
251
  def scoped[**P, T](
252
252
  self,
253
- scope_name: str,
254
253
  /,
255
- *,
254
+ *scope_names: str,
256
255
  ignore_type_hint: bool = False,
257
256
  inject: bool = True,
258
257
  on: TypeInfo[T] = (),
@@ -284,7 +283,7 @@ class Module(EventListener, InjectionProvider): # type: ignore[misc]
284
283
 
285
284
  self.injectable(
286
285
  ctx.wrapper,
287
- cls=ctx.cls.bind_scope_name(scope_name),
286
+ cls=ctx.cls.bind_scope_names(scope_names),
288
287
  ignore_type_hint=True,
289
288
  inject=inject,
290
289
  on=(*ctx.hints, on),
@@ -151,34 +151,36 @@ def define_scope(
151
151
  if TYPE_CHECKING: # pragma: no cover
152
152
 
153
153
  @overload
154
- def get_scope(name: str, default: EllipsisType = ...) -> Scope: ...
154
+ def get_first_scope(*names: str, default: EllipsisType = ...) -> Scope: ...
155
155
 
156
156
  @overload
157
- def get_scope[T](name: str, default: T) -> Scope | T: ...
157
+ def get_first_scope[T](*names: str, default: T) -> Scope | T: ...
158
158
 
159
159
 
160
- def get_scope[T](name: str, default: T | EllipsisType = ...) -> Scope | T:
160
+ def get_first_scope[T](*names: str, default: T | EllipsisType = ...) -> Scope | T:
161
161
  for resolvers in __scope_resolvers.values():
162
- resolver = resolvers.get(name)
163
- if resolver and (scope := resolver.get_scope()):
164
- return scope
162
+ for name in names:
163
+ resolver = resolvers.get(name)
164
+ if resolver and (scope := resolver.get_scope()):
165
+ return scope
165
166
 
166
167
  if default is ...:
167
168
  raise ScopeUndefinedError(
168
- f"Scope `{name}` isn't defined in the current context."
169
+ f"No scope in [{', '.join(f'`{name}`' for name in names)}] is defined in the current context."
169
170
  )
170
171
 
171
172
  return default
172
173
 
173
174
 
174
- def in_scope_cache(key: SlotKey[Any], scope_name: str) -> bool:
175
- return any(key in scope.cache for scope in iter_active_scopes(scope_name))
175
+ def in_scope_cache(key: SlotKey[Any], *scope_names: str) -> bool:
176
+ return any(key in scope.cache for scope in iter_active_scopes(*scope_names))
176
177
 
177
178
 
178
- def iter_active_scopes(name: str) -> Iterator[Scope]:
179
+ def iter_active_scopes(*names: str) -> Iterator[Scope]:
179
180
  active_scopes = (
180
181
  resolver.active_scopes
181
182
  for resolvers in __scope_resolvers.values()
183
+ for name in names
182
184
  if (resolver := resolvers.get(name))
183
185
  )
184
186
  return itertools.chain.from_iterable(active_scopes)
@@ -194,7 +196,7 @@ def _bind_scope(
194
196
  lock = get_lock(threadsafe)
195
197
 
196
198
  with lock:
197
- if get_scope(name, None):
199
+ if get_first_scope(name, default=None):
198
200
  raise ScopeAlreadyDefinedError(
199
201
  f"Scope `{name}` is already defined in the current context."
200
202
  )
@@ -24,7 +24,7 @@ test = [
24
24
 
25
25
  [project]
26
26
  name = "python-injection"
27
- version = "0.25.14"
27
+ version = "0.26.0"
28
28
  description = "Dead-simple dependency injection framework for Python."
29
29
  license = "MIT"
30
30
  license-files = ["LICENSE"]