python-injection 0.7.5__py3-none-any.whl → 0.8.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/_pkg.pyi CHANGED
@@ -13,7 +13,7 @@ from typing import (
13
13
  runtime_checkable,
14
14
  )
15
15
 
16
- from injection.common.lazy import Lazy
16
+ from injection.common.invertible import Invertible
17
17
 
18
18
  _T = TypeVar("_T")
19
19
 
@@ -104,11 +104,16 @@ class Module:
104
104
  will be raised.
105
105
  """
106
106
 
107
- def get_lazy_instance(self, cls: type[_T]) -> Lazy[_T | None]:
107
+ def get_lazy_instance(
108
+ self,
109
+ cls: type[_T],
110
+ cache: bool = ...,
111
+ ) -> Invertible[_T | None]:
108
112
  """
109
113
  Function used to retrieve an instance associated with the type passed in
110
- parameter or `None`. Return a `Lazy` object. To access the instance contained
111
- in a lazy object, simply use a wavy line (~).
114
+ parameter or `None`. Return a `Invertible` object. To access the instance
115
+ contained in an invertible object, simply use a wavy line (~).
116
+ With `cache=True`, the instance retrieved will always be the same.
112
117
 
113
118
  Example: instance = ~lazy_instance
114
119
  """
injection/common/event.py CHANGED
@@ -4,8 +4,6 @@ from dataclasses import dataclass, field
4
4
  from typing import ContextManager
5
5
  from weakref import WeakSet
6
6
 
7
- from injection.common.tools.threading import frozen_collection
8
-
9
7
  __all__ = ("Event", "EventChannel", "EventListener")
10
8
 
11
9
 
@@ -28,7 +26,7 @@ class EventChannel:
28
26
  @contextmanager
29
27
  def dispatch(self, event: Event) -> ContextManager | ContextDecorator:
30
28
  with ExitStack() as stack:
31
- for listener in frozen_collection(self.__listeners):
29
+ for listener in tuple(self.__listeners):
32
30
  context_manager = listener.on_event(event)
33
31
 
34
32
  if context_manager is None:
@@ -0,0 +1,23 @@
1
+ from abc import abstractmethod
2
+ from collections.abc import Callable
3
+ from dataclasses import dataclass
4
+ from typing import Protocol, TypeVar, runtime_checkable
5
+
6
+ __all__ = ("Invertible", "SimpleInvertible")
7
+
8
+ _T = TypeVar("_T")
9
+
10
+
11
+ @runtime_checkable
12
+ class Invertible(Protocol[_T]):
13
+ @abstractmethod
14
+ def __invert__(self) -> _T:
15
+ raise NotImplementedError
16
+
17
+
18
+ @dataclass(repr=False, eq=False, frozen=True, slots=True)
19
+ class SimpleInvertible(Invertible[_T]):
20
+ callable: Callable[[], _T]
21
+
22
+ def __invert__(self) -> _T:
23
+ return self.callable()
injection/common/lazy.py CHANGED
@@ -1,9 +1,8 @@
1
1
  from collections.abc import Callable, Iterator, Mapping
2
- from contextlib import suppress
3
2
  from types import MappingProxyType
4
- from typing import Any, Generic, TypeVar
3
+ from typing import TypeVar
5
4
 
6
- from injection.common.tools.threading import thread_lock
5
+ from injection.common.invertible import Invertible
7
6
 
8
7
  __all__ = ("Lazy", "LazyMapping")
9
8
 
@@ -12,36 +11,31 @@ _K = TypeVar("_K")
12
11
  _V = TypeVar("_V")
13
12
 
14
13
 
15
- class Lazy(Generic[_T]):
16
- __slots__ = ("is_set", "__generator")
14
+ class Lazy(Invertible[_T]):
15
+ __slots__ = ("__cache", "__is_set")
17
16
 
18
17
  def __init__(self, factory: Callable[[], _T]):
19
- def generator() -> Iterator[_T]:
20
- nonlocal factory
21
-
22
- with thread_lock:
23
- value = factory()
24
- self.is_set = True
25
- del factory
26
-
27
- while True:
28
- yield value
29
-
30
- self.is_set = False
31
- self.__generator = generator()
18
+ self.__setup_cache(factory)
32
19
 
33
20
  def __invert__(self) -> _T:
34
- return next(self.__generator)
21
+ return next(self.__cache)
35
22
 
36
- def __call__(self) -> _T:
37
- return ~self
23
+ @property
24
+ def is_set(self) -> bool:
25
+ return self.__is_set
38
26
 
39
- def __setattr__(self, name: str, value: Any, /):
40
- with suppress(AttributeError):
41
- if self.is_set:
42
- raise TypeError(f"`{self}` is frozen.")
27
+ def __setup_cache(self, factory: Callable[[], _T]):
28
+ def cache_generator() -> Iterator[_T]:
29
+ nonlocal factory
30
+ cached = factory()
31
+ self.__is_set = True
32
+ del factory
33
+
34
+ while True:
35
+ yield cached
43
36
 
44
- return super().__setattr__(name, value)
37
+ self.__cache = cache_generator()
38
+ self.__is_set = False
45
39
 
46
40
 
47
41
  class LazyMapping(Mapping[_K, _V]):
@@ -0,0 +1,63 @@
1
+ from abc import abstractmethod
2
+ from collections import deque
3
+ from collections.abc import Iterator
4
+ from dataclasses import dataclass, field
5
+ from typing import NoReturn, Protocol, TypeVar
6
+
7
+ __all__ = ("LimitedQueue",)
8
+
9
+ _T = TypeVar("_T")
10
+
11
+
12
+ class Queue(Iterator[_T], Protocol):
13
+ __slots__ = ()
14
+
15
+ @abstractmethod
16
+ def add(self, item: _T):
17
+ raise NotImplementedError
18
+
19
+
20
+ @dataclass(repr=False, frozen=True, slots=True)
21
+ class SimpleQueue(Queue[_T]):
22
+ __items: deque[_T] = field(default_factory=deque, init=False)
23
+
24
+ def __next__(self) -> _T:
25
+ try:
26
+ return self.__items.popleft()
27
+ except IndexError as exc:
28
+ raise StopIteration from exc
29
+
30
+ def add(self, item: _T):
31
+ self.__items.append(item)
32
+ return self
33
+
34
+
35
+ class DeadQueue(Queue[_T]):
36
+ __slots__ = ()
37
+
38
+ def __bool__(self) -> bool:
39
+ return False
40
+
41
+ def __next__(self) -> NoReturn:
42
+ raise StopIteration
43
+
44
+ def add(self, item: _T) -> NoReturn:
45
+ raise TypeError("Queue is dead.")
46
+
47
+
48
+ @dataclass(repr=False, slots=True)
49
+ class LimitedQueue(Queue[_T]):
50
+ __state: Queue[_T] = field(default_factory=SimpleQueue)
51
+
52
+ def __next__(self) -> _T:
53
+ try:
54
+ return next(self.__state)
55
+ except StopIteration as exc:
56
+ if self.__state:
57
+ self.__state = DeadQueue()
58
+
59
+ raise exc
60
+
61
+ def add(self, item: _T):
62
+ self.__state.add(item)
63
+ return self
@@ -1,28 +1,13 @@
1
- from collections.abc import Callable, Collection, Iterator
2
- from functools import wraps
1
+ from contextlib import ContextDecorator, contextmanager
3
2
  from threading import RLock
4
- from typing import Any, TypeVar
3
+ from typing import ContextManager
5
4
 
6
- __all__ = ("frozen_collection", "synchronized", "thread_lock")
5
+ __all__ = ("synchronized",)
7
6
 
8
- _T = TypeVar("_T")
9
- thread_lock = RLock()
7
+ __lock = RLock()
10
8
 
11
9
 
12
- def synchronized(function: Callable[..., Any] = None, /):
13
- def decorator(fn):
14
- @wraps(fn)
15
- def wrapper(*args, **kwargs):
16
- with thread_lock:
17
- return fn(*args, **kwargs)
18
-
19
- return wrapper
20
-
21
- return decorator(function) if function else decorator
22
-
23
-
24
- def frozen_collection(collection: Collection[_T]) -> Iterator[_T]:
25
- with thread_lock:
26
- t = tuple(collection)
27
-
28
- yield from t
10
+ @contextmanager
11
+ def synchronized() -> ContextManager | ContextDecorator:
12
+ with __lock:
13
+ yield
injection/core/module.py CHANGED
@@ -37,12 +37,10 @@ from typing import (
37
37
  )
38
38
 
39
39
  from injection.common.event import Event, EventChannel, EventListener
40
+ from injection.common.invertible import Invertible, SimpleInvertible
40
41
  from injection.common.lazy import Lazy, LazyMapping
41
- from injection.common.tools.threading import (
42
- frozen_collection,
43
- synchronized,
44
- thread_lock,
45
- )
42
+ from injection.common.queue import LimitedQueue
43
+ from injection.common.tools.threading import synchronized
46
44
  from injection.common.tools.type import find_types, format_type, get_origins
47
45
  from injection.exceptions import (
48
46
  InjectionError,
@@ -160,6 +158,13 @@ class Injectable(Protocol[_T]):
160
158
  raise NotImplementedError
161
159
 
162
160
 
161
+ class FallbackInjectable(Injectable[_T], ABC):
162
+ __slots__ = ()
163
+
164
+ def __bool__(self) -> bool:
165
+ return False
166
+
167
+
163
168
  @dataclass(repr=False, frozen=True, slots=True)
164
169
  class BaseInjectable(Injectable[_T], ABC):
165
170
  factory: Callable[[], _T]
@@ -192,7 +197,7 @@ class SingletonInjectable(BaseInjectable[_T]):
192
197
  with suppress(KeyError):
193
198
  return self.cache[self.__INSTANCE_KEY]
194
199
 
195
- with thread_lock:
200
+ with synchronized():
196
201
  instance = self.factory()
197
202
  self.cache[self.__INSTANCE_KEY] = instance
198
203
 
@@ -200,12 +205,9 @@ class SingletonInjectable(BaseInjectable[_T]):
200
205
 
201
206
 
202
207
  @dataclass(repr=False, frozen=True, slots=True)
203
- class ShouldBeInjectable(Injectable[_T]):
208
+ class ShouldBeInjectable(FallbackInjectable[_T]):
204
209
  cls: type[_T]
205
210
 
206
- def __bool__(self) -> bool:
207
- return False
208
-
209
211
  def get_instance(self) -> NoReturn:
210
212
  raise InjectionError(f"`{format_type(self.cls)}` should be an injectable.")
211
213
 
@@ -263,36 +265,38 @@ class Container(Broker):
263
265
 
264
266
  @property
265
267
  def __classes(self) -> frozenset[type]:
266
- return frozenset(self.__data.keys())
268
+ return frozenset(self.__data)
267
269
 
268
270
  @property
269
271
  def __injectables(self) -> frozenset[Injectable]:
270
272
  return frozenset(self.__data.values())
271
273
 
274
+ @synchronized()
272
275
  def update(self, classes: Iterable[type], injectable: Injectable, override: bool):
273
276
  classes = frozenset(get_origins(*classes))
274
277
 
275
- with thread_lock:
276
- if not injectable:
277
- classes -= self.__classes
278
- override = True
278
+ if not injectable:
279
+ classes -= self.__classes
280
+ override = True
279
281
 
280
- if classes:
281
- event = ContainerDependenciesUpdated(self, classes, override)
282
+ if classes:
283
+ event = ContainerDependenciesUpdated(self, classes, override)
282
284
 
283
- with self.notify(event):
284
- if not override:
285
- self.__check_if_exists(classes)
285
+ with self.notify(event):
286
+ if not override:
287
+ self.__check_if_exists(classes)
286
288
 
287
- self.__data.update((cls, injectable) for cls in classes)
289
+ self.__data.update((cls, injectable) for cls in classes)
288
290
 
289
291
  return self
290
292
 
291
- @synchronized
293
+ @synchronized()
292
294
  def unlock(self):
293
295
  for injectable in self.__injectables:
294
296
  injectable.unlock()
295
297
 
298
+ return self
299
+
296
300
  def add_listener(self, listener: EventListener):
297
301
  self.__channel.add_listener(listener)
298
302
  return self
@@ -359,7 +363,7 @@ class Module(EventListener, Broker):
359
363
 
360
364
  @property
361
365
  def __brokers(self) -> Iterator[Broker]:
362
- yield from frozen_collection(self.__modules)
366
+ yield from tuple(self.__modules)
363
367
  yield self.__container
364
368
 
365
369
  def injectable(
@@ -418,9 +422,14 @@ class Module(EventListener, Broker):
418
422
  wp.__init__ = self.inject(wp.__init__)
419
423
  return wp
420
424
 
421
- wrapper = InjectedFunction(wp).update(self)
422
- self.add_listener(wrapper)
423
- return wrapper
425
+ function = InjectedFunction(wp)
426
+
427
+ @function.on_setup
428
+ def listen():
429
+ function.update(self)
430
+ self.add_listener(function)
431
+
432
+ return function
424
433
 
425
434
  return decorator(wrapped) if wrapped else decorator
426
435
 
@@ -436,8 +445,17 @@ class Module(EventListener, Broker):
436
445
  instance = injectable.get_instance()
437
446
  return cast(cls, instance)
438
447
 
439
- def get_lazy_instance(self, cls: type[_T]) -> Lazy[_T | None]:
440
- return Lazy(lambda: self.get_instance(cls))
448
+ def get_lazy_instance(
449
+ self,
450
+ cls: type[_T],
451
+ cache: bool = False,
452
+ ) -> Invertible[_T | None]:
453
+ if cache:
454
+ return Lazy(lambda: self.get_instance(cls))
455
+
456
+ function = self.inject(lambda instance=None: instance)
457
+ function.set_owner(cls)
458
+ return SimpleInvertible(function)
441
459
 
442
460
  def update(
443
461
  self,
@@ -496,11 +514,13 @@ class Module(EventListener, Broker):
496
514
 
497
515
  return self
498
516
 
499
- @synchronized
517
+ @synchronized()
500
518
  def unlock(self):
501
519
  for broker in self.__brokers:
502
520
  broker.unlock()
503
521
 
522
+ return self
523
+
504
524
  def add_listener(self, listener: EventListener):
505
525
  self.__channel.add_listener(listener)
506
526
  return self
@@ -612,22 +632,36 @@ class Arguments(NamedTuple):
612
632
 
613
633
 
614
634
  class InjectedFunction(EventListener):
615
- __slots__ = ("__dict__", "__wrapper", "__dependencies", "__owner")
635
+ __slots__ = (
636
+ "__dict__",
637
+ "__signature__",
638
+ "__dependencies",
639
+ "__owner",
640
+ "__setup_queue",
641
+ "__wrapper",
642
+ )
616
643
 
617
644
  def __init__(self, wrapped: Callable[..., Any], /):
618
645
  update_wrapper(self, wrapped)
619
- self.__signature__ = Lazy[Signature](
620
- lambda: inspect.signature(wrapped, eval_str=True)
621
- )
622
646
 
623
647
  @wraps(wrapped)
624
648
  def wrapper(*args, **kwargs):
649
+ self.__consume_setup_queue()
625
650
  args, kwargs = self.bind(args, kwargs)
626
651
  return wrapped(*args, **kwargs)
627
652
 
628
653
  self.__wrapper = wrapper
629
654
  self.__dependencies = Dependencies.empty()
630
655
  self.__owner = None
656
+ self.__setup_queue = LimitedQueue[Callable[[], Any]]()
657
+ self.on_setup(
658
+ lambda: self.__set_signature(
659
+ inspect.signature(
660
+ wrapped,
661
+ eval_str=True,
662
+ )
663
+ )
664
+ )
631
665
 
632
666
  def __repr__(self) -> str:
633
667
  return repr(self.__wrapper)
@@ -638,26 +672,18 @@ class InjectedFunction(EventListener):
638
672
  def __call__(self, /, *args, **kwargs) -> Any:
639
673
  return self.__wrapper(*args, **kwargs)
640
674
 
641
- def __get__(self, instance: object | None, owner: type):
675
+ def __get__(self, instance: object = None, owner: type = None):
642
676
  if instance is None:
643
677
  return self
644
678
 
645
679
  return self.__wrapper.__get__(instance, owner)
646
680
 
647
681
  def __set_name__(self, owner: type, name: str):
648
- if self.__dependencies.are_resolved:
649
- raise TypeError(
650
- "`__set_name__` is called after dependencies have been resolved."
651
- )
652
-
653
- if self.__owner:
654
- raise TypeError("Function owner is already defined.")
655
-
656
- self.__owner = owner
682
+ self.set_owner(owner)
657
683
 
658
684
  @property
659
685
  def signature(self) -> Signature:
660
- return self.__signature__()
686
+ return self.__signature__
661
687
 
662
688
  def bind(
663
689
  self,
@@ -671,21 +697,35 @@ class InjectedFunction(EventListener):
671
697
  return Arguments(args, kwargs)
672
698
 
673
699
  bound = self.signature.bind_partial(*args, **kwargs)
674
- dependencies = self.__dependencies.arguments
675
- bound.arguments = dependencies | bound.arguments
676
-
700
+ bound.arguments = (
701
+ bound.arguments | self.__dependencies.arguments | bound.arguments
702
+ )
677
703
  return Arguments(bound.args, bound.kwargs)
678
704
 
679
- def update(self, module: Module):
680
- with thread_lock:
681
- self.__dependencies = Dependencies.resolve(
682
- self.signature,
683
- module,
684
- self.__owner,
705
+ def set_owner(self, owner: type):
706
+ if self.__dependencies.are_resolved:
707
+ raise TypeError(
708
+ "Function owner must be assigned before dependencies are resolved."
685
709
  )
686
710
 
711
+ if self.__owner:
712
+ raise TypeError("Function owner is already defined.")
713
+
714
+ self.__owner = owner
687
715
  return self
688
716
 
717
+ @synchronized()
718
+ def update(self, module: Module):
719
+ self.__dependencies = Dependencies.resolve(self.signature, module, self.__owner)
720
+ return self
721
+
722
+ def on_setup(self, wrapped: Callable[[], Any] = None, /):
723
+ def decorator(wp):
724
+ self.__setup_queue.add(wp)
725
+ return wp
726
+
727
+ return decorator(wrapped) if wrapped else decorator
728
+
689
729
  @singledispatchmethod
690
730
  def on_event(self, event: Event, /):
691
731
  pass
@@ -695,3 +735,13 @@ class InjectedFunction(EventListener):
695
735
  def _(self, event: ModuleEvent, /) -> ContextManager:
696
736
  yield
697
737
  self.update(event.on_module)
738
+
739
+ def __consume_setup_queue(self):
740
+ for function in self.__setup_queue:
741
+ function()
742
+
743
+ return self
744
+
745
+ def __set_signature(self, signature: Signature):
746
+ self.__signature__ = signature
747
+ return self
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: python-injection
3
- Version: 0.7.5
3
+ Version: 0.8.1
4
4
  Summary: Fast and easy dependency injection framework.
5
5
  Home-page: https://github.com/100nm/python-injection
6
6
  License: MIT
@@ -1,18 +1,20 @@
1
1
  injection/__init__.py,sha256=9_AVJILxKIBiL_6KJSh2RRydgSHXH38uahAGD0S1-dI,20
2
2
  injection/_pkg.py,sha256=nMIRLAQG6096bcR2Mz1egxe5FItBUsw8zR9yzjt1HDM,647
3
- injection/_pkg.pyi,sha256=5IE5vbi_sSeKjcFtUT9UuCWeIIwEi1hy3qmTdZeZSMI,5171
3
+ injection/_pkg.pyi,sha256=7ep6m-8XK4H7XIFkael2UMIG01qYw73zjdv5DtEBwes,5327
4
4
  injection/common/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
5
- injection/common/event.py,sha256=Lq3XTi2iTK9Y51uHtmsxoqeKJsgretqQQAb00HR454c,1392
6
- injection/common/lazy.py,sha256=BmjkcwxI-R4I81lzqF0dHmTlfBtflhGKXeBK9lJmG18,1596
5
+ injection/common/event.py,sha256=uFoGRnxxkohH53JEStn4tN2Pn79HlgGExh7VkXdBwVQ,1316
6
+ injection/common/invertible.py,sha256=CyGp57Ik1pSQ2G7bRvnFWkY0kJkZDD5_19OjMYNvQes,558
7
+ injection/common/lazy.py,sha256=1C34uoG229Gl0DEUcD9-eQrL4K_oIofOLzdQ1SiY6rw,1401
8
+ injection/common/queue.py,sha256=mV0AGxp5aYMr438MxmoIsZcV5jmqi5x_GD2S-utrnzA,1443
7
9
  injection/common/tools/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
8
- injection/common/tools/threading.py,sha256=doz_TQJWmo3qIaSAhFEyCmdVG7RZoXDmdyfbkYS_vgU,688
10
+ injection/common/tools/threading.py,sha256=02mBkIir2kQaXt47ewMJnOvhCldXCmPI2V-BW_l_6V8,271
9
11
  injection/common/tools/type.py,sha256=-zL0dtoVZme71Mscvav7iEWxY2-JltzNTekbWOCPSFo,1276
10
12
  injection/core/__init__.py,sha256=zuf0ubI2dHnbjn1059eduhS-ACIkkROa6-dhp10krh0,22
11
- injection/core/module.py,sha256=J0BpNFlO3MlzBM6-4TPc9nMcSFeTU8eoxbjfcZwLAxM,18084
13
+ injection/core/module.py,sha256=hYnNwFNzw-1TaTjPAtmCpKgghF1JTdbkrJ9jO07KQps,19284
12
14
  injection/exceptions.py,sha256=nE56jW00ZB1T-Z-dvfPczPShs3CwIc7tIvdYlOXlaXA,653
13
15
  injection/integrations/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
14
16
  injection/integrations/blacksheep.py,sha256=vcLil1IccS7JtXpuVu7s2LqN5Zravfe_7xpAt5cTIU0,723
15
17
  injection/utils.py,sha256=_79aiciimZpuoUTz5lojKySUMMzpkU-e7SotiHIFTI8,676
16
- python_injection-0.7.5.dist-info/METADATA,sha256=czSlDaVt9ev1mgzLpQOnrV2wb_Cx6VC1f4js-i-ClzE,3433
17
- python_injection-0.7.5.dist-info/WHEEL,sha256=sP946D7jFCHeNz5Iq4fL4Lu-PrWrFsgfLXbbkciIZwg,88
18
- python_injection-0.7.5.dist-info/RECORD,,
18
+ python_injection-0.8.1.dist-info/METADATA,sha256=Atf1DZFIwYCdMKnK7_xQf_muy_Ma7qQmEAnGYQr5P-M,3433
19
+ python_injection-0.8.1.dist-info/WHEEL,sha256=sP946D7jFCHeNz5Iq4fL4Lu-PrWrFsgfLXbbkciIZwg,88
20
+ python_injection-0.8.1.dist-info/RECORD,,