python-injection 0.24.0__tar.gz → 0.25.1__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 (33) hide show
  1. {python_injection-0.24.0 → python_injection-0.25.1}/PKG-INFO +1 -1
  2. {python_injection-0.24.0 → python_injection-0.25.1}/injection/__init__.pyi +3 -2
  3. {python_injection-0.24.0 → python_injection-0.25.1}/injection/_core/asfunction.py +1 -1
  4. {python_injection-0.24.0 → python_injection-0.25.1}/injection/_core/common/asynchronous.py +14 -0
  5. {python_injection-0.24.0 → python_injection-0.25.1}/injection/_core/common/type.py +1 -2
  6. {python_injection-0.24.0 → python_injection-0.25.1}/injection/_core/injectables.py +5 -4
  7. python_injection-0.25.1/injection/_core/locator.py +284 -0
  8. {python_injection-0.24.0 → python_injection-0.25.1}/injection/_core/module.py +104 -299
  9. {python_injection-0.24.0 → python_injection-0.25.1}/injection/_core/scope.py +4 -4
  10. python_injection-0.25.1/injection/testing/__init__.py +30 -0
  11. {python_injection-0.24.0 → python_injection-0.25.1}/injection/testing/__init__.pyi +2 -0
  12. {python_injection-0.24.0 → python_injection-0.25.1}/pyproject.toml +1 -1
  13. python_injection-0.24.0/injection/testing/__init__.py +0 -29
  14. {python_injection-0.24.0 → python_injection-0.25.1}/.gitignore +0 -0
  15. {python_injection-0.24.0 → python_injection-0.25.1}/LICENSE +0 -0
  16. {python_injection-0.24.0 → python_injection-0.25.1}/README.md +0 -0
  17. {python_injection-0.24.0 → python_injection-0.25.1}/injection/__init__.py +0 -0
  18. {python_injection-0.24.0 → python_injection-0.25.1}/injection/_core/__init__.py +0 -0
  19. {python_injection-0.24.0 → python_injection-0.25.1}/injection/_core/common/__init__.py +0 -0
  20. {python_injection-0.24.0 → python_injection-0.25.1}/injection/_core/common/event.py +0 -0
  21. {python_injection-0.24.0 → python_injection-0.25.1}/injection/_core/common/invertible.py +0 -0
  22. {python_injection-0.24.0 → python_injection-0.25.1}/injection/_core/common/key.py +0 -0
  23. {python_injection-0.24.0 → python_injection-0.25.1}/injection/_core/common/lazy.py +0 -0
  24. {python_injection-0.24.0 → python_injection-0.25.1}/injection/_core/common/threading.py +0 -0
  25. {python_injection-0.24.0 → python_injection-0.25.1}/injection/_core/descriptors.py +0 -0
  26. {python_injection-0.24.0 → python_injection-0.25.1}/injection/_core/slots.py +0 -0
  27. {python_injection-0.24.0 → python_injection-0.25.1}/injection/entrypoint.py +0 -0
  28. {python_injection-0.24.0 → python_injection-0.25.1}/injection/exceptions.py +0 -0
  29. {python_injection-0.24.0 → python_injection-0.25.1}/injection/ext/__init__.py +0 -0
  30. {python_injection-0.24.0 → python_injection-0.25.1}/injection/ext/fastapi.py +0 -0
  31. {python_injection-0.24.0 → python_injection-0.25.1}/injection/ext/fastapi.pyi +0 -0
  32. {python_injection-0.24.0 → python_injection-0.25.1}/injection/loaders.py +0 -0
  33. {python_injection-0.24.0 → python_injection-0.25.1}/injection/py.typed +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: python-injection
3
- Version: 0.24.0
3
+ Version: 0.25.1
4
4
  Summary: Fast and easy dependency injection framework.
5
5
  Project-URL: Repository, https://github.com/100nm/python-injection
6
6
  Author: remimd
@@ -9,8 +9,9 @@ from ._core.asfunction import AsFunctionWrappedType as _AsFunctionWrappedType
9
9
  from ._core.common.invertible import Invertible as _Invertible
10
10
  from ._core.common.type import InputType as _InputType
11
11
  from ._core.common.type import TypeInfo as _TypeInfo
12
- from ._core.module import InjectableFactory as _InjectableFactory
13
- from ._core.module import ModeStr, PriorityStr
12
+ from ._core.locator import InjectableFactory as _InjectableFactory
13
+ from ._core.locator import ModeStr
14
+ from ._core.module import PriorityStr
14
15
  from ._core.scope import ScopeKindStr
15
16
 
16
17
  type _Decorator[T] = Callable[[T], T]
@@ -23,7 +23,7 @@ def asfunction[**P, T](
23
23
  factory: Caller[..., Callable[P, T]] = module.make_injected_function(
24
24
  wp,
25
25
  threadsafe=threadsafe,
26
- ).__inject_metadata__
26
+ ).__injection_metadata__
27
27
 
28
28
  wrapper: Callable[P, T]
29
29
 
@@ -64,3 +64,17 @@ class SyncCaller[**P, T](Caller[P, T]):
64
64
 
65
65
  def call(self, /, *args: P.args, **kwargs: P.kwargs) -> T:
66
66
  return self.callable(*args, **kwargs)
67
+
68
+
69
+ @runtime_checkable
70
+ class HiddenCaller[**P, T](Protocol):
71
+ __slots__ = ()
72
+
73
+ @property
74
+ @abstractmethod
75
+ def __injection_hidden_caller__(self) -> Caller[P, T]:
76
+ raise NotImplementedError
77
+
78
+ @abstractmethod
79
+ def __call__(self, /, *args: P.args, **kwargs: P.kwargs) -> T:
80
+ raise NotImplementedError
@@ -19,8 +19,7 @@ from typing import (
19
19
  get_type_hints,
20
20
  )
21
21
 
22
- type TypeDef[T] = type[T] | TypeAliasType | GenericAlias
23
- type InputType[T] = TypeDef[T] | UnionType
22
+ type InputType[T] = type[T] | TypeAliasType | GenericAlias | UnionType
24
23
  type TypeInfo[T] = (
25
24
  InputType[T]
26
25
  | Callable[..., T]
@@ -122,11 +122,12 @@ class SingletonInjectable[T](Injectable[T]):
122
122
  class ScopedInjectable[R, T](Injectable[T], ABC):
123
123
  factory: Caller[..., R]
124
124
  scope_name: str
125
+ key: SlotKey[T] = field(default_factory=SlotKey)
125
126
  logic: CacheLogic[T] = field(default_factory=CacheLogic)
126
127
 
127
128
  @property
128
129
  def is_locked(self) -> bool:
129
- return in_scope_cache(self, self.scope_name)
130
+ return in_scope_cache(self.key, self.scope_name)
130
131
 
131
132
  @abstractmethod
132
133
  async def abuild(self, scope: Scope) -> T:
@@ -139,12 +140,12 @@ class ScopedInjectable[R, T](Injectable[T], ABC):
139
140
  async def aget_instance(self) -> T:
140
141
  scope = self.__get_scope()
141
142
  factory = partial(self.abuild, scope)
142
- return await self.logic.aget_or_create(scope.cache, self, factory)
143
+ return await self.logic.aget_or_create(scope.cache, self.key, factory)
143
144
 
144
145
  def get_instance(self) -> T:
145
146
  scope = self.__get_scope()
146
147
  factory = partial(self.build, scope)
147
- return self.logic.get_or_create(scope.cache, self, factory)
148
+ return self.logic.get_or_create(scope.cache, self.key, factory)
148
149
 
149
150
  def unlock(self) -> None:
150
151
  if self.is_locked:
@@ -187,7 +188,7 @@ class SimpleScopedInjectable[T](ScopedInjectable[T, T]):
187
188
  return self.factory.call()
188
189
 
189
190
  def unlock(self) -> None:
190
- remove_scoped_values(self, self.scope_name)
191
+ remove_scoped_values(self.key, self.scope_name)
191
192
 
192
193
 
193
194
  @dataclass(repr=False, eq=False, frozen=True, slots=True)
@@ -0,0 +1,284 @@
1
+ from __future__ import annotations
2
+
3
+ from abc import ABC, abstractmethod
4
+ from collections.abc import Awaitable, Callable, Collection, Iterable, Iterator
5
+ from contextlib import suppress
6
+ from dataclasses import dataclass, field
7
+ from enum import StrEnum
8
+ from inspect import iscoroutinefunction
9
+ from typing import (
10
+ Any,
11
+ ContextManager,
12
+ Literal,
13
+ NamedTuple,
14
+ Protocol,
15
+ Self,
16
+ runtime_checkable,
17
+ )
18
+ from weakref import WeakKeyDictionary
19
+
20
+ from injection._core.common.asynchronous import (
21
+ AsyncCaller,
22
+ Caller,
23
+ HiddenCaller,
24
+ SyncCaller,
25
+ )
26
+ from injection._core.common.event import Event, EventChannel, EventListener
27
+ from injection._core.common.type import InputType
28
+ from injection._core.injectables import Injectable
29
+ from injection.exceptions import NoInjectable, SkipInjectable
30
+
31
+
32
+ @dataclass(frozen=True, slots=True)
33
+ class LocatorEvent(Event, ABC):
34
+ locator: Locator
35
+
36
+
37
+ @dataclass(frozen=True, slots=True)
38
+ class LocatorDependenciesUpdated[T](LocatorEvent):
39
+ classes: Collection[InputType[T]]
40
+ mode: Mode
41
+
42
+ def __str__(self) -> str:
43
+ length = len(self.classes)
44
+ formatted_types = ", ".join(f"`{cls}`" for cls in self.classes)
45
+ return (
46
+ f"{length} dependenc{'ies' if length > 1 else 'y'} have been "
47
+ f"updated{f': {formatted_types}' if formatted_types else ''}."
48
+ )
49
+
50
+
51
+ type InjectableFactory[T] = Callable[[Caller[..., T]], Injectable[T]]
52
+
53
+ type Recipe[**P, T] = Callable[P, T] | Callable[P, Awaitable[T]]
54
+
55
+
56
+ class InjectionProvider(ABC):
57
+ __slots__ = ("__weakref__",)
58
+
59
+ @abstractmethod
60
+ def make_injected_function[**P, T](
61
+ self,
62
+ wrapped: Callable[P, T],
63
+ /,
64
+ ) -> Callable[P, T]:
65
+ raise NotImplementedError
66
+
67
+
68
+ @runtime_checkable
69
+ class InjectableBroker[T](Protocol):
70
+ __slots__ = ()
71
+
72
+ @abstractmethod
73
+ def get(self, provider: InjectionProvider) -> Injectable[T] | None:
74
+ raise NotImplementedError
75
+
76
+ @abstractmethod
77
+ def request(self, provider: InjectionProvider) -> Injectable[T]:
78
+ raise NotImplementedError
79
+
80
+
81
+ @dataclass(repr=False, eq=False, frozen=True, slots=True)
82
+ class DynamicInjectableBroker[T](InjectableBroker[T]):
83
+ injectable_factory: InjectableFactory[T]
84
+ recipe: Recipe[..., T]
85
+ cache: WeakKeyDictionary[InjectionProvider, Injectable[T]] = field(
86
+ default_factory=WeakKeyDictionary,
87
+ init=False,
88
+ )
89
+
90
+ def get(self, provider: InjectionProvider) -> Injectable[T] | None:
91
+ return self.cache.get(provider)
92
+
93
+ def request(self, provider: InjectionProvider) -> Injectable[T]:
94
+ with suppress(KeyError):
95
+ return self.cache[provider]
96
+
97
+ injectable = _make_injectable(
98
+ self.injectable_factory,
99
+ provider.make_injected_function(self.recipe), # type: ignore[misc]
100
+ )
101
+ self.cache[provider] = injectable
102
+ return injectable
103
+
104
+
105
+ @dataclass(repr=False, eq=False, frozen=True, slots=True)
106
+ class StaticInjectableBroker[T](InjectableBroker[T]):
107
+ value: Injectable[T]
108
+
109
+ def get(self, provider: InjectionProvider) -> Injectable[T] | None:
110
+ return self.value
111
+
112
+ def request(self, provider: InjectionProvider) -> Injectable[T]:
113
+ return self.value
114
+
115
+ @classmethod
116
+ def from_factory(
117
+ cls,
118
+ injectable_factory: InjectableFactory[T],
119
+ recipe: Recipe[..., T],
120
+ ) -> Self:
121
+ return cls(_make_injectable(injectable_factory, recipe))
122
+
123
+
124
+ class Mode(StrEnum):
125
+ FALLBACK = "fallback"
126
+ NORMAL = "normal"
127
+ OVERRIDE = "override"
128
+
129
+ @property
130
+ def rank(self) -> int:
131
+ return tuple(type(self)).index(self)
132
+
133
+ @classmethod
134
+ def get_default(cls) -> Mode:
135
+ return cls.NORMAL
136
+
137
+
138
+ type ModeStr = Literal["fallback", "normal", "override"]
139
+
140
+
141
+ class Record[T](NamedTuple):
142
+ broker: InjectableBroker[T]
143
+ mode: Mode
144
+
145
+
146
+ @dataclass(repr=False, eq=False, frozen=True, kw_only=True, slots=True)
147
+ class Updater[T]:
148
+ classes: Collection[InputType[T]]
149
+ broker: InjectableBroker[T]
150
+ mode: Mode
151
+
152
+ def make_record(self) -> Record[T]:
153
+ return Record(self.broker, self.mode)
154
+
155
+
156
+ @dataclass(repr=False, frozen=True, slots=True)
157
+ class Locator:
158
+ __records: dict[InputType[Any], Record[Any]] = field(
159
+ default_factory=dict,
160
+ init=False,
161
+ )
162
+ __channel: EventChannel = field(
163
+ default_factory=EventChannel,
164
+ init=False,
165
+ )
166
+
167
+ def __contains__(self, cls: InputType[Any], /) -> bool:
168
+ return cls in self.__records
169
+
170
+ @property
171
+ def __brokers(self) -> frozenset[InjectableBroker[Any]]:
172
+ return frozenset(record.broker for record in self.__records.values())
173
+
174
+ def is_locked(self, provider: InjectionProvider) -> bool:
175
+ return any(
176
+ injectable.is_locked for injectable in self.__iter_injectables(provider)
177
+ )
178
+
179
+ def request[T](
180
+ self,
181
+ cls: InputType[T],
182
+ /,
183
+ provider: InjectionProvider,
184
+ ) -> Injectable[T]:
185
+ try:
186
+ record = self.__records[cls]
187
+ except KeyError as exc:
188
+ raise NoInjectable(cls) from exc
189
+ else:
190
+ return record.broker.request(provider)
191
+
192
+ def update[T](self, updater: Updater[T]) -> Self:
193
+ record = updater.make_record()
194
+ records = dict(self.__prepare_for_updating(updater.classes, record))
195
+
196
+ if records:
197
+ event = LocatorDependenciesUpdated(self, records.keys(), record.mode)
198
+
199
+ with self.dispatch(event):
200
+ self.__records.update(records)
201
+
202
+ return self
203
+
204
+ def unlock(self, provider: InjectionProvider) -> None:
205
+ for injectable in self.__iter_injectables(provider):
206
+ injectable.unlock()
207
+
208
+ async def all_ready(self, provider: InjectionProvider) -> None:
209
+ for injectable in self.__iter_injectables(provider):
210
+ if injectable.is_locked:
211
+ continue
212
+
213
+ with suppress(SkipInjectable):
214
+ await injectable.aget_instance()
215
+
216
+ def add_listener(self, listener: EventListener) -> Self:
217
+ self.__channel.add_listener(listener)
218
+ return self
219
+
220
+ def dispatch(self, event: Event) -> ContextManager[None]:
221
+ return self.__channel.dispatch(event)
222
+
223
+ def __iter_injectables(
224
+ self,
225
+ provider: InjectionProvider,
226
+ ) -> Iterator[Injectable[Any]]:
227
+ for broker in self.__brokers:
228
+ injectable = broker.get(provider)
229
+
230
+ if injectable is None:
231
+ continue
232
+
233
+ yield injectable
234
+
235
+ def __prepare_for_updating[T](
236
+ self,
237
+ classes: Iterable[InputType[T]],
238
+ record: Record[T],
239
+ ) -> Iterator[tuple[InputType[T], Record[T]]]:
240
+ for cls in classes:
241
+ try:
242
+ existing = self.__records[cls]
243
+ except KeyError:
244
+ ...
245
+ else:
246
+ if not self.__keep_new_record(record, existing, cls):
247
+ continue
248
+
249
+ yield cls, record
250
+
251
+ @staticmethod
252
+ def __keep_new_record[T](
253
+ new: Record[T],
254
+ existing: Record[T],
255
+ cls: InputType[T],
256
+ ) -> bool:
257
+ new_mode, existing_mode = new.mode, existing.mode
258
+
259
+ if new_mode == Mode.OVERRIDE:
260
+ return True
261
+
262
+ elif new_mode == existing_mode:
263
+ raise RuntimeError(f"An injectable already exists for the class `{cls}`.")
264
+
265
+ return new_mode.rank > existing_mode.rank
266
+
267
+
268
+ def _extract_caller[**P, T](
269
+ function: Callable[P, T] | Callable[P, Awaitable[T]],
270
+ ) -> Caller[P, T]:
271
+ if iscoroutinefunction(function):
272
+ return AsyncCaller(function)
273
+
274
+ elif isinstance(function, HiddenCaller):
275
+ return function.__injection_hidden_caller__
276
+
277
+ return SyncCaller(function) # type: ignore[arg-type]
278
+
279
+
280
+ def _make_injectable[T](
281
+ injectable_factory: InjectableFactory[T],
282
+ recipe: Recipe[..., T],
283
+ ) -> Injectable[T]:
284
+ return injectable_factory(_extract_caller(recipe))
@@ -1,14 +1,13 @@
1
1
  from __future__ import annotations
2
2
 
3
3
  import itertools
4
- from abc import ABC, abstractmethod
4
+ from abc import ABC
5
5
  from collections import OrderedDict, deque
6
6
  from collections.abc import (
7
7
  AsyncGenerator,
8
8
  AsyncIterator,
9
9
  Awaitable,
10
10
  Callable,
11
- Collection,
12
11
  Generator,
13
12
  Iterable,
14
13
  Iterator,
@@ -37,19 +36,16 @@ from typing import (
37
36
  ContextManager,
38
37
  Literal,
39
38
  NamedTuple,
40
- Protocol,
41
39
  Self,
42
40
  overload,
43
- runtime_checkable,
44
41
  )
45
42
 
46
- from type_analyzer import MatchingTypesConfig, iter_matching_types
43
+ from type_analyzer import MatchingTypesConfig, iter_matching_types, matching_types
47
44
 
48
45
  from injection._core.common.asynchronous import (
49
- AsyncCaller,
50
46
  Caller,
47
+ HiddenCaller,
51
48
  SimpleAwaitable,
52
- SyncCaller,
53
49
  )
54
50
  from injection._core.common.event import Event, EventChannel, EventListener
55
51
  from injection._core.common.invertible import Invertible, SimpleInvertible
@@ -58,7 +54,6 @@ from injection._core.common.lazy import Lazy, alazy, lazy
58
54
  from injection._core.common.threading import get_lock
59
55
  from injection._core.common.type import (
60
56
  InputType,
61
- TypeDef,
62
57
  TypeInfo,
63
58
  get_return_types,
64
59
  get_yield_hint,
@@ -74,6 +69,18 @@ from injection._core.injectables import (
74
69
  SingletonInjectable,
75
70
  TransientInjectable,
76
71
  )
72
+ from injection._core.locator import (
73
+ DynamicInjectableBroker,
74
+ InjectableBroker,
75
+ InjectableFactory,
76
+ InjectionProvider,
77
+ Locator,
78
+ Mode,
79
+ ModeStr,
80
+ Recipe,
81
+ StaticInjectableBroker,
82
+ Updater,
83
+ )
77
84
  from injection._core.slots import SlotKey
78
85
  from injection.exceptions import (
79
86
  ModuleError,
@@ -88,25 +95,6 @@ Events
88
95
  """
89
96
 
90
97
 
91
- @dataclass(frozen=True, slots=True)
92
- class LocatorEvent(Event, ABC):
93
- locator: Locator
94
-
95
-
96
- @dataclass(frozen=True, slots=True)
97
- class LocatorDependenciesUpdated[T](LocatorEvent):
98
- classes: Collection[InputType[T]]
99
- mode: Mode
100
-
101
- def __str__(self) -> str:
102
- length = len(self.classes)
103
- formatted_types = ", ".join(f"`{cls}`" for cls in self.classes)
104
- return (
105
- f"{length} dependenc{'ies' if length > 1 else 'y'} have been "
106
- f"updated{f': {formatted_types}' if formatted_types else ''}."
107
- )
108
-
109
-
110
98
  @dataclass(frozen=True, slots=True)
111
99
  class ModuleEvent(Event, ABC):
112
100
  module: Module
@@ -120,15 +108,17 @@ class ModuleEventProxy(ModuleEvent):
120
108
  return f"`{self.module}` has propagated an event: {self.origin}"
121
109
 
122
110
  @property
123
- def history(self) -> Iterator[Event]:
124
- if isinstance(self.event, ModuleEventProxy):
125
- yield from self.event.history
126
-
127
- yield self.event
111
+ def origin(self) -> Event:
112
+ reversed_proxy_history = reversed(tuple(self.proxy_history))
113
+ return next(reversed_proxy_history, self).event
128
114
 
129
115
  @property
130
- def origin(self) -> Event:
131
- return next(self.history)
116
+ def proxy_history(self) -> Iterator[ModuleEventProxy]:
117
+ event = self.event
118
+
119
+ if isinstance(event, ModuleEventProxy):
120
+ yield event
121
+ yield from event.proxy_history
132
122
 
133
123
 
134
124
  @dataclass(frozen=True, slots=True)
@@ -160,211 +150,6 @@ class ModulePriorityUpdated(ModuleEvent):
160
150
  )
161
151
 
162
152
 
163
- @dataclass(frozen=True, slots=True)
164
- class UnlockCalled(Event):
165
- def __str__(self) -> str:
166
- return "An `unlock` method has been called."
167
-
168
-
169
- """
170
- Broker
171
- """
172
-
173
-
174
- @runtime_checkable
175
- class Broker(Protocol):
176
- __slots__ = ()
177
-
178
- @abstractmethod
179
- def __getitem__[T](self, cls: InputType[T], /) -> Injectable[T]:
180
- raise NotImplementedError
181
-
182
- @abstractmethod
183
- def __contains__(self, cls: InputType[Any], /) -> bool:
184
- raise NotImplementedError
185
-
186
- @property
187
- @abstractmethod
188
- def is_locked(self) -> bool:
189
- raise NotImplementedError
190
-
191
- @abstractmethod
192
- def unsafe_unlocking(self) -> None:
193
- raise NotImplementedError
194
-
195
- @abstractmethod
196
- async def all_ready(self) -> None:
197
- raise NotImplementedError
198
-
199
-
200
- """
201
- Locator
202
- """
203
-
204
-
205
- class Mode(StrEnum):
206
- FALLBACK = "fallback"
207
- NORMAL = "normal"
208
- OVERRIDE = "override"
209
-
210
- @property
211
- def rank(self) -> int:
212
- return tuple(type(self)).index(self)
213
-
214
- @classmethod
215
- def get_default(cls) -> Mode:
216
- return cls.NORMAL
217
-
218
-
219
- type ModeStr = Literal["fallback", "normal", "override"]
220
-
221
- type InjectableFactory[T] = Callable[[Caller[..., T]], Injectable[T]]
222
-
223
-
224
- class Record[T](NamedTuple):
225
- injectable: Injectable[T]
226
- mode: Mode
227
-
228
-
229
- @dataclass(repr=False, eq=False, frozen=True, kw_only=True, slots=True)
230
- class Updater[T]:
231
- classes: Iterable[InputType[T]]
232
- injectable: Injectable[T]
233
- mode: Mode
234
-
235
- def make_record(self) -> Record[T]:
236
- return Record(self.injectable, self.mode)
237
-
238
- @classmethod
239
- def with_basics(
240
- cls,
241
- on: TypeInfo[T],
242
- /,
243
- injectable: Injectable[T],
244
- mode: Mode | ModeStr,
245
- ) -> Self:
246
- return cls(
247
- classes=get_return_types(on),
248
- injectable=injectable,
249
- mode=Mode(mode),
250
- )
251
-
252
-
253
- @dataclass(repr=False, frozen=True, slots=True)
254
- class Locator(Broker):
255
- __records: dict[InputType[Any], Record[Any]] = field(
256
- default_factory=dict,
257
- init=False,
258
- )
259
- __channel: EventChannel = field(
260
- default_factory=EventChannel,
261
- init=False,
262
- )
263
-
264
- def __getitem__[T](self, cls: InputType[T], /) -> Injectable[T]:
265
- for key_type in self.__iter_key_types((cls,)):
266
- try:
267
- record = self.__records[key_type]
268
- except KeyError:
269
- continue
270
-
271
- return record.injectable
272
-
273
- raise NoInjectable(cls)
274
-
275
- def __contains__(self, cls: InputType[Any], /) -> bool:
276
- return any(
277
- key_type in self.__records for key_type in self.__iter_key_types((cls,))
278
- )
279
-
280
- @property
281
- def is_locked(self) -> bool:
282
- return any(injectable.is_locked for injectable in self.__injectables)
283
-
284
- @property
285
- def __injectables(self) -> frozenset[Injectable[Any]]:
286
- return frozenset(record.injectable for record in self.__records.values())
287
-
288
- def update[T](self, updater: Updater[T]) -> Self:
289
- record = updater.make_record()
290
- key_types = self.__build_key_types(updater.classes)
291
- records = dict(self.__prepare_for_updating(key_types, record))
292
-
293
- if records:
294
- event = LocatorDependenciesUpdated(self, records.keys(), record.mode)
295
-
296
- with self.dispatch(event):
297
- self.__records.update(records)
298
-
299
- return self
300
-
301
- def unsafe_unlocking(self) -> None:
302
- for injectable in self.__injectables:
303
- injectable.unlock()
304
-
305
- async def all_ready(self) -> None:
306
- for injectable in self.__injectables:
307
- if injectable.is_locked:
308
- continue
309
-
310
- with suppress(SkipInjectable):
311
- await injectable.aget_instance()
312
-
313
- def add_listener(self, listener: EventListener) -> Self:
314
- self.__channel.add_listener(listener)
315
- return self
316
-
317
- def dispatch(self, event: Event) -> ContextManager[None]:
318
- return self.__channel.dispatch(event)
319
-
320
- def __prepare_for_updating[T](
321
- self,
322
- classes: Iterable[InputType[T]],
323
- record: Record[T],
324
- ) -> Iterator[tuple[InputType[T], Record[T]]]:
325
- for cls in classes:
326
- try:
327
- existing = self.__records[cls]
328
- except KeyError:
329
- ...
330
- else:
331
- if not self.__keep_new_record(record, existing, cls):
332
- continue
333
-
334
- yield cls, record
335
-
336
- @staticmethod
337
- def __build_key_types[T](classes: Iterable[InputType[T]]) -> frozenset[TypeDef[T]]:
338
- config = MatchingTypesConfig(ignore_none=True)
339
- return frozenset(
340
- itertools.chain.from_iterable(
341
- iter_matching_types(cls, config) for cls in classes
342
- )
343
- )
344
-
345
- @staticmethod
346
- def __iter_key_types[T](classes: Iterable[InputType[T]]) -> Iterator[InputType[T]]:
347
- config = MatchingTypesConfig(with_origin=True, with_type_alias_value=True)
348
- for cls in classes:
349
- yield from iter_matching_types(cls, config)
350
-
351
- @staticmethod
352
- def __keep_new_record[T](
353
- new: Record[T],
354
- existing: Record[T],
355
- cls: InputType[T],
356
- ) -> bool:
357
- new_mode, existing_mode = new.mode, existing.mode
358
-
359
- if new_mode == Mode.OVERRIDE:
360
- return True
361
-
362
- elif new_mode == existing_mode:
363
- raise RuntimeError(f"An injectable already exists for the class `{cls}`.")
364
-
365
- return new_mode.rank > existing_mode.rank
366
-
367
-
368
153
  """
369
154
  Module
370
155
  """
@@ -387,11 +172,10 @@ type ContextManagerLikeRecipe[**P, T] = (
387
172
  type GeneratorRecipe[**P, T] = (
388
173
  Callable[P, Generator[T, Any, Any]] | Callable[P, AsyncGenerator[T, Any]]
389
174
  )
390
- type Recipe[**P, T] = Callable[P, T] | Callable[P, Awaitable[T]]
391
175
 
392
176
 
393
177
  @dataclass(eq=False, frozen=True, slots=True)
394
- class Module(Broker, EventListener):
178
+ class Module(EventListener, InjectionProvider): # type: ignore[misc]
395
179
  name: str = field(default_factory=lambda: f"anonymous@{new_short_key()}")
396
180
  __channel: EventChannel = field(
397
181
  default_factory=EventChannel,
@@ -420,23 +204,26 @@ class Module(Broker, EventListener):
420
204
  self.__locator.add_listener(self)
421
205
 
422
206
  def __getitem__[T](self, cls: InputType[T], /) -> Injectable[T]:
423
- for broker in self.__brokers:
424
- with suppress(KeyError):
425
- return broker[cls]
207
+ key_types = self.__matching_key_types(cls)
208
+
209
+ for locator in self._iter_locators():
210
+ for key_type in key_types:
211
+ with suppress(KeyError):
212
+ return locator.request(key_type, self)
426
213
 
427
214
  raise NoInjectable(cls)
428
215
 
429
216
  def __contains__(self, cls: InputType[Any], /) -> bool:
430
- return any(cls in broker for broker in self.__brokers)
217
+ key_types = self.__matching_key_types(cls)
218
+ return any(
219
+ key_type in locator
220
+ for locator in self._iter_locators()
221
+ for key_type in key_types
222
+ )
431
223
 
432
224
  @property
433
225
  def is_locked(self) -> bool:
434
- return any(broker.is_locked for broker in self.__brokers)
435
-
436
- @property
437
- def __brokers(self) -> Iterator[Broker]:
438
- yield from self.__modules
439
- yield self.__locator
226
+ return any(locator.is_locked(self) for locator in self._iter_locators())
440
227
 
441
228
  def injectable[**P, T](
442
229
  self,
@@ -450,11 +237,13 @@ class Module(Broker, EventListener):
450
237
  mode: Mode | ModeStr = Mode.get_default(),
451
238
  ) -> Any:
452
239
  def decorator(wp: Recipe[P, T]) -> Recipe[P, T]:
453
- factory = extract_caller(self.make_injected_function(wp) if inject else wp)
454
- injectable = cls(factory) # type: ignore[arg-type]
455
240
  hints = on if ignore_type_hint else (wp, on)
456
- updater = Updater.with_basics(hints, injectable, mode)
457
- self.update(updater)
241
+ broker = (
242
+ DynamicInjectableBroker(cls, wp)
243
+ if inject
244
+ else StaticInjectableBroker.from_factory(cls, wp)
245
+ )
246
+ self.update_from(hints, broker, mode)
458
247
  return wp
459
248
 
460
249
  return decorator(wrapped) if wrapped else decorator
@@ -505,8 +294,8 @@ class Module(Broker, EventListener):
505
294
  def should_be_injectable[T](self, wrapped: type[T] | None = None, /) -> Any:
506
295
  def decorator(wp: type[T]) -> type[T]:
507
296
  injectable = ShouldBeInjectable(wp)
508
- updater = Updater.with_basics(wp, injectable, Mode.FALLBACK)
509
- self.update(updater)
297
+ broker = StaticInjectableBroker(injectable)
298
+ self.update_from(wp, broker, Mode.FALLBACK)
510
299
  return wp
511
300
 
512
301
  return decorator(wrapped) if wrapped else decorator
@@ -564,8 +353,8 @@ class Module(Broker, EventListener):
564
353
  mode: Mode | ModeStr = Mode.get_default(),
565
354
  ) -> SlotKey[T]:
566
355
  injectable = ScopedSlotInjectable(cls, scope_name)
567
- updater = Updater.with_basics(cls, injectable, mode)
568
- self.update(updater)
356
+ broker = StaticInjectableBroker(injectable)
357
+ self.update_from(cls, broker, mode)
569
358
  return injectable.key
570
359
 
571
360
  def inject[**P, T](
@@ -630,7 +419,7 @@ class Module(Broker, EventListener):
630
419
  wrapped,
631
420
  threadsafe,
632
421
  )
633
- return factory.__inject_metadata__.acall
422
+ return factory.__injection_metadata__.acall
634
423
 
635
424
  async def afind_instance[T](
636
425
  self,
@@ -747,7 +536,7 @@ class Module(Broker, EventListener):
747
536
  lambda instance=default: instance,
748
537
  threadsafe=threadsafe,
749
538
  )
750
- metadata = function.__inject_metadata__.set_owner(cls)
539
+ metadata = function.__injection_metadata__.set_owner(cls)
751
540
  return SimpleAwaitable(metadata.acall)
752
541
 
753
542
  if TYPE_CHECKING: # pragma: no cover
@@ -781,13 +570,28 @@ class Module(Broker, EventListener):
781
570
  lambda instance=default: instance,
782
571
  threadsafe=threadsafe,
783
572
  )
784
- metadata = function.__inject_metadata__.set_owner(cls)
573
+ metadata = function.__injection_metadata__.set_owner(cls)
785
574
  return SimpleInvertible(metadata.call)
786
575
 
787
576
  def update[T](self, updater: Updater[T]) -> Self:
788
577
  self.__locator.update(updater)
789
578
  return self
790
579
 
580
+ def update_from[T](
581
+ self,
582
+ on: TypeInfo[T],
583
+ /,
584
+ broker: InjectableBroker[T],
585
+ mode: Mode | ModeStr,
586
+ ) -> Self:
587
+ updater = Updater(
588
+ classes=self.__build_key_types(on),
589
+ broker=broker,
590
+ mode=Mode(mode),
591
+ )
592
+ self.update(updater)
593
+ return self
594
+
791
595
  def init_modules(self, *modules: Module) -> Self:
792
596
  for module in tuple(self.__modules):
793
597
  self.stop_using(module)
@@ -857,20 +661,14 @@ class Module(Broker, EventListener):
857
661
  return self
858
662
 
859
663
  def unlock(self) -> Self:
860
- event = UnlockCalled()
861
-
862
- with self.dispatch(event, lock_bypass=True):
863
- self.unsafe_unlocking()
664
+ for locator in self._iter_locators():
665
+ locator.unlock(self)
864
666
 
865
667
  return self
866
668
 
867
- def unsafe_unlocking(self) -> None:
868
- for broker in self.__brokers:
869
- broker.unsafe_unlocking()
870
-
871
669
  async def all_ready(self) -> None:
872
- for broker in self.__brokers:
873
- await broker.all_ready()
670
+ for locator in self._iter_locators():
671
+ await locator.all_ready(self)
874
672
 
875
673
  def add_logger(self, logger: Logger) -> Self:
876
674
  self.__loggers.append(logger)
@@ -889,9 +687,8 @@ class Module(Broker, EventListener):
889
687
  return self.dispatch(self_event)
890
688
 
891
689
  @contextmanager
892
- def dispatch(self, event: Event, *, lock_bypass: bool = False) -> Iterator[None]:
893
- if not lock_bypass:
894
- self.__check_locking()
690
+ def dispatch(self, event: Event) -> Iterator[None]:
691
+ self.__check_locking()
895
692
 
896
693
  with self.__channel.dispatch(event):
897
694
  try:
@@ -899,6 +696,12 @@ class Module(Broker, EventListener):
899
696
  finally:
900
697
  self.__debug(event)
901
698
 
699
+ def _iter_locators(self) -> Iterator[Locator]:
700
+ for module in self.__modules:
701
+ yield from module._iter_locators()
702
+
703
+ yield self.__locator
704
+
902
705
  def __debug(self, message: object) -> None:
903
706
  for logger in self.__loggers:
904
707
  logger.debug(message)
@@ -930,6 +733,20 @@ class Module(Broker, EventListener):
930
733
  def default(cls) -> Module:
931
734
  return cls.from_name("__default__")
932
735
 
736
+ @staticmethod
737
+ def __build_key_types(input_cls: Any) -> frozenset[Any]:
738
+ config = MatchingTypesConfig(ignore_none=True)
739
+ return frozenset(
740
+ itertools.chain.from_iterable(
741
+ iter_matching_types(cls, config) for cls in get_return_types(input_cls)
742
+ )
743
+ )
744
+
745
+ @staticmethod
746
+ def __matching_key_types(input_cls: Any) -> tuple[Any, ...]:
747
+ config = MatchingTypesConfig(with_origin=True, with_type_alias_value=True)
748
+ return matching_types(input_cls, config)
749
+
933
750
 
934
751
  def mod(name: str | None = None, /) -> Module:
935
752
  if name is None:
@@ -1151,24 +968,24 @@ class InjectMetadata[**P, T](Caller[P, T], EventListener):
1151
968
  task()
1152
969
 
1153
970
 
1154
- class InjectedFunction[**P, T](ABC):
1155
- __slots__ = ("__dict__", "__inject_metadata__")
971
+ class InjectedFunction[**P, T](HiddenCaller[P, T], ABC):
972
+ __slots__ = ("__dict__", "__injection_metadata__")
1156
973
 
1157
- __inject_metadata__: InjectMetadata[P, T]
974
+ __injection_metadata__: InjectMetadata[P, T]
1158
975
 
1159
976
  def __init__(self, metadata: InjectMetadata[P, T]) -> None:
1160
977
  update_wrapper(self, metadata.wrapped)
1161
- self.__inject_metadata__ = metadata
978
+ self.__injection_metadata__ = metadata
1162
979
 
1163
980
  def __repr__(self) -> str: # pragma: no cover
1164
- return repr(self.__inject_metadata__.wrapped)
981
+ return repr(self.__injection_metadata__.wrapped)
1165
982
 
1166
983
  def __str__(self) -> str: # pragma: no cover
1167
- return str(self.__inject_metadata__.wrapped)
984
+ return str(self.__injection_metadata__.wrapped)
1168
985
 
1169
- @abstractmethod
1170
- def __call__(self, /, *args: P.args, **kwargs: P.kwargs) -> T:
1171
- raise NotImplementedError
986
+ @property
987
+ def __injection_hidden_caller__(self) -> Caller[P, T]:
988
+ return self.__injection_metadata__
1172
989
 
1173
990
  def __get__(
1174
991
  self,
@@ -1181,7 +998,7 @@ class InjectedFunction[**P, T](ABC):
1181
998
  return MethodType(self, instance)
1182
999
 
1183
1000
  def __set_name__(self, owner: type, name: str) -> None:
1184
- self.__inject_metadata__.set_owner(owner)
1001
+ self.__injection_metadata__.set_owner(owner)
1185
1002
 
1186
1003
 
1187
1004
  class AsyncInjectedFunction[**P, T](InjectedFunction[P, Awaitable[T]]):
@@ -1192,23 +1009,11 @@ class AsyncInjectedFunction[**P, T](InjectedFunction[P, Awaitable[T]]):
1192
1009
  markcoroutinefunction(self)
1193
1010
 
1194
1011
  async def __call__(self, /, *args: P.args, **kwargs: P.kwargs) -> T:
1195
- return await (await self.__inject_metadata__.acall(*args, **kwargs))
1012
+ return await (await self.__injection_metadata__.acall(*args, **kwargs))
1196
1013
 
1197
1014
 
1198
1015
  class SyncInjectedFunction[**P, T](InjectedFunction[P, T]):
1199
1016
  __slots__ = ()
1200
1017
 
1201
1018
  def __call__(self, /, *args: P.args, **kwargs: P.kwargs) -> T:
1202
- return self.__inject_metadata__.call(*args, **kwargs)
1203
-
1204
-
1205
- def extract_caller[**P, T](
1206
- function: Callable[P, T] | Callable[P, Awaitable[T]],
1207
- ) -> Caller[P, T]:
1208
- if iscoroutinefunction(function):
1209
- return AsyncCaller(function)
1210
-
1211
- elif isinstance(function, InjectedFunction):
1212
- return function.__inject_metadata__
1213
-
1214
- return SyncCaller(function) # type: ignore[arg-type]
1019
+ return self.__injection_metadata__.call(*args, **kwargs)
@@ -182,11 +182,11 @@ def get_scope[T](name: str, default: T | EllipsisType = ...) -> Scope | T:
182
182
  return default
183
183
 
184
184
 
185
- def in_scope_cache(key: Any, scope_name: str) -> bool:
185
+ def in_scope_cache(key: SlotKey[Any], scope_name: str) -> bool:
186
186
  return any(key in scope.cache for scope in get_active_scopes(scope_name))
187
187
 
188
188
 
189
- def remove_scoped_values(key: Any, scope_name: str) -> None:
189
+ def remove_scoped_values(key: SlotKey[Any], scope_name: str) -> None:
190
190
  for scope in get_active_scopes(scope_name):
191
191
  scope.cache.pop(key, None)
192
192
 
@@ -233,7 +233,7 @@ def _bind_scope(
233
233
  class Scope(Protocol):
234
234
  __slots__ = ()
235
235
 
236
- cache: MutableMapping[Any, Any]
236
+ cache: MutableMapping[SlotKey[Any], Any]
237
237
 
238
238
  @abstractmethod
239
239
  async def aenter[T](self, context_manager: AsyncContextManager[T]) -> T:
@@ -247,7 +247,7 @@ class Scope(Protocol):
247
247
  @dataclass(repr=False, frozen=True, slots=True)
248
248
  class BaseScope[T](Scope, ABC):
249
249
  delegate: T
250
- cache: MutableMapping[Any, Any] = field(
250
+ cache: MutableMapping[SlotKey[Any], Any] = field(
251
251
  default_factory=dict,
252
252
  init=False,
253
253
  hash=False,
@@ -0,0 +1,30 @@
1
+ from typing import Final
2
+
3
+ from injection import mod
4
+ from injection.loaders import LoadedProfile, ProfileLoader, load_profile
5
+
6
+ __all__ = (
7
+ "TEST_PROFILE_NAME",
8
+ "load_test_profile",
9
+ "reserve_scoped_test_slot",
10
+ "set_test_constant",
11
+ "should_be_test_injectable",
12
+ "test_constant",
13
+ "test_injectable",
14
+ "test_scoped",
15
+ "test_singleton",
16
+ )
17
+
18
+ TEST_PROFILE_NAME: Final[str] = "__testing__"
19
+
20
+ reserve_scoped_test_slot = mod(TEST_PROFILE_NAME).reserve_scoped_slot
21
+ set_test_constant = mod(TEST_PROFILE_NAME).set_constant
22
+ should_be_test_injectable = mod(TEST_PROFILE_NAME).should_be_injectable
23
+ test_constant = mod(TEST_PROFILE_NAME).constant
24
+ test_injectable = mod(TEST_PROFILE_NAME).injectable
25
+ test_scoped = mod(TEST_PROFILE_NAME).scoped
26
+ test_singleton = mod(TEST_PROFILE_NAME).singleton
27
+
28
+
29
+ def load_test_profile(loader: ProfileLoader | None = None) -> LoadedProfile:
30
+ return load_profile(TEST_PROFILE_NAME, loader)
@@ -3,6 +3,8 @@ from typing import Final
3
3
  from injection import Module
4
4
  from injection.loaders import LoadedProfile, ProfileLoader
5
5
 
6
+ TEST_PROFILE_NAME: Final[str] = ...
7
+
6
8
  __MODULE: Final[Module] = ...
7
9
 
8
10
  reserve_scoped_test_slot = __MODULE.reserve_scoped_slot
@@ -30,7 +30,7 @@ test = [
30
30
 
31
31
  [project]
32
32
  name = "python-injection"
33
- version = "0.24.0"
33
+ version = "0.25.1"
34
34
  description = "Fast and easy dependency injection framework."
35
35
  license = "MIT"
36
36
  license-files = ["LICENSE"]
@@ -1,29 +0,0 @@
1
- from typing import Final
2
-
3
- from injection import mod
4
- from injection.loaders import LoadedProfile, ProfileLoader, load_profile
5
-
6
- __all__ = (
7
- "load_test_profile",
8
- "reserve_scoped_test_slot",
9
- "set_test_constant",
10
- "should_be_test_injectable",
11
- "test_constant",
12
- "test_injectable",
13
- "test_scoped",
14
- "test_singleton",
15
- )
16
-
17
- _TEST_PROFILE_NAME: Final[str] = "__testing__"
18
-
19
- reserve_scoped_test_slot = mod(_TEST_PROFILE_NAME).reserve_scoped_slot
20
- set_test_constant = mod(_TEST_PROFILE_NAME).set_constant
21
- should_be_test_injectable = mod(_TEST_PROFILE_NAME).should_be_injectable
22
- test_constant = mod(_TEST_PROFILE_NAME).constant
23
- test_injectable = mod(_TEST_PROFILE_NAME).injectable
24
- test_scoped = mod(_TEST_PROFILE_NAME).scoped
25
- test_singleton = mod(_TEST_PROFILE_NAME).singleton
26
-
27
-
28
- def load_test_profile(loader: ProfileLoader | None = None) -> LoadedProfile:
29
- return load_profile(_TEST_PROFILE_NAME, loader)