python-injection 0.6.7__tar.gz → 0.6.9__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,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: python-injection
3
- Version: 0.6.7
3
+ Version: 0.6.9
4
4
  Summary: Fast and easy dependency injection framework.
5
5
  Home-page: https://github.com/100nm/python-injection
6
6
  License: MIT
@@ -19,6 +19,8 @@ Description-Content-Type: text/markdown
19
19
 
20
20
  ## Create an injectable
21
21
 
22
+ > **Note**: If the class needs dependencies, these will be resolved when the instance is retrieved.
23
+
22
24
  If you wish to inject a singleton, use `singleton` decorator.
23
25
 
24
26
  ```python
@@ -39,7 +41,14 @@ class Injectable:
39
41
  """ class implementation """
40
42
  ```
41
43
 
42
- > **Note**: If the class needs dependencies, these will be resolved when the instance is retrieved.
44
+ If you have a constant (such as a global variable) and wish to register it as an injectable, use `set_constant`
45
+ function.
46
+
47
+ ```python
48
+ from injection import set_constant
49
+
50
+ app = set_constant(Application())
51
+ ```
43
52
 
44
53
  ## Inject an instance
45
54
 
@@ -57,6 +66,9 @@ def my_function(instance: Injectable):
57
66
  If `inject` decorates a class, it will be applied to the `__init__` method.
58
67
  _Especially useful for dataclasses:_
59
68
 
69
+ > **Note**: Doesn't work with Pydantic `BaseModel` because the signature of the `__init__` method doesn't contain the
70
+ > dependencies.
71
+
60
72
  ```python
61
73
  from dataclasses import dataclass
62
74
 
@@ -68,9 +80,6 @@ class DataClass:
68
80
  instance: Injectable = ...
69
81
  ```
70
82
 
71
- > **Note**: Doesn't work with Pydantic `BaseModel` because the signature of the `__init__` method doesn't contain the
72
- > dependencies.
73
-
74
83
  ## Get an instance
75
84
 
76
85
  _Example with `get_instance` function:_
@@ -2,6 +2,8 @@
2
2
 
3
3
  ## Create an injectable
4
4
 
5
+ > **Note**: If the class needs dependencies, these will be resolved when the instance is retrieved.
6
+
5
7
  If you wish to inject a singleton, use `singleton` decorator.
6
8
 
7
9
  ```python
@@ -22,7 +24,14 @@ class Injectable:
22
24
  """ class implementation """
23
25
  ```
24
26
 
25
- > **Note**: If the class needs dependencies, these will be resolved when the instance is retrieved.
27
+ If you have a constant (such as a global variable) and wish to register it as an injectable, use `set_constant`
28
+ function.
29
+
30
+ ```python
31
+ from injection import set_constant
32
+
33
+ app = set_constant(Application())
34
+ ```
26
35
 
27
36
  ## Inject an instance
28
37
 
@@ -40,6 +49,9 @@ def my_function(instance: Injectable):
40
49
  If `inject` decorates a class, it will be applied to the `__init__` method.
41
50
  _Especially useful for dataclasses:_
42
51
 
52
+ > **Note**: Doesn't work with Pydantic `BaseModel` because the signature of the `__init__` method doesn't contain the
53
+ > dependencies.
54
+
43
55
  ```python
44
56
  from dataclasses import dataclass
45
57
 
@@ -51,9 +63,6 @@ class DataClass:
51
63
  instance: Injectable = ...
52
64
  ```
53
65
 
54
- > **Note**: Doesn't work with Pydantic `BaseModel` because the signature of the `__init__` method doesn't contain the
55
- > dependencies.
56
-
57
66
  ## Get an instance
58
67
 
59
68
  _Example with `get_instance` function:_
@@ -1,6 +1,7 @@
1
- from .core import Module, ModulePriorities
1
+ from .core import Injectable, Module, ModulePriorities
2
2
 
3
3
  __all__ = (
4
+ "Injectable",
4
5
  "Module",
5
6
  "ModulePriorities",
6
7
  "default_module",
@@ -8,6 +9,7 @@ __all__ = (
8
9
  "get_lazy_instance",
9
10
  "inject",
10
11
  "injectable",
12
+ "set_constant",
11
13
  "singleton",
12
14
  )
13
15
 
@@ -15,7 +17,7 @@ default_module = Module(f"{__name__}:default_module")
15
17
 
16
18
  get_instance = default_module.get_instance
17
19
  get_lazy_instance = default_module.get_lazy_instance
18
-
19
20
  inject = default_module.inject
20
21
  injectable = default_module.injectable
22
+ set_constant = default_module.set_constant
21
23
  singleton = default_module.singleton
@@ -1,8 +1,17 @@
1
+ from abc import abstractmethod
1
2
  from collections.abc import Callable, Iterable
2
3
  from contextlib import ContextDecorator
3
4
  from enum import Enum
4
5
  from types import UnionType
5
- from typing import Any, ContextManager, Final, TypeVar, final
6
+ from typing import (
7
+ Any,
8
+ ContextManager,
9
+ Final,
10
+ Protocol,
11
+ TypeVar,
12
+ final,
13
+ runtime_checkable,
14
+ )
6
15
 
7
16
  from injection.common.lazy import Lazy
8
17
 
@@ -12,9 +21,9 @@ default_module: Final[Module] = ...
12
21
 
13
22
  get_instance = default_module.get_instance
14
23
  get_lazy_instance = default_module.get_lazy_instance
15
-
16
24
  inject = default_module.inject
17
25
  injectable = default_module.injectable
26
+ set_constant = default_module.set_constant
18
27
  singleton = default_module.singleton
19
28
 
20
29
  @final
@@ -40,6 +49,7 @@ class Module:
40
49
  wrapped: Callable[..., Any] = ...,
41
50
  /,
42
51
  *,
52
+ cls: type[Injectable] = ...,
43
53
  on: type | Iterable[type] | UnionType = ...,
44
54
  ):
45
55
  """
@@ -59,12 +69,22 @@ class Module:
59
69
  singleton will be constructed. At injection time, the injected instance will
60
70
  always be the same.
61
71
  """
62
- def get_instance(self, cls: type[_T] | UnionType) -> _T | None:
72
+ def set_constant(
73
+ self,
74
+ instance: _T,
75
+ on: type | Iterable[type] | UnionType = ...,
76
+ ) -> _T:
77
+ """
78
+ Function for registering a specific instance to be injected. This is useful for
79
+ registering global variables. The difference with the singleton decorator is
80
+ that no dependencies are resolved, so the module doesn't need to be locked.
81
+ """
82
+ def get_instance(self, cls: type[_T]) -> _T | None:
63
83
  """
64
84
  Function used to retrieve an instance associated with the type passed in
65
85
  parameter or return `None`.
66
86
  """
67
- def get_lazy_instance(self, cls: type[_T] | UnionType) -> Lazy[_T | None]:
87
+ def get_lazy_instance(self, cls: type[_T]) -> Lazy[_T | None]:
68
88
  """
69
89
  Function used to retrieve an instance associated with the type passed in
70
90
  parameter or `None`. Return a `Lazy` object. To access the instance contained
@@ -107,3 +127,12 @@ class Module:
107
127
  class ModulePriorities(Enum):
108
128
  HIGH = ...
109
129
  LOW = ...
130
+
131
+ @runtime_checkable
132
+ class Injectable(Protocol[_T]):
133
+ def __init__(self, factory: Callable[[], _T] = ..., *args, **kwargs): ...
134
+ @property
135
+ def is_locked(self) -> bool: ...
136
+ def unlock(self): ...
137
+ @abstractmethod
138
+ def get_instance(self) -> _T: ...
@@ -0,0 +1,48 @@
1
+ from collections.abc import Iterable, Iterator
2
+ from inspect import get_annotations, isfunction
3
+ from types import NoneType, UnionType
4
+ from typing import Annotated, Any, Union, get_args, get_origin
5
+
6
+ __all__ = ("find_types", "format_type", "get_origins")
7
+
8
+
9
+ def format_type(cls: type | Any) -> str:
10
+ try:
11
+ return f"{cls.__module__}.{cls.__qualname__}"
12
+ except AttributeError:
13
+ return str(cls)
14
+
15
+
16
+ def get_origins(*types: type | Any) -> Iterator[type | Any]:
17
+ for tp in types:
18
+ origin = get_origin(tp) or tp
19
+
20
+ if origin in (None, NoneType):
21
+ continue
22
+
23
+ elif origin in (Union, UnionType):
24
+ args = get_args(tp)
25
+
26
+ elif origin is Annotated is not tp:
27
+ args = (tp.__origin__,)
28
+
29
+ else:
30
+ yield origin
31
+ continue
32
+
33
+ yield from get_origins(*args)
34
+
35
+
36
+ def find_types(*args: Any) -> Iterator[type | UnionType]:
37
+ for argument in args:
38
+ if isinstance(argument, Iterable) and not isinstance(argument, type | str):
39
+ arguments = argument
40
+
41
+ elif isfunction(argument):
42
+ arguments = (get_annotations(argument, eval_str=True).get("return"),)
43
+
44
+ else:
45
+ yield argument
46
+ continue
47
+
48
+ yield from find_types(*arguments)
@@ -15,8 +15,8 @@ from collections.abc import (
15
15
  from contextlib import ContextDecorator, contextmanager, suppress
16
16
  from dataclasses import dataclass, field
17
17
  from enum import Enum, auto
18
- from functools import singledispatchmethod, wraps
19
- from inspect import Signature, get_annotations, isclass, isfunction
18
+ from functools import partialmethod, singledispatchmethod, wraps
19
+ from inspect import Signature, isclass
20
20
  from threading import RLock
21
21
  from types import MappingProxyType, UnionType
22
22
  from typing import (
@@ -26,13 +26,12 @@ from typing import (
26
26
  Protocol,
27
27
  TypeVar,
28
28
  cast,
29
- final,
30
29
  runtime_checkable,
31
30
  )
32
31
 
33
32
  from injection.common.event import Event, EventChannel, EventListener
34
33
  from injection.common.lazy import Lazy, LazyMapping
35
- from injection.common.tools import format_type, get_origins
34
+ from injection.common.tools import find_types, format_type, get_origins
36
35
  from injection.exceptions import (
37
36
  ModuleError,
38
37
  ModuleLockError,
@@ -46,6 +45,7 @@ _logger = logging.getLogger(__name__)
46
45
  _thread_lock = RLock()
47
46
 
48
47
  _T = TypeVar("_T")
48
+ Types = Iterable[type] | UnionType
49
49
 
50
50
 
51
51
  """
@@ -132,6 +132,9 @@ Injectables
132
132
  class Injectable(Protocol[_T]):
133
133
  __slots__ = ()
134
134
 
135
+ def __init__(self, factory: Callable[[], _T] = ..., *args, **kwargs):
136
+ ...
137
+
135
138
  @property
136
139
  def is_locked(self) -> bool:
137
140
  return False
@@ -211,7 +214,7 @@ class Container:
211
214
  def __injectables(self) -> frozenset[Injectable]:
212
215
  return frozenset(self.__data.values())
213
216
 
214
- def update(self, classes: Iterable[type] | UnionType, injectable: Injectable):
217
+ def update(self, classes: Types, injectable: Injectable):
215
218
  classes = frozenset(get_origins(*classes))
216
219
 
217
220
  if classes:
@@ -287,18 +290,6 @@ class Module(EventListener):
287
290
  def __str__(self) -> str:
288
291
  return self.name or object.__str__(self)
289
292
 
290
- @property
291
- def inject(self) -> InjectDecorator:
292
- return InjectDecorator(self)
293
-
294
- @property
295
- def injectable(self) -> InjectableDecorator:
296
- return InjectableDecorator(self, NewInjectable)
297
-
298
- @property
299
- def singleton(self) -> InjectableDecorator:
300
- return InjectableDecorator(self, SingletonInjectable)
301
-
302
293
  @property
303
294
  def is_locked(self) -> bool:
304
295
  return any(broker.is_locked for broker in self.__brokers)
@@ -308,7 +299,58 @@ class Module(EventListener):
308
299
  yield from tuple(self.__modules)
309
300
  yield self.__container
310
301
 
311
- def get_instance(self, cls: type[_T] | UnionType) -> _T | None:
302
+ def injectable(
303
+ self,
304
+ wrapped: Callable[..., Any] = None,
305
+ /,
306
+ *,
307
+ cls: type[Injectable] = NewInjectable,
308
+ on: type | Types = None,
309
+ ):
310
+ def decorator(wp):
311
+ factory = self.inject(wp, return_factory=True)
312
+ injectable = cls(factory)
313
+ classes = find_types(wp, on)
314
+ self.update(classes, injectable)
315
+ return wp
316
+
317
+ return decorator(wrapped) if wrapped else decorator
318
+
319
+ singleton = partialmethod(injectable, cls=SingletonInjectable)
320
+
321
+ def set_constant(self, instance: _T, on: type | Types = None) -> _T:
322
+ cls = type(instance)
323
+
324
+ @self.injectable(on=(cls, on))
325
+ def get_constant():
326
+ return instance
327
+
328
+ return instance
329
+
330
+ def inject(
331
+ self,
332
+ wrapped: Callable[..., Any] = None,
333
+ /,
334
+ *,
335
+ return_factory: bool = False,
336
+ ):
337
+ def decorator(wp):
338
+ if not return_factory and isclass(wp):
339
+ wp.__init__ = decorator(wp.__init__)
340
+ return wp
341
+
342
+ lazy_binder = Lazy[Binder](lambda: self.__new_binder(wp))
343
+
344
+ @wraps(wp)
345
+ def wrapper(*args, **kwargs):
346
+ arguments = (~lazy_binder).bind(*args, **kwargs)
347
+ return wp(*arguments.args, **arguments.kwargs)
348
+
349
+ return wrapper
350
+
351
+ return decorator(wrapped) if wrapped else decorator
352
+
353
+ def get_instance(self, cls: type[_T]) -> _T | None:
312
354
  try:
313
355
  injectable = self[cls]
314
356
  except KeyError:
@@ -317,10 +359,10 @@ class Module(EventListener):
317
359
  instance = injectable.get_instance()
318
360
  return cast(cls, instance)
319
361
 
320
- def get_lazy_instance(self, cls: type[_T] | UnionType) -> Lazy[_T | None]:
362
+ def get_lazy_instance(self, cls: type[_T]) -> Lazy[_T | None]:
321
363
  return Lazy(lambda: self.get_instance(cls))
322
364
 
323
- def update(self, classes: Iterable[type] | UnionType, injectable: Injectable):
365
+ def update(self, classes: Types, injectable: Injectable):
324
366
  self.__container.update(classes, injectable)
325
367
  return self
326
368
 
@@ -410,6 +452,12 @@ class Module(EventListener):
410
452
  f"`{module}` can't be found in the modules used by `{self}`."
411
453
  ) from exc
412
454
 
455
+ def __new_binder(self, target: Callable[..., Any]) -> Binder:
456
+ signature = inspect.signature(target, eval_str=True)
457
+ binder = Binder(signature).update(self)
458
+ self.add_listener(binder)
459
+ return binder
460
+
413
461
 
414
462
  """
415
463
  Binder
@@ -492,95 +540,3 @@ class Binder(EventListener):
492
540
  def _(self, event: ModuleEvent, /) -> ContextManager:
493
541
  yield
494
542
  self.update(event.on_module)
495
-
496
-
497
- """
498
- Decorators
499
- """
500
-
501
-
502
- @final
503
- @dataclass(repr=False, frozen=True, slots=True)
504
- class InjectDecorator:
505
- __module: Module
506
-
507
- def __call__(self, wrapped: Callable[..., Any] = None, /):
508
- def decorator(wp):
509
- if isclass(wp):
510
- return self.__class_decorator(wp)
511
-
512
- return self.__decorator(wp)
513
-
514
- return decorator(wrapped) if wrapped else decorator
515
-
516
- def __decorator(self, function: Callable[..., Any], /) -> Callable[..., Any]:
517
- lazy_binder = Lazy[Binder](lambda: self.__new_binder(function))
518
-
519
- @wraps(function)
520
- def wrapper(*args, **kwargs):
521
- arguments = (~lazy_binder).bind(*args, **kwargs)
522
- return function(*arguments.args, **arguments.kwargs)
523
-
524
- return wrapper
525
-
526
- def __class_decorator(self, cls: type, /) -> type:
527
- cls.__init__ = self.__decorator(cls.__init__)
528
- return cls
529
-
530
- def __new_binder(self, function: Callable[..., Any]) -> Binder:
531
- signature = inspect.signature(function, eval_str=True)
532
- binder = Binder(signature).update(self.__module)
533
- self.__module.add_listener(binder)
534
- return binder
535
-
536
-
537
- @final
538
- @dataclass(repr=False, frozen=True, slots=True)
539
- class InjectableDecorator:
540
- __module: Module
541
- __injectable_type: type[BaseInjectable]
542
-
543
- def __repr__(self) -> str:
544
- return f"<{self.__injectable_type.__qualname__} decorator>"
545
-
546
- def __call__(
547
- self,
548
- wrapped: Callable[..., Any] = None,
549
- /,
550
- *,
551
- on: type | Iterable[type] | UnionType = None,
552
- ):
553
- def decorator(wp):
554
- @lambda fn: fn()
555
- def classes():
556
- if cls := self.__get_class(wp):
557
- yield cls
558
-
559
- if on is None:
560
- return
561
- elif isinstance(on, type | str):
562
- yield on
563
- else:
564
- yield from on
565
-
566
- @self.__module.inject
567
- @wraps(wp, updated=())
568
- def factory(*args, **kwargs):
569
- return wp(*args, **kwargs)
570
-
571
- injectable = self.__injectable_type(factory)
572
- self.__module.update(classes, injectable)
573
-
574
- return wp
575
-
576
- return decorator(wrapped) if wrapped else decorator
577
-
578
- @staticmethod
579
- def __get_class(wrapped: Callable[..., Any]) -> type | None:
580
- if isclass(wrapped):
581
- return wrapped
582
-
583
- if isfunction(wrapped):
584
- return get_annotations(wrapped, eval_str=True).get("return")
585
-
586
- return None
@@ -1,6 +1,6 @@
1
1
  [tool.poetry]
2
2
  name = "python-injection"
3
- version = "0.6.7"
3
+ version = "0.6.9"
4
4
  description = "Fast and easy dependency injection framework."
5
5
  authors = ["remimd"]
6
6
  keywords = ["dependencies", "inject", "injection"]
@@ -14,7 +14,7 @@ python = ">=3.10, <4"
14
14
 
15
15
  [tool.poetry.group.dev.dependencies]
16
16
  black = "*"
17
- blacksheep = "^2.0.4"
17
+ blacksheep = "^2.0.5"
18
18
  flake8 = "*"
19
19
  isort = "*"
20
20
  pydantic = "^2.5.3"
@@ -1,35 +0,0 @@
1
- from types import NoneType, UnionType
2
- from typing import Any, Iterator, Union, get_args
3
-
4
- __all__ = ("format_type", "get_origins")
5
-
6
-
7
- def format_type(cls: type | Any) -> str:
8
- try:
9
- return f"{cls.__module__}.{cls.__qualname__}"
10
- except AttributeError:
11
- return str(cls)
12
-
13
-
14
- def get_full_origin(cls: type | Any) -> type | Any:
15
- try:
16
- origin = cls.__origin__
17
- except AttributeError:
18
- return cls
19
-
20
- return get_full_origin(origin)
21
-
22
-
23
- def get_origins(*classes: type | Any) -> Iterator[type | Any]:
24
- for cls in classes:
25
- if cls in (None, NoneType):
26
- continue
27
-
28
- origin = get_full_origin(cls)
29
-
30
- if origin is Union or isinstance(cls, UnionType):
31
- for argument in get_args(cls):
32
- yield from get_origins(argument)
33
-
34
- else:
35
- yield origin