python-injection 0.4.2__tar.gz → 0.5.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.

Potentially problematic release.


This version of python-injection might be problematic. Click here for more details.

@@ -1,8 +1,8 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: python-injection
3
- Version: 0.4.2
3
+ Version: 0.5.1
4
4
  Summary: Fast and easy dependency injection framework.
5
- Home-page: https://github.com/soon-app/python-injection
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/soon-app/python-injection
16
+ Project-URL: Repository, https://github.com/simplysquare/python-injection
17
17
  Description-Content-Type: text/markdown
18
18
 
19
- # How to use?
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 reference class(es) when defining the recipe.
111
+ the return type annotation when defining the recipe.
112
112
 
113
113
  ```python
114
114
  from injection import singleton
115
115
 
116
- @singleton(on=Singleton)
116
+ @singleton
117
117
  def my_recipe() -> Singleton:
118
118
  """ recipe implementation """
119
119
  ```
@@ -1,4 +1,4 @@
1
- # How to use?
1
+ # Basic usage
2
2
 
3
3
  ## Create an injectable
4
4
 
@@ -90,12 +90,12 @@ class C(B):
90
90
  ## Recipes
91
91
 
92
92
  A recipe is a function that tells the injector how to construct the instance to be injected. It is important to specify
93
- the reference class(es) when defining the recipe.
93
+ the return type annotation when defining the recipe.
94
94
 
95
95
  ```python
96
96
  from injection import singleton
97
97
 
98
- @singleton(on=Singleton)
98
+ @singleton
99
99
  def my_recipe() -> Singleton:
100
100
  """ recipe implementation """
101
101
  ```
@@ -0,0 +1,19 @@
1
+ from .core import Injectable, Module, ModulePriorities
2
+
3
+ __all__ = (
4
+ "Injectable",
5
+ "Module",
6
+ "ModulePriorities",
7
+ "default_module",
8
+ "get_instance",
9
+ "inject",
10
+ "injectable",
11
+ "singleton",
12
+ )
13
+
14
+ default_module = Module(f"{__name__}:default_module")
15
+
16
+ get_instance = default_module.get_instance
17
+ inject = default_module.inject
18
+ injectable = default_module.injectable
19
+ singleton = default_module.singleton
@@ -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
@@ -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.") # pragma: no cover
25
+ raise TypeError(f"`{repr(self)}` is frozen.")
26
26
 
27
27
  return super().__setattr__(name, value)
28
28
 
@@ -0,0 +1,573 @@
1
+ from __future__ import annotations
2
+
3
+ import inspect
4
+ from abc import ABC, abstractmethod
5
+ from collections import ChainMap, OrderedDict
6
+ from contextlib import ContextDecorator, contextmanager
7
+ from dataclasses import dataclass, field
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
12
+ from types import MappingProxyType
13
+ from typing import (
14
+ Any,
15
+ Callable,
16
+ Iterable,
17
+ Iterator,
18
+ Mapping,
19
+ NamedTuple,
20
+ Protocol,
21
+ TypeVar,
22
+ cast,
23
+ final,
24
+ get_origin,
25
+ runtime_checkable,
26
+ )
27
+
28
+ from injection.common.event import Event, EventChannel, EventListener
29
+ from injection.common.lazy import LazyMapping
30
+ from injection.exceptions import ModuleError, NoInjectable
31
+
32
+ __all__ = ("Injectable", "Module", "ModulePriorities")
33
+
34
+ _logger = getLogger(__name__)
35
+
36
+ T = TypeVar("T")
37
+
38
+
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__ = ()
139
+
140
+ @abstractmethod
141
+ def get_instance(self) -> T:
142
+ raise NotImplementedError
143
+
144
+
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]):
151
+ __slots__ = ()
152
+
153
+ def get_instance(self) -> T:
154
+ return self.factory()
155
+
156
+
157
+ class SingletonInjectable(_BaseInjectable[T]):
158
+ @cached_property
159
+ def __instance(self) -> T:
160
+ return self.factory()
161
+
162
+ def get_instance(self) -> T:
163
+ return self.__instance
164
+
165
+
166
+ """
167
+ Container
168
+ """
169
+
170
+
171
+ @dataclass(repr=False, frozen=True, slots=True)
172
+ class Container:
173
+ __data: dict[type, Injectable] = field(default_factory=dict, init=False)
174
+ __channel: EventChannel = field(default_factory=EventChannel, init=False)
175
+
176
+ def __getitem__(self, reference: type[T]) -> Injectable[T]:
177
+ cls = origin if (origin := get_origin(reference)) else reference
178
+
179
+ try:
180
+ return self.__data[cls]
181
+ except KeyError as exc:
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)
254
+
255
+ def __str__(self) -> str:
256
+ return self.name or object.__str__(self)
257
+
258
+ @property
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
+
266
+ return InjectDecorator(self)
267
+
268
+ @property
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
+
277
+ return InjectableDecorator(self, NewInjectable)
278
+
279
+ @property
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
+
288
+ return InjectableDecorator(self, SingletonInjectable)
289
+
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)
326
+ return self
327
+
328
+ def stop_using(self, module: Module):
329
+ """
330
+ Function to remove a module in use.
331
+ """
332
+
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
371
+
372
+ def add_listener(self, listener: EventListener):
373
+ self.__channel.add_listener(listener)
374
+ return self
375
+
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}")
393
+ self.__channel.dispatch(event)
394
+ return self
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
+
411
+
412
+ @dataclass(repr=False, frozen=True, slots=True)
413
+ class Dependencies:
414
+ __mapping: MappingProxyType[str, Injectable]
415
+
416
+ def __bool__(self) -> bool:
417
+ return bool(self.__mapping)
418
+
419
+ def __iter__(self) -> Iterator[tuple[str, Any]]:
420
+ for name, injectable in self.__mapping.items():
421
+ yield name, injectable.get_instance()
422
+
423
+ @property
424
+ def arguments(self) -> Mapping[str, Any]:
425
+ return dict(self)
426
+
427
+ @classmethod
428
+ def from_mapping(cls, mapping: Mapping[str, Injectable]):
429
+ return cls(MappingProxyType(mapping))
430
+
431
+ @classmethod
432
+ def empty(cls):
433
+ return cls.from_mapping({})
434
+
435
+ @classmethod
436
+ def resolve(cls, signature: Signature, module: Module):
437
+ dependencies = LazyMapping(cls.__resolver(signature, module))
438
+ return cls.from_mapping(dependencies)
439
+
440
+ @classmethod
441
+ def __resolver(
442
+ cls,
443
+ signature: Signature,
444
+ module: Module,
445
+ ) -> Iterator[tuple[str, Injectable]]:
446
+ for name, parameter in signature.parameters.items():
447
+ try:
448
+ injectable = module[parameter.annotation]
449
+ except KeyError:
450
+ continue
451
+
452
+ yield name, injectable
453
+
454
+
455
+ class Arguments(NamedTuple):
456
+ args: Iterable[Any]
457
+ kwargs: Mapping[str, Any]
458
+
459
+
460
+ class Binder(EventListener):
461
+ __slots__ = ("__signature", "__dependencies")
462
+
463
+ def __init__(self, signature: Signature):
464
+ self.__signature = signature
465
+ self.__dependencies = Dependencies.empty()
466
+
467
+ def bind(self, /, *args, **kwargs) -> Arguments:
468
+ if not self.__dependencies:
469
+ return Arguments(args, kwargs)
470
+
471
+ bound = self.__signature.bind_partial(*args, **kwargs)
472
+ bound.arguments = self.__dependencies.arguments | bound.arguments
473
+ return Arguments(bound.args, bound.kwargs)
474
+
475
+ def update(self, module: Module):
476
+ self.__dependencies = Dependencies.resolve(self.__signature, module)
477
+ return self
478
+
479
+ @singledispatchmethod
480
+ def on_event(self, event: Event, /):
481
+ ...
482
+
483
+ @on_event.register
484
+ def _(self, event: ModuleEvent, /):
485
+ self.update(event.on_module)
486
+
487
+
488
+ """
489
+ Decorators
490
+ """
491
+
492
+
493
+ @final
494
+ @dataclass(repr=False, frozen=True, slots=True)
495
+ class InjectDecorator:
496
+ __module: Module
497
+
498
+ def __call__(self, wrapped: Callable[..., Any] = None, /):
499
+ def decorator(wp):
500
+ if isinstance(wp, type):
501
+ return self.__class_decorator(wp)
502
+
503
+ return self.__decorator(wp)
504
+
505
+ return decorator(wrapped) if wrapped else decorator
506
+
507
+ def __decorator(self, function: Callable[..., Any], /) -> Callable[..., Any]:
508
+ signature = inspect.signature(function, eval_str=True)
509
+ binder = Binder(signature).update(self.__module)
510
+ self.__module.add_listener(binder)
511
+
512
+ @wraps(function)
513
+ def wrapper(*args, **kwargs):
514
+ arguments = binder.bind(*args, **kwargs)
515
+ return function(*arguments.args, **arguments.kwargs)
516
+
517
+ return wrapper
518
+
519
+ def __class_decorator(self, cls: type, /) -> type:
520
+ init_function = type.__getattribute__(cls, "__init__")
521
+ type.__setattr__(cls, "__init__", self.__decorator(init_function))
522
+ return cls
523
+
524
+
525
+ @final
526
+ @dataclass(repr=False, frozen=True, slots=True)
527
+ class InjectableDecorator:
528
+ __module: Module
529
+ __class: type[_BaseInjectable]
530
+
531
+ def __repr__(self) -> str:
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
+ ):
541
+ def decorator(wp):
542
+ if auto_inject:
543
+ wp = self.__module.inject(wp)
544
+
545
+ @lambda function: function()
546
+ def references():
547
+ if reference := self.__get_reference(wp):
548
+ yield reference
549
+
550
+ if on is None:
551
+ return
552
+ elif isinstance(on, Iterable):
553
+ yield from on
554
+ else:
555
+ yield on
556
+
557
+ self.__module[references] = self.__class(wp)
558
+ return wp
559
+
560
+ return decorator(wrapped) if wrapped else decorator
561
+
562
+ @staticmethod
563
+ def __get_reference(wrapped: Callable[..., Any], /) -> type | None:
564
+ if isinstance(wrapped, type):
565
+ return wrapped
566
+
567
+ elif callable(wrapped):
568
+ return_type = get_annotations(wrapped, eval_str=True).get("return")
569
+
570
+ if isinstance(return_type, type):
571
+ return return_type
572
+
573
+ return None
@@ -0,0 +1,13 @@
1
+ __all__ = ("InjectionError", "ModuleError", "NoInjectable")
2
+
3
+
4
+ class InjectionError(Exception):
5
+ ...
6
+
7
+
8
+ class NoInjectable(KeyError, InjectionError):
9
+ ...
10
+
11
+
12
+ class ModuleError(InjectionError):
13
+ ...
@@ -6,6 +6,10 @@ __all__ = ("load_package",)
6
6
 
7
7
 
8
8
  def load_package(package: Package):
9
+ """
10
+ Function for importing all modules in a Python package.
11
+ """
12
+
9
13
  try:
10
14
  path = package.__path__
11
15
  except AttributeError as exc:
@@ -1,13 +1,13 @@
1
1
  [tool.poetry]
2
2
  name = "python-injection"
3
- version = "0.4.2"
3
+ version = "0.5.1"
4
4
  description = "Fast and easy dependency injection framework."
5
5
  authors = ["remimd"]
6
6
  keywords = ["dependencies", "inject", "injection"]
7
7
  license = "MIT"
8
8
  packages = [{ include = "injection" }]
9
- readme = "documentation/how-to-use.md"
10
- repository = "https://github.com/soon-app/python-injection"
9
+ readme = "documentation/basic-usage.md"
10
+ repository = "https://github.com/simplysquare/python-injection"
11
11
 
12
12
  [tool.poetry.dependencies]
13
13
  python = ">=3.10, <4"
@@ -1,17 +0,0 @@
1
- from .core import Module, new_module
2
-
3
- __all__ = (
4
- "Module",
5
- "get_instance",
6
- "inject",
7
- "injectable",
8
- "new_module",
9
- "singleton",
10
- )
11
-
12
- _default_module = new_module()
13
-
14
- get_instance = _default_module.get_instance
15
- inject = _default_module.inject
16
- injectable = _default_module.injectable
17
- singleton = _default_module.singleton
@@ -1,335 +0,0 @@
1
- from __future__ import annotations
2
-
3
- import inspect
4
- from abc import ABC, abstractmethod
5
- from dataclasses import dataclass, field
6
- from functools import singledispatchmethod, wraps
7
- from inspect import Parameter, Signature
8
- from types import MappingProxyType
9
- from typing import (
10
- Any,
11
- Callable,
12
- Generic,
13
- Iterable,
14
- Iterator,
15
- Mapping,
16
- NamedTuple,
17
- Protocol,
18
- TypeVar,
19
- cast,
20
- final,
21
- get_origin,
22
- runtime_checkable,
23
- )
24
-
25
- from injection.common.event import Event, EventChannel, EventListener
26
- from injection.common.lazy import LazyMapping
27
- from injection.exceptions import NoInjectable
28
-
29
- __all__ = ("Module", "new_module")
30
-
31
- T = TypeVar("T")
32
-
33
-
34
- @dataclass(repr=False, frozen=True, slots=True)
35
- class Injectable(Generic[T], ABC):
36
- factory: Callable[[], T]
37
-
38
- @abstractmethod
39
- def get_instance(self) -> T:
40
- raise NotImplementedError
41
-
42
-
43
- class NewInjectable(Injectable[T]):
44
- __slots__ = ()
45
-
46
- def get_instance(self) -> T:
47
- return self.factory()
48
-
49
-
50
- class SingletonInjectable(Injectable[T]):
51
- __instance_attribute: str = "_instance"
52
-
53
- __slots__ = (__instance_attribute,)
54
-
55
- def get_instance(self) -> T:
56
- cls = type(self)
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)
63
-
64
- return instance
65
-
66
-
67
- @dataclass(repr=False, frozen=True, slots=True)
68
- class ContainerUpdated(Event):
69
- container: Container
70
-
71
-
72
- @dataclass(repr=False, frozen=True, slots=True)
73
- class Container:
74
- __data: dict[type, Injectable] = field(default_factory=dict, init=False)
75
- __channel: EventChannel = field(default_factory=EventChannel, init=False)
76
-
77
- def __getitem__(self, reference: type) -> Injectable:
78
- cls = origin if (origin := get_origin(reference)) else reference
79
-
80
- try:
81
- return self.__data[cls]
82
- except KeyError as exc:
83
- try:
84
- name = f"{cls.__module__}.{cls.__qualname__}"
85
- except AttributeError:
86
- name = repr(reference)
87
-
88
- raise NoInjectable(f"No injectable for `{name}`.") from exc
89
-
90
- @property
91
- def inject(self) -> InjectDecorator:
92
- return InjectDecorator(self)
93
-
94
- @property
95
- def injectable(self) -> InjectableDecorator:
96
- return InjectableDecorator(self, NewInjectable)
97
-
98
- @property
99
- def singleton(self) -> InjectableDecorator:
100
- return InjectableDecorator(self, SingletonInjectable)
101
-
102
- def set_multiple(self, references: Iterable[type], injectable: Injectable):
103
- new_values = (
104
- (self.check_if_exists(reference), injectable) for reference in references
105
- )
106
- self.__data.update(new_values)
107
- self.__notify()
108
- return self
109
-
110
- def check_if_exists(self, reference: type) -> type:
111
- if reference in self.__data:
112
- raise RuntimeError(
113
- "An injectable already exists for the "
114
- f"reference class `{reference.__name__}`."
115
- )
116
-
117
- return reference
118
-
119
- def add_listener(self, listener: EventListener):
120
- self.__channel.add_listener(listener)
121
- return self
122
-
123
- def __notify(self):
124
- event = ContainerUpdated(self)
125
- self.__channel.dispatch(event)
126
- return self
127
-
128
-
129
- @dataclass(repr=False, frozen=True, slots=True)
130
- class Dependencies:
131
- __mapping: MappingProxyType[str, Injectable]
132
-
133
- def __bool__(self) -> bool:
134
- return bool(self.__mapping)
135
-
136
- def __iter__(self) -> Iterator[tuple[str, Any]]:
137
- for name, injectable in self.__mapping.items():
138
- yield name, injectable.get_instance()
139
-
140
- @property
141
- def arguments(self) -> Mapping[str, Any]:
142
- return dict(self)
143
-
144
- @classmethod
145
- def from_mapping(cls, mapping: Mapping[str, Injectable]):
146
- return cls(MappingProxyType(mapping))
147
-
148
- @classmethod
149
- def empty(cls):
150
- return cls.from_mapping({})
151
-
152
- @classmethod
153
- def resolve(cls, signature: Signature, container: Container):
154
- dependencies = LazyMapping(cls.__resolver(signature, container))
155
- return cls.from_mapping(dependencies)
156
-
157
- @classmethod
158
- def __resolver(
159
- cls,
160
- signature: Signature,
161
- container: Container,
162
- ) -> Iterator[tuple[str, Injectable]]:
163
- for name, parameter in signature.parameters.items():
164
- try:
165
- injectable = container[parameter.annotation]
166
- except NoInjectable:
167
- continue
168
-
169
- yield name, injectable
170
-
171
-
172
- class Arguments(NamedTuple):
173
- args: Iterable[Any]
174
- kwargs: Mapping[str, Any]
175
-
176
-
177
- class Binder(EventListener):
178
- __slots__ = ("__signature", "__dependencies")
179
-
180
- def __init__(self, signature: Signature):
181
- self.__signature = signature
182
- self.__dependencies = Dependencies.empty()
183
-
184
- def bind(self, /, *args, **kwargs) -> Arguments:
185
- if not self.__dependencies:
186
- return Arguments(args, kwargs)
187
-
188
- bound = self.__signature.bind_partial(*args, **kwargs)
189
- arguments = self.__dependencies.arguments | bound.arguments
190
-
191
- positional = []
192
- keywords = {}
193
-
194
- for name, parameter in self.__signature.parameters.items():
195
- try:
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)
214
- return self
215
-
216
- @singledispatchmethod
217
- def on_event(self, event: Event, /):
218
- ... # pragma: no cover
219
-
220
- @on_event.register
221
- def _(self, event: ContainerUpdated, /):
222
- self.update(event.container)
223
-
224
-
225
- @final
226
- @dataclass(repr=False, frozen=True, slots=True)
227
- class InjectDecorator:
228
- __container: Container
229
-
230
- def __call__(self, wrapped=None, /):
231
- def decorator(wp):
232
- if isinstance(wp, type):
233
- return self.__class_decorator(wp)
234
-
235
- return self.__decorator(wp)
236
-
237
- return decorator(wrapped) if wrapped else decorator
238
-
239
- def __decorator(self, function: Callable[..., Any], /) -> Callable[..., Any]:
240
- signature = inspect.signature(function)
241
- binder = Binder(signature).update(self.__container)
242
- self.__container.add_listener(binder)
243
-
244
- @wraps(function)
245
- def wrapper(*args, **kwargs):
246
- arguments = binder.bind(*args, **kwargs)
247
- return function(*arguments.args, **arguments.kwargs)
248
-
249
- return wrapper
250
-
251
- def __class_decorator(self, cls: type, /) -> type:
252
- init_function = type.__getattribute__(cls, "__init__")
253
- type.__setattr__(cls, "__init__", self.__decorator(init_function))
254
- return cls
255
-
256
-
257
- @final
258
- @dataclass(repr=False, frozen=True, slots=True)
259
- class InjectableDecorator:
260
- __container: Container
261
- __class: type[Injectable]
262
-
263
- def __repr__(self) -> str:
264
- return f"<{self.__class.__name__} decorator>" # pragma: no cover
265
-
266
- def __call__(self, wrapped=None, /, on=None, auto_inject=True):
267
- def decorator(wp):
268
- if auto_inject:
269
- wp = self.__container.inject(wp)
270
-
271
- @lambda fn: fn()
272
- def references():
273
- if isinstance(wp, type):
274
- yield wp
275
-
276
- if on is None:
277
- return
278
- elif isinstance(on, Iterable):
279
- yield from on
280
- else:
281
- yield on
282
-
283
- injectable = self.__class(wp)
284
- self.__container.set_multiple(references, injectable)
285
-
286
- return wp
287
-
288
- return decorator(wrapped) if wrapped else decorator
289
-
290
-
291
- @runtime_checkable
292
- class Module(Protocol):
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)
331
-
332
-
333
- def new_module() -> Module:
334
- module = InjectionModule()
335
- return cast(Module, module)
@@ -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
- """
@@ -1,9 +0,0 @@
1
- __all__ = ("InjectionError", "NoInjectable")
2
-
3
-
4
- class InjectionError(Exception):
5
- ...
6
-
7
-
8
- class NoInjectable(KeyError, InjectionError):
9
- ...
@@ -1,6 +0,0 @@
1
- from types import ModuleType as Package
2
-
3
- def load_package(package: Package):
4
- """
5
- Function for importing all modules in a Python package.
6
- """