python-injection 0.4.2__py3-none-any.whl → 0.5.1__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.
Potentially problematic release.
This version of python-injection might be problematic. Click here for more details.
- injection/__init__.py +9 -7
- injection/common/event.py +8 -1
- injection/common/lazy.py +1 -1
- injection/core/module.py +376 -138
- injection/exceptions.py +5 -1
- injection/utils.py +4 -0
- {python_injection-0.4.2.dist-info → python_injection-0.5.1.dist-info}/METADATA +6 -6
- python_injection-0.5.1.dist-info/RECORD +11 -0
- injection/core/module.pyi +0 -44
- injection/utils.pyi +0 -6
- python_injection-0.4.2.dist-info/RECORD +0 -13
- {python_injection-0.4.2.dist-info → python_injection-0.5.1.dist-info}/WHEEL +0 -0
injection/__init__.py
CHANGED
|
@@ -1,17 +1,19 @@
|
|
|
1
|
-
from .core import Module,
|
|
1
|
+
from .core import Injectable, Module, ModulePriorities
|
|
2
2
|
|
|
3
3
|
__all__ = (
|
|
4
|
+
"Injectable",
|
|
4
5
|
"Module",
|
|
6
|
+
"ModulePriorities",
|
|
7
|
+
"default_module",
|
|
5
8
|
"get_instance",
|
|
6
9
|
"inject",
|
|
7
10
|
"injectable",
|
|
8
|
-
"new_module",
|
|
9
11
|
"singleton",
|
|
10
12
|
)
|
|
11
13
|
|
|
12
|
-
|
|
14
|
+
default_module = Module(f"{__name__}:default_module")
|
|
13
15
|
|
|
14
|
-
get_instance =
|
|
15
|
-
inject =
|
|
16
|
-
injectable =
|
|
17
|
-
singleton =
|
|
16
|
+
get_instance = default_module.get_instance
|
|
17
|
+
inject = default_module.inject
|
|
18
|
+
injectable = default_module.injectable
|
|
19
|
+
singleton = default_module.singleton
|
injection/common/event.py
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
from abc import ABC, abstractmethod
|
|
2
|
+
from contextlib import suppress
|
|
2
3
|
from dataclasses import dataclass, field
|
|
3
4
|
from weakref import WeakSet
|
|
4
5
|
|
|
@@ -17,7 +18,7 @@ class EventListener(ABC):
|
|
|
17
18
|
raise NotImplementedError
|
|
18
19
|
|
|
19
20
|
|
|
20
|
-
@dataclass(repr=False, frozen=True, slots=True)
|
|
21
|
+
@dataclass(repr=False, eq=False, frozen=True, slots=True)
|
|
21
22
|
class EventChannel:
|
|
22
23
|
__listeners: WeakSet[EventListener] = field(default_factory=WeakSet, init=False)
|
|
23
24
|
|
|
@@ -30,3 +31,9 @@ class EventChannel:
|
|
|
30
31
|
def add_listener(self, listener: EventListener):
|
|
31
32
|
self.__listeners.add(listener)
|
|
32
33
|
return self
|
|
34
|
+
|
|
35
|
+
def remove_listener(self, listener: EventListener):
|
|
36
|
+
with suppress(KeyError):
|
|
37
|
+
self.__listeners.remove(listener)
|
|
38
|
+
|
|
39
|
+
return self
|
injection/common/lazy.py
CHANGED
|
@@ -22,7 +22,7 @@ class Lazy(Generic[T]):
|
|
|
22
22
|
|
|
23
23
|
def __setattr__(self, name: str, value: Any):
|
|
24
24
|
if self.is_set:
|
|
25
|
-
raise TypeError(f"`{repr(self)}` is frozen.")
|
|
25
|
+
raise TypeError(f"`{repr(self)}` is frozen.")
|
|
26
26
|
|
|
27
27
|
return super().__setattr__(name, value)
|
|
28
28
|
|
injection/core/module.py
CHANGED
|
@@ -2,14 +2,17 @@ from __future__ import annotations
|
|
|
2
2
|
|
|
3
3
|
import inspect
|
|
4
4
|
from abc import ABC, abstractmethod
|
|
5
|
+
from collections import ChainMap, OrderedDict
|
|
6
|
+
from contextlib import ContextDecorator, contextmanager
|
|
5
7
|
from dataclasses import dataclass, field
|
|
6
|
-
from
|
|
7
|
-
from
|
|
8
|
+
from enum import Enum, auto
|
|
9
|
+
from functools import cached_property, singledispatchmethod, wraps
|
|
10
|
+
from inspect import Signature, get_annotations
|
|
11
|
+
from logging import getLogger
|
|
8
12
|
from types import MappingProxyType
|
|
9
13
|
from typing import (
|
|
10
14
|
Any,
|
|
11
15
|
Callable,
|
|
12
|
-
Generic,
|
|
13
16
|
Iterable,
|
|
14
17
|
Iterator,
|
|
15
18
|
Mapping,
|
|
@@ -24,49 +27,145 @@ from typing import (
|
|
|
24
27
|
|
|
25
28
|
from injection.common.event import Event, EventChannel, EventListener
|
|
26
29
|
from injection.common.lazy import LazyMapping
|
|
27
|
-
from injection.exceptions import NoInjectable
|
|
30
|
+
from injection.exceptions import ModuleError, NoInjectable
|
|
28
31
|
|
|
29
|
-
__all__ = ("Module", "
|
|
32
|
+
__all__ = ("Injectable", "Module", "ModulePriorities")
|
|
33
|
+
|
|
34
|
+
_logger = getLogger(__name__)
|
|
30
35
|
|
|
31
36
|
T = TypeVar("T")
|
|
32
37
|
|
|
33
38
|
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
39
|
+
def _format_type(cls: type) -> str:
|
|
40
|
+
try:
|
|
41
|
+
return f"{cls.__module__}.{cls.__qualname__}"
|
|
42
|
+
except AttributeError:
|
|
43
|
+
return str(cls)
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
"""
|
|
47
|
+
Events
|
|
48
|
+
"""
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
@dataclass(frozen=True, slots=True)
|
|
52
|
+
class ContainerEvent(Event, ABC):
|
|
53
|
+
on_container: Container
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
@dataclass(frozen=True, slots=True)
|
|
57
|
+
class ContainerDependenciesUpdated(ContainerEvent):
|
|
58
|
+
references: set[type]
|
|
59
|
+
|
|
60
|
+
def __str__(self) -> str:
|
|
61
|
+
length = len(self.references)
|
|
62
|
+
formatted_references = ", ".join(
|
|
63
|
+
f"`{_format_type(reference)}`" for reference in self.references
|
|
64
|
+
)
|
|
65
|
+
return (
|
|
66
|
+
f"{length} container dependenc{'ies' if length > 1 else 'y'} have been "
|
|
67
|
+
f"updated{f': {formatted_references}' if formatted_references else ''}."
|
|
68
|
+
)
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
@dataclass(frozen=True, slots=True)
|
|
72
|
+
class ModuleEvent(Event, ABC):
|
|
73
|
+
on_module: Module
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
@dataclass(frozen=True)
|
|
77
|
+
class ModuleEventProxy(ModuleEvent):
|
|
78
|
+
event: Event
|
|
79
|
+
|
|
80
|
+
def __str__(self) -> str:
|
|
81
|
+
return f"`{self.on_module}` has propagated an event: {self.origin}"
|
|
82
|
+
|
|
83
|
+
@cached_property
|
|
84
|
+
def origin(self) -> Event:
|
|
85
|
+
if isinstance(self.event, ModuleEventProxy):
|
|
86
|
+
return self.event.origin
|
|
87
|
+
|
|
88
|
+
return self.event
|
|
89
|
+
|
|
90
|
+
@property
|
|
91
|
+
def is_circular(self) -> bool:
|
|
92
|
+
origin = self.origin
|
|
93
|
+
return isinstance(origin, ModuleEvent) and origin.on_module is self.on_module
|
|
94
|
+
|
|
95
|
+
@property
|
|
96
|
+
def previous_module(self) -> Module | None:
|
|
97
|
+
if isinstance(self.event, ModuleEvent):
|
|
98
|
+
return self.event.on_module
|
|
99
|
+
|
|
100
|
+
return None
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
@dataclass(frozen=True, slots=True)
|
|
104
|
+
class ModuleAdded(ModuleEvent):
|
|
105
|
+
module_added: Module
|
|
106
|
+
|
|
107
|
+
def __str__(self) -> str:
|
|
108
|
+
return f"`{self.on_module}` now uses `{self.module_added}`."
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
@dataclass(frozen=True, slots=True)
|
|
112
|
+
class ModuleRemoved(ModuleEvent):
|
|
113
|
+
module_removed: Module
|
|
114
|
+
|
|
115
|
+
def __str__(self) -> str:
|
|
116
|
+
return f"`{self.on_module}` no longer uses `{self.module_removed}`."
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
@dataclass(frozen=True, slots=True)
|
|
120
|
+
class ModulePriorityUpdated(ModuleEvent):
|
|
121
|
+
module_updated: Module
|
|
122
|
+
priority: ModulePriorities
|
|
123
|
+
|
|
124
|
+
def __str__(self) -> str:
|
|
125
|
+
return (
|
|
126
|
+
f"In `{self.on_module}`, the priority `{self.priority.name}` "
|
|
127
|
+
f"has been applied to `{self.module_updated}`."
|
|
128
|
+
)
|
|
129
|
+
|
|
130
|
+
|
|
131
|
+
"""
|
|
132
|
+
Injectables
|
|
133
|
+
"""
|
|
134
|
+
|
|
135
|
+
|
|
136
|
+
@runtime_checkable
|
|
137
|
+
class Injectable(Protocol[T]):
|
|
138
|
+
__slots__ = ()
|
|
37
139
|
|
|
38
140
|
@abstractmethod
|
|
39
141
|
def get_instance(self) -> T:
|
|
40
142
|
raise NotImplementedError
|
|
41
143
|
|
|
42
144
|
|
|
43
|
-
|
|
145
|
+
@dataclass(repr=False, frozen=True, slots=True)
|
|
146
|
+
class _BaseInjectable(Injectable[T], ABC):
|
|
147
|
+
factory: Callable[[], T]
|
|
148
|
+
|
|
149
|
+
|
|
150
|
+
class NewInjectable(_BaseInjectable[T]):
|
|
44
151
|
__slots__ = ()
|
|
45
152
|
|
|
46
153
|
def get_instance(self) -> T:
|
|
47
154
|
return self.factory()
|
|
48
155
|
|
|
49
156
|
|
|
50
|
-
class SingletonInjectable(
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
157
|
+
class SingletonInjectable(_BaseInjectable[T]):
|
|
158
|
+
@cached_property
|
|
159
|
+
def __instance(self) -> T:
|
|
160
|
+
return self.factory()
|
|
54
161
|
|
|
55
162
|
def get_instance(self) -> T:
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
try:
|
|
59
|
-
instance = getattr(self, cls.__instance_attribute)
|
|
60
|
-
except AttributeError:
|
|
61
|
-
instance = self.factory()
|
|
62
|
-
object.__setattr__(self, cls.__instance_attribute, instance)
|
|
163
|
+
return self.__instance
|
|
63
164
|
|
|
64
|
-
return instance
|
|
65
165
|
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
container: Container
|
|
166
|
+
"""
|
|
167
|
+
Container
|
|
168
|
+
"""
|
|
70
169
|
|
|
71
170
|
|
|
72
171
|
@dataclass(repr=False, frozen=True, slots=True)
|
|
@@ -74,57 +173,241 @@ class Container:
|
|
|
74
173
|
__data: dict[type, Injectable] = field(default_factory=dict, init=False)
|
|
75
174
|
__channel: EventChannel = field(default_factory=EventChannel, init=False)
|
|
76
175
|
|
|
77
|
-
def __getitem__(self, reference: type) -> Injectable:
|
|
176
|
+
def __getitem__(self, reference: type[T]) -> Injectable[T]:
|
|
78
177
|
cls = origin if (origin := get_origin(reference)) else reference
|
|
79
178
|
|
|
80
179
|
try:
|
|
81
180
|
return self.__data[cls]
|
|
82
181
|
except KeyError as exc:
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
182
|
+
raise NoInjectable(f"No injectable for `{_format_type(cls)}`.") from exc
|
|
183
|
+
|
|
184
|
+
def set_multiple(self, references: Iterable[type], injectable: Injectable):
|
|
185
|
+
if not isinstance(references, set):
|
|
186
|
+
references = set(references)
|
|
187
|
+
|
|
188
|
+
if references:
|
|
189
|
+
new_values = (
|
|
190
|
+
(self.check_if_exists(reference), injectable)
|
|
191
|
+
for reference in references
|
|
192
|
+
)
|
|
193
|
+
self.__data.update(new_values)
|
|
194
|
+
event = ContainerDependenciesUpdated(self, references)
|
|
195
|
+
self.notify(event)
|
|
196
|
+
|
|
197
|
+
return self
|
|
198
|
+
|
|
199
|
+
def check_if_exists(self, reference: type) -> type:
|
|
200
|
+
if reference in self.__data:
|
|
201
|
+
raise RuntimeError(
|
|
202
|
+
"An injectable already exists for the reference "
|
|
203
|
+
f"class `{_format_type(reference)}`."
|
|
204
|
+
)
|
|
205
|
+
|
|
206
|
+
return reference
|
|
207
|
+
|
|
208
|
+
def add_listener(self, listener: EventListener):
|
|
209
|
+
self.__channel.add_listener(listener)
|
|
210
|
+
return self
|
|
211
|
+
|
|
212
|
+
def notify(self, event: Event):
|
|
213
|
+
self.__channel.dispatch(event)
|
|
214
|
+
return self
|
|
215
|
+
|
|
216
|
+
|
|
217
|
+
"""
|
|
218
|
+
Module
|
|
219
|
+
"""
|
|
220
|
+
|
|
221
|
+
|
|
222
|
+
class ModulePriorities(Enum):
|
|
223
|
+
HIGH = auto()
|
|
224
|
+
LOW = auto()
|
|
225
|
+
|
|
226
|
+
|
|
227
|
+
@dataclass(repr=False, eq=False, frozen=True, slots=True)
|
|
228
|
+
class Module(EventListener):
|
|
229
|
+
"""
|
|
230
|
+
Object with isolated injection environment.
|
|
231
|
+
|
|
232
|
+
Modules have been designed to simplify unit test writing. So think carefully before
|
|
233
|
+
instantiating a new one. They could increase complexity unnecessarily if used
|
|
234
|
+
extensively.
|
|
235
|
+
"""
|
|
236
|
+
|
|
237
|
+
name: str = field(default=None)
|
|
238
|
+
__container: Container = field(default_factory=Container, init=False)
|
|
239
|
+
__channel: EventChannel = field(default_factory=EventChannel, init=False)
|
|
240
|
+
__modules: OrderedDict[Module, None] = field(
|
|
241
|
+
default_factory=OrderedDict,
|
|
242
|
+
init=False,
|
|
243
|
+
)
|
|
244
|
+
|
|
245
|
+
def __post_init__(self):
|
|
246
|
+
self.__container.add_listener(self)
|
|
247
|
+
|
|
248
|
+
def __getitem__(self, reference: type[T], /) -> Injectable[T]:
|
|
249
|
+
return ChainMap(*self.__modules, self.__container)[reference]
|
|
250
|
+
|
|
251
|
+
def __setitem__(self, on: type | Iterable[type], injectable: Injectable, /):
|
|
252
|
+
references = on if isinstance(on, Iterable) else (on,)
|
|
253
|
+
self.__container.set_multiple(references, injectable)
|
|
87
254
|
|
|
88
|
-
|
|
255
|
+
def __str__(self) -> str:
|
|
256
|
+
return self.name or object.__str__(self)
|
|
89
257
|
|
|
90
258
|
@property
|
|
91
259
|
def inject(self) -> InjectDecorator:
|
|
260
|
+
"""
|
|
261
|
+
Decorator applicable to a class or function. Inject function dependencies using
|
|
262
|
+
parameter type annotations. If applied to a class, the dependencies resolved
|
|
263
|
+
will be those of the `__init__` method.
|
|
264
|
+
"""
|
|
265
|
+
|
|
92
266
|
return InjectDecorator(self)
|
|
93
267
|
|
|
94
268
|
@property
|
|
95
269
|
def injectable(self) -> InjectableDecorator:
|
|
270
|
+
"""
|
|
271
|
+
Decorator applicable to a class or function. It is used to indicate how the
|
|
272
|
+
injectable will be constructed. At injection time, a new instance will be
|
|
273
|
+
injected each time. Automatically injects constructor dependencies, can be
|
|
274
|
+
disabled with `auto_inject=False`.
|
|
275
|
+
"""
|
|
276
|
+
|
|
96
277
|
return InjectableDecorator(self, NewInjectable)
|
|
97
278
|
|
|
98
279
|
@property
|
|
99
280
|
def singleton(self) -> InjectableDecorator:
|
|
281
|
+
"""
|
|
282
|
+
Decorator applicable to a class or function. It is used to indicate how the
|
|
283
|
+
singleton will be constructed. At injection time, the injected instance will
|
|
284
|
+
always be the same. Automatically injects constructor dependencies, can be
|
|
285
|
+
disabled with `auto_inject=False`.
|
|
286
|
+
"""
|
|
287
|
+
|
|
100
288
|
return InjectableDecorator(self, SingletonInjectable)
|
|
101
289
|
|
|
102
|
-
def
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
290
|
+
def get_instance(self, reference: type[T]) -> T | None:
|
|
291
|
+
"""
|
|
292
|
+
Function used to retrieve an instance associated with the type passed in
|
|
293
|
+
parameter or return `None`.
|
|
294
|
+
"""
|
|
295
|
+
|
|
296
|
+
try:
|
|
297
|
+
injectable = self[reference]
|
|
298
|
+
except KeyError:
|
|
299
|
+
return None
|
|
300
|
+
|
|
301
|
+
instance = injectable.get_instance()
|
|
302
|
+
return cast(reference, instance)
|
|
303
|
+
|
|
304
|
+
def use(
|
|
305
|
+
self,
|
|
306
|
+
module: Module,
|
|
307
|
+
priority: ModulePriorities = ModulePriorities.LOW,
|
|
308
|
+
):
|
|
309
|
+
"""
|
|
310
|
+
Function for using another module. Using another module replaces the module's
|
|
311
|
+
dependencies with those of the module used. If the dependency is not found, it
|
|
312
|
+
will be searched for in the module's dependency container.
|
|
313
|
+
"""
|
|
314
|
+
|
|
315
|
+
if module is self:
|
|
316
|
+
raise ModuleError("Module can't be used by itself.")
|
|
317
|
+
|
|
318
|
+
if module in self.__modules:
|
|
319
|
+
raise ModuleError(f"`{self}` already uses `{module}`.")
|
|
320
|
+
|
|
321
|
+
self.__modules[module] = None
|
|
322
|
+
self.__move_module(module, priority)
|
|
323
|
+
module.add_listener(self)
|
|
324
|
+
event = ModuleAdded(self, module)
|
|
325
|
+
self.notify(event)
|
|
108
326
|
return self
|
|
109
327
|
|
|
110
|
-
def
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
f"reference class `{reference.__name__}`."
|
|
115
|
-
)
|
|
328
|
+
def stop_using(self, module: Module):
|
|
329
|
+
"""
|
|
330
|
+
Function to remove a module in use.
|
|
331
|
+
"""
|
|
116
332
|
|
|
117
|
-
|
|
333
|
+
try:
|
|
334
|
+
self.__modules.pop(module)
|
|
335
|
+
except KeyError:
|
|
336
|
+
...
|
|
337
|
+
else:
|
|
338
|
+
module.remove_listener(self)
|
|
339
|
+
event = ModuleRemoved(self, module)
|
|
340
|
+
self.notify(event)
|
|
341
|
+
|
|
342
|
+
return self
|
|
343
|
+
|
|
344
|
+
@contextmanager
|
|
345
|
+
def use_temporarily(
|
|
346
|
+
self,
|
|
347
|
+
module: Module,
|
|
348
|
+
priority: ModulePriorities = ModulePriorities.LOW,
|
|
349
|
+
) -> ContextDecorator:
|
|
350
|
+
"""
|
|
351
|
+
Context manager or decorator for temporary use of a module.
|
|
352
|
+
"""
|
|
353
|
+
|
|
354
|
+
self.use(module, priority)
|
|
355
|
+
yield
|
|
356
|
+
self.stop_using(module)
|
|
357
|
+
|
|
358
|
+
def change_priority(self, module: Module, priority: ModulePriorities):
|
|
359
|
+
"""
|
|
360
|
+
Function for changing the priority of a module in use.
|
|
361
|
+
There are two priority values:
|
|
362
|
+
|
|
363
|
+
* **LOW**: The module concerned becomes the least important of the modules used.
|
|
364
|
+
* **HIGH**: The module concerned becomes the most important of the modules used.
|
|
365
|
+
"""
|
|
366
|
+
|
|
367
|
+
self.__move_module(module, priority)
|
|
368
|
+
event = ModulePriorityUpdated(self, module, priority)
|
|
369
|
+
self.notify(event)
|
|
370
|
+
return self
|
|
118
371
|
|
|
119
372
|
def add_listener(self, listener: EventListener):
|
|
120
373
|
self.__channel.add_listener(listener)
|
|
121
374
|
return self
|
|
122
375
|
|
|
123
|
-
def
|
|
124
|
-
|
|
376
|
+
def remove_listener(self, listener: EventListener):
|
|
377
|
+
self.__channel.remove_listener(listener)
|
|
378
|
+
return self
|
|
379
|
+
|
|
380
|
+
def on_event(self, event: Event, /):
|
|
381
|
+
self_event = ModuleEventProxy(self, event)
|
|
382
|
+
|
|
383
|
+
if self_event.is_circular:
|
|
384
|
+
raise ModuleError(
|
|
385
|
+
"Circular dependency between two modules: "
|
|
386
|
+
f"`{self_event.previous_module}` and `{self}`."
|
|
387
|
+
)
|
|
388
|
+
|
|
389
|
+
self.notify(self_event)
|
|
390
|
+
|
|
391
|
+
def notify(self, event: Event):
|
|
392
|
+
_logger.debug(f"{event}")
|
|
125
393
|
self.__channel.dispatch(event)
|
|
126
394
|
return self
|
|
127
395
|
|
|
396
|
+
def __move_module(self, module: Module, priority: ModulePriorities):
|
|
397
|
+
last = priority == ModulePriorities.LOW
|
|
398
|
+
|
|
399
|
+
try:
|
|
400
|
+
self.__modules.move_to_end(module, last=last)
|
|
401
|
+
except KeyError as exc:
|
|
402
|
+
raise ModuleError(
|
|
403
|
+
f"`{module}` can't be found in the modules used by `{self}`."
|
|
404
|
+
) from exc
|
|
405
|
+
|
|
406
|
+
|
|
407
|
+
"""
|
|
408
|
+
Binder
|
|
409
|
+
"""
|
|
410
|
+
|
|
128
411
|
|
|
129
412
|
@dataclass(repr=False, frozen=True, slots=True)
|
|
130
413
|
class Dependencies:
|
|
@@ -150,20 +433,20 @@ class Dependencies:
|
|
|
150
433
|
return cls.from_mapping({})
|
|
151
434
|
|
|
152
435
|
@classmethod
|
|
153
|
-
def resolve(cls, signature: Signature,
|
|
154
|
-
dependencies = LazyMapping(cls.__resolver(signature,
|
|
436
|
+
def resolve(cls, signature: Signature, module: Module):
|
|
437
|
+
dependencies = LazyMapping(cls.__resolver(signature, module))
|
|
155
438
|
return cls.from_mapping(dependencies)
|
|
156
439
|
|
|
157
440
|
@classmethod
|
|
158
441
|
def __resolver(
|
|
159
442
|
cls,
|
|
160
443
|
signature: Signature,
|
|
161
|
-
|
|
444
|
+
module: Module,
|
|
162
445
|
) -> Iterator[tuple[str, Injectable]]:
|
|
163
446
|
for name, parameter in signature.parameters.items():
|
|
164
447
|
try:
|
|
165
|
-
injectable =
|
|
166
|
-
except
|
|
448
|
+
injectable = module[parameter.annotation]
|
|
449
|
+
except KeyError:
|
|
167
450
|
continue
|
|
168
451
|
|
|
169
452
|
yield name, injectable
|
|
@@ -186,48 +469,33 @@ class Binder(EventListener):
|
|
|
186
469
|
return Arguments(args, kwargs)
|
|
187
470
|
|
|
188
471
|
bound = self.__signature.bind_partial(*args, **kwargs)
|
|
189
|
-
arguments = self.__dependencies.arguments | bound.arguments
|
|
190
|
-
|
|
191
|
-
positional = []
|
|
192
|
-
keywords = {}
|
|
472
|
+
bound.arguments = self.__dependencies.arguments | bound.arguments
|
|
473
|
+
return Arguments(bound.args, bound.kwargs)
|
|
193
474
|
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
value = arguments.pop(name)
|
|
197
|
-
except KeyError:
|
|
198
|
-
continue
|
|
199
|
-
|
|
200
|
-
match parameter.kind:
|
|
201
|
-
case Parameter.POSITIONAL_ONLY:
|
|
202
|
-
positional.append(value)
|
|
203
|
-
case Parameter.VAR_POSITIONAL:
|
|
204
|
-
positional.extend(value)
|
|
205
|
-
case Parameter.VAR_KEYWORD:
|
|
206
|
-
keywords.update(value)
|
|
207
|
-
case _:
|
|
208
|
-
keywords[name] = value
|
|
209
|
-
|
|
210
|
-
return Arguments(tuple(positional), keywords)
|
|
211
|
-
|
|
212
|
-
def update(self, container: Container):
|
|
213
|
-
self.__dependencies = Dependencies.resolve(self.__signature, container)
|
|
475
|
+
def update(self, module: Module):
|
|
476
|
+
self.__dependencies = Dependencies.resolve(self.__signature, module)
|
|
214
477
|
return self
|
|
215
478
|
|
|
216
479
|
@singledispatchmethod
|
|
217
480
|
def on_event(self, event: Event, /):
|
|
218
|
-
...
|
|
481
|
+
...
|
|
219
482
|
|
|
220
483
|
@on_event.register
|
|
221
|
-
def _(self, event:
|
|
222
|
-
self.update(event.
|
|
484
|
+
def _(self, event: ModuleEvent, /):
|
|
485
|
+
self.update(event.on_module)
|
|
486
|
+
|
|
487
|
+
|
|
488
|
+
"""
|
|
489
|
+
Decorators
|
|
490
|
+
"""
|
|
223
491
|
|
|
224
492
|
|
|
225
493
|
@final
|
|
226
494
|
@dataclass(repr=False, frozen=True, slots=True)
|
|
227
495
|
class InjectDecorator:
|
|
228
|
-
|
|
496
|
+
__module: Module
|
|
229
497
|
|
|
230
|
-
def __call__(self, wrapped=None, /):
|
|
498
|
+
def __call__(self, wrapped: Callable[..., Any] = None, /):
|
|
231
499
|
def decorator(wp):
|
|
232
500
|
if isinstance(wp, type):
|
|
233
501
|
return self.__class_decorator(wp)
|
|
@@ -237,9 +505,9 @@ class InjectDecorator:
|
|
|
237
505
|
return decorator(wrapped) if wrapped else decorator
|
|
238
506
|
|
|
239
507
|
def __decorator(self, function: Callable[..., Any], /) -> Callable[..., Any]:
|
|
240
|
-
signature = inspect.signature(function)
|
|
241
|
-
binder = Binder(signature).update(self.
|
|
242
|
-
self.
|
|
508
|
+
signature = inspect.signature(function, eval_str=True)
|
|
509
|
+
binder = Binder(signature).update(self.__module)
|
|
510
|
+
self.__module.add_listener(binder)
|
|
243
511
|
|
|
244
512
|
@wraps(function)
|
|
245
513
|
def wrapper(*args, **kwargs):
|
|
@@ -257,21 +525,27 @@ class InjectDecorator:
|
|
|
257
525
|
@final
|
|
258
526
|
@dataclass(repr=False, frozen=True, slots=True)
|
|
259
527
|
class InjectableDecorator:
|
|
260
|
-
|
|
261
|
-
__class: type[
|
|
528
|
+
__module: Module
|
|
529
|
+
__class: type[_BaseInjectable]
|
|
262
530
|
|
|
263
531
|
def __repr__(self) -> str:
|
|
264
|
-
return f"<{self.__class.
|
|
265
|
-
|
|
266
|
-
def __call__(
|
|
532
|
+
return f"<{self.__class.__qualname__} decorator>"
|
|
533
|
+
|
|
534
|
+
def __call__(
|
|
535
|
+
self,
|
|
536
|
+
wrapped: Callable[..., Any] = None,
|
|
537
|
+
/,
|
|
538
|
+
on: type | Iterable[type] = None,
|
|
539
|
+
auto_inject: bool = True,
|
|
540
|
+
):
|
|
267
541
|
def decorator(wp):
|
|
268
542
|
if auto_inject:
|
|
269
|
-
wp = self.
|
|
543
|
+
wp = self.__module.inject(wp)
|
|
270
544
|
|
|
271
|
-
@lambda
|
|
545
|
+
@lambda function: function()
|
|
272
546
|
def references():
|
|
273
|
-
if
|
|
274
|
-
yield
|
|
547
|
+
if reference := self.__get_reference(wp):
|
|
548
|
+
yield reference
|
|
275
549
|
|
|
276
550
|
if on is None:
|
|
277
551
|
return
|
|
@@ -280,56 +554,20 @@ class InjectableDecorator:
|
|
|
280
554
|
else:
|
|
281
555
|
yield on
|
|
282
556
|
|
|
283
|
-
|
|
284
|
-
self.__container.set_multiple(references, injectable)
|
|
285
|
-
|
|
557
|
+
self.__module[references] = self.__class(wp)
|
|
286
558
|
return wp
|
|
287
559
|
|
|
288
560
|
return decorator(wrapped) if wrapped else decorator
|
|
289
561
|
|
|
562
|
+
@staticmethod
|
|
563
|
+
def __get_reference(wrapped: Callable[..., Any], /) -> type | None:
|
|
564
|
+
if isinstance(wrapped, type):
|
|
565
|
+
return wrapped
|
|
290
566
|
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
__slots__ = ()
|
|
294
|
-
|
|
295
|
-
@abstractmethod
|
|
296
|
-
def get_instance(self, *args, **kwargs):
|
|
297
|
-
raise NotImplementedError
|
|
298
|
-
|
|
299
|
-
@abstractmethod
|
|
300
|
-
def inject(self, *args, **kwargs):
|
|
301
|
-
raise NotImplementedError
|
|
302
|
-
|
|
303
|
-
@abstractmethod
|
|
304
|
-
def injectable(self, *args, **kwargs):
|
|
305
|
-
raise NotImplementedError
|
|
306
|
-
|
|
307
|
-
@abstractmethod
|
|
308
|
-
def singleton(self, *args, **kwargs):
|
|
309
|
-
raise NotImplementedError
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
@dataclass(repr=False, frozen=True, slots=True)
|
|
313
|
-
class InjectionModule:
|
|
314
|
-
__container: Container = field(default_factory=Container, init=False)
|
|
315
|
-
|
|
316
|
-
@property
|
|
317
|
-
def inject(self) -> InjectDecorator:
|
|
318
|
-
return self.__container.inject
|
|
319
|
-
|
|
320
|
-
@property
|
|
321
|
-
def injectable(self) -> InjectableDecorator:
|
|
322
|
-
return self.__container.injectable
|
|
323
|
-
|
|
324
|
-
@property
|
|
325
|
-
def singleton(self) -> InjectableDecorator:
|
|
326
|
-
return self.__container.singleton
|
|
327
|
-
|
|
328
|
-
def get_instance(self, reference: type[T]) -> T:
|
|
329
|
-
instance = self.__container[reference].get_instance()
|
|
330
|
-
return cast(reference, instance)
|
|
567
|
+
elif callable(wrapped):
|
|
568
|
+
return_type = get_annotations(wrapped, eval_str=True).get("return")
|
|
331
569
|
|
|
570
|
+
if isinstance(return_type, type):
|
|
571
|
+
return return_type
|
|
332
572
|
|
|
333
|
-
|
|
334
|
-
module = InjectionModule()
|
|
335
|
-
return cast(Module, module)
|
|
573
|
+
return None
|
injection/exceptions.py
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
__all__ = ("InjectionError", "NoInjectable")
|
|
1
|
+
__all__ = ("InjectionError", "ModuleError", "NoInjectable")
|
|
2
2
|
|
|
3
3
|
|
|
4
4
|
class InjectionError(Exception):
|
|
@@ -7,3 +7,7 @@ class InjectionError(Exception):
|
|
|
7
7
|
|
|
8
8
|
class NoInjectable(KeyError, InjectionError):
|
|
9
9
|
...
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class ModuleError(InjectionError):
|
|
13
|
+
...
|
injection/utils.py
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
Metadata-Version: 2.1
|
|
2
2
|
Name: python-injection
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.5.1
|
|
4
4
|
Summary: Fast and easy dependency injection framework.
|
|
5
|
-
Home-page: https://github.com/
|
|
5
|
+
Home-page: https://github.com/simplysquare/python-injection
|
|
6
6
|
License: MIT
|
|
7
7
|
Keywords: dependencies,inject,injection
|
|
8
8
|
Author: remimd
|
|
@@ -13,10 +13,10 @@ Classifier: Programming Language :: Python :: 3.10
|
|
|
13
13
|
Classifier: Programming Language :: Python :: 3.11
|
|
14
14
|
Classifier: Programming Language :: Python :: 3.12
|
|
15
15
|
Requires-Dist: frozendict
|
|
16
|
-
Project-URL: Repository, https://github.com/
|
|
16
|
+
Project-URL: Repository, https://github.com/simplysquare/python-injection
|
|
17
17
|
Description-Content-Type: text/markdown
|
|
18
18
|
|
|
19
|
-
#
|
|
19
|
+
# Basic usage
|
|
20
20
|
|
|
21
21
|
## Create an injectable
|
|
22
22
|
|
|
@@ -108,12 +108,12 @@ class C(B):
|
|
|
108
108
|
## Recipes
|
|
109
109
|
|
|
110
110
|
A recipe is a function that tells the injector how to construct the instance to be injected. It is important to specify
|
|
111
|
-
the
|
|
111
|
+
the return type annotation when defining the recipe.
|
|
112
112
|
|
|
113
113
|
```python
|
|
114
114
|
from injection import singleton
|
|
115
115
|
|
|
116
|
-
@singleton
|
|
116
|
+
@singleton
|
|
117
117
|
def my_recipe() -> Singleton:
|
|
118
118
|
""" recipe implementation """
|
|
119
119
|
```
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
injection/__init__.py,sha256=4WiI0l-y8fDBm_Rt7-z9oZ5aJKApHvIyEBDhvN03TOA,423
|
|
2
|
+
injection/common/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
3
|
+
injection/common/event.py,sha256=0Jvfvws7bx-NxA32JrqeD-9ywBEgSmHtdEHbJNg_hAo,961
|
|
4
|
+
injection/common/lazy.py,sha256=TqFcozHOVY6OY9Y43oCXzNHydTITf3JkM5hzEn7Z4cw,1503
|
|
5
|
+
injection/core/__init__.py,sha256=zuf0ubI2dHnbjn1059eduhS-ACIkkROa6-dhp10krh0,22
|
|
6
|
+
injection/core/module.py,sha256=DC-iawfHKnNmNjTP7pIi3Na-v4jU9O2dQoLTrMaGnB0,15910
|
|
7
|
+
injection/exceptions.py,sha256=o_LN7rDeX0fRw2cdQlyxvS4OAO2y9P2aRiiBKXPwbCQ,204
|
|
8
|
+
injection/utils.py,sha256=Y_5EEbcpDNdaCaPq8g1e6lHcHRCTMWsGOnZYuHa46aQ,598
|
|
9
|
+
python_injection-0.5.1.dist-info/METADATA,sha256=DJT_0yMjoi9dZ82cfWIiEYUGKKgRzgspiNK4DyPUodQ,2842
|
|
10
|
+
python_injection-0.5.1.dist-info/WHEEL,sha256=FMvqSimYX_P7y0a7UY-_Mc83r5zkBZsCYPm7Lr0Bsq4,88
|
|
11
|
+
python_injection-0.5.1.dist-info/RECORD,,
|
injection/core/module.pyi
DELETED
|
@@ -1,44 +0,0 @@
|
|
|
1
|
-
from abc import abstractmethod
|
|
2
|
-
from typing import Any, Callable, Iterable, Protocol, TypeVar, runtime_checkable
|
|
3
|
-
|
|
4
|
-
_T = TypeVar("_T")
|
|
5
|
-
|
|
6
|
-
@runtime_checkable
|
|
7
|
-
class Module(Protocol):
|
|
8
|
-
"""
|
|
9
|
-
Object with isolated injection environment.
|
|
10
|
-
"""
|
|
11
|
-
|
|
12
|
-
@abstractmethod
|
|
13
|
-
def get_instance(self, reference: type[_T]) -> _T:
|
|
14
|
-
"""
|
|
15
|
-
Function used to retrieve an instance associated with the type passed in parameter or raise `NoInjectable`
|
|
16
|
-
exception.
|
|
17
|
-
"""
|
|
18
|
-
@abstractmethod
|
|
19
|
-
def inject(self, wrapped: Callable[..., Any] = ..., /):
|
|
20
|
-
"""
|
|
21
|
-
Decorator applicable to a class or function. Inject function dependencies using parameter type annotations. If
|
|
22
|
-
applied to a class, the dependencies resolved will be those of the `__init__` method.
|
|
23
|
-
|
|
24
|
-
Doesn't work with type annotations resolved by `__future__` module.
|
|
25
|
-
"""
|
|
26
|
-
@abstractmethod
|
|
27
|
-
def injectable(self, *, on: type | Iterable[type] = ..., auto_inject: bool = ...):
|
|
28
|
-
"""
|
|
29
|
-
Decorator applicable to a class or function. It is used to indicate how the injectable will be constructed. At
|
|
30
|
-
injection time, a new instance will be injected each time. Automatically injects constructor dependencies, can
|
|
31
|
-
be disabled with `auto_inject=False`.
|
|
32
|
-
"""
|
|
33
|
-
@abstractmethod
|
|
34
|
-
def singleton(self, *, on: type | Iterable[type] = ..., auto_inject: bool = ...):
|
|
35
|
-
"""
|
|
36
|
-
Decorator applicable to a class or function. It is used to indicate how the singleton will be constructed. At
|
|
37
|
-
injection time, the injected instance will always be the same. Automatically injects constructor dependencies,
|
|
38
|
-
can be disabled with `auto_inject=False`.
|
|
39
|
-
"""
|
|
40
|
-
|
|
41
|
-
def new_module() -> Module:
|
|
42
|
-
"""
|
|
43
|
-
Function to create a new injection module.
|
|
44
|
-
"""
|
injection/utils.pyi
DELETED
|
@@ -1,13 +0,0 @@
|
|
|
1
|
-
injection/__init__.py,sha256=kcWDMrMTrUtrn0wDYSD1FbTP8G0WV1IAKhFDfHNI83c,340
|
|
2
|
-
injection/common/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
3
|
-
injection/common/event.py,sha256=ncSFjFWCOsZczpfIkKtvA33hAmyTTcpvVpjCm9CJQmo,762
|
|
4
|
-
injection/common/lazy.py,sha256=_mnEq8A2aFhgNYEaz8QNEfRhdlwVclgTGHg6koYeAiU,1523
|
|
5
|
-
injection/core/__init__.py,sha256=zuf0ubI2dHnbjn1059eduhS-ACIkkROa6-dhp10krh0,22
|
|
6
|
-
injection/core/module.py,sha256=Mj7h-tDoW4VEYkXEeDCWlRHdtEGJHQfPFGS5rh02pW8,9256
|
|
7
|
-
injection/core/module.pyi,sha256=PHWQ92faA56FQCwhDzi1K9mZkbKAPXbm2Pru7VX8cTQ,1792
|
|
8
|
-
injection/exceptions.py,sha256=bxWdH61sB6gRfAMltt4GdrPHh-1k6QqV8m3vKQN8O_o,144
|
|
9
|
-
injection/utils.py,sha256=fSVNgUhsbiWzS8I8db9hWhlX6rv5mDJS-d1Vfb6EBMI,521
|
|
10
|
-
injection/utils.pyi,sha256=xpSq678YijDe1SRO9_B2q4SW9WYCYEQgPUhRCTEwVlY,153
|
|
11
|
-
python_injection-0.4.2.dist-info/METADATA,sha256=tAPJfNrnnVqoxHlkz47BXZSeypEQXiLgDDJQfjbubzc,2845
|
|
12
|
-
python_injection-0.4.2.dist-info/WHEEL,sha256=FMvqSimYX_P7y0a7UY-_Mc83r5zkBZsCYPm7Lr0Bsq4,88
|
|
13
|
-
python_injection-0.4.2.dist-info/RECORD,,
|
|
File without changes
|