python-injection 0.7.4__tar.gz → 0.7.5__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.

Files changed (19) hide show
  1. {python_injection-0.7.4 → python_injection-0.7.5}/PKG-INFO +1 -1
  2. {python_injection-0.7.4 → python_injection-0.7.5}/injection/_pkg.pyi +1 -9
  3. {python_injection-0.7.4 → python_injection-0.7.5}/injection/common/event.py +3 -1
  4. {python_injection-0.7.4 → python_injection-0.7.5}/injection/common/lazy.py +27 -23
  5. python_injection-0.7.5/injection/common/tools/threading.py +28 -0
  6. {python_injection-0.7.4 → python_injection-0.7.5}/injection/core/module.py +107 -46
  7. {python_injection-0.7.4 → python_injection-0.7.5}/injection/exceptions.py +1 -1
  8. python_injection-0.7.5/injection/integrations/__init__.py +0 -0
  9. {python_injection-0.7.4 → python_injection-0.7.5}/pyproject.toml +1 -1
  10. python_injection-0.7.4/injection/common/tools/__init__.py +0 -1
  11. {python_injection-0.7.4 → python_injection-0.7.5}/documentation/basic-usage.md +0 -0
  12. {python_injection-0.7.4 → python_injection-0.7.5}/injection/__init__.py +0 -0
  13. {python_injection-0.7.4 → python_injection-0.7.5}/injection/_pkg.py +0 -0
  14. {python_injection-0.7.4 → python_injection-0.7.5}/injection/common/__init__.py +0 -0
  15. {python_injection-0.7.4/injection/integrations → python_injection-0.7.5/injection/common/tools}/__init__.py +0 -0
  16. /python_injection-0.7.4/injection/common/tools/_type.py → /python_injection-0.7.5/injection/common/tools/type.py +0 -0
  17. {python_injection-0.7.4 → python_injection-0.7.5}/injection/core/__init__.py +0 -0
  18. {python_injection-0.7.4 → python_injection-0.7.5}/injection/integrations/blacksheep.py +0 -0
  19. {python_injection-0.7.4 → python_injection-0.7.5}/injection/utils.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: python-injection
3
- Version: 0.7.4
3
+ Version: 0.7.5
4
4
  Summary: Fast and easy dependency injection framework.
5
5
  Home-page: https://github.com/100nm/python-injection
6
6
  License: MIT
@@ -39,19 +39,11 @@ class Module:
39
39
 
40
40
  def __init__(self, name: str = ...): ...
41
41
  def __contains__(self, cls: type | UnionType, /) -> bool: ...
42
- def inject(
43
- self,
44
- wrapped: Callable[..., Any] = ...,
45
- /,
46
- *,
47
- force: bool = ...,
48
- ):
42
+ def inject(self, wrapped: Callable[..., Any] = ..., /):
49
43
  """
50
44
  Decorator applicable to a class or function. Inject function dependencies using
51
45
  parameter type annotations. If applied to a class, the dependencies resolved
52
46
  will be those of the `__init__` method.
53
-
54
- With `force=True`, parameters passed to replace dependencies will be ignored.
55
47
  """
56
48
 
57
49
  def injectable(
@@ -4,6 +4,8 @@ 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
+
7
9
  __all__ = ("Event", "EventChannel", "EventListener")
8
10
 
9
11
 
@@ -26,7 +28,7 @@ class EventChannel:
26
28
  @contextmanager
27
29
  def dispatch(self, event: Event) -> ContextManager | ContextDecorator:
28
30
  with ExitStack() as stack:
29
- for listener in tuple(self.__listeners):
31
+ for listener in frozen_collection(self.__listeners):
30
32
  context_manager = listener.on_event(event)
31
33
 
32
34
  if context_manager is None:
@@ -1,12 +1,11 @@
1
1
  from collections.abc import Callable, Iterator, Mapping
2
- from threading import RLock
2
+ from contextlib import suppress
3
3
  from types import MappingProxyType
4
4
  from typing import Any, Generic, TypeVar
5
5
 
6
- __all__ = ("Lazy", "LazyMapping")
6
+ from injection.common.tools.threading import thread_lock
7
7
 
8
- _sentinel = object()
9
- _thread_lock = RLock()
8
+ __all__ = ("Lazy", "LazyMapping")
10
9
 
11
10
  _T = TypeVar("_T")
12
11
  _K = TypeVar("_K")
@@ -14,39 +13,40 @@ _V = TypeVar("_V")
14
13
 
15
14
 
16
15
  class Lazy(Generic[_T]):
17
- __slots__ = ("__factory", "__value")
16
+ __slots__ = ("is_set", "__generator")
18
17
 
19
18
  def __init__(self, factory: Callable[[], _T]):
20
- self.__factory = factory
21
- self.__value = _sentinel
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()
22
32
 
23
33
  def __invert__(self) -> _T:
24
- if not self.is_set:
25
- with _thread_lock:
26
- self.__value = self.__factory()
27
- self.__factory = _sentinel
34
+ return next(self.__generator)
28
35
 
29
- return self.__value
36
+ def __call__(self) -> _T:
37
+ return ~self
30
38
 
31
39
  def __setattr__(self, name: str, value: Any, /):
32
- if self.is_set:
33
- raise TypeError(f"`{self}` is frozen.")
40
+ with suppress(AttributeError):
41
+ if self.is_set:
42
+ raise TypeError(f"`{self}` is frozen.")
34
43
 
35
44
  return super().__setattr__(name, value)
36
45
 
37
- @property
38
- def is_set(self) -> bool:
39
- try:
40
- return self.__factory is _sentinel
41
- except AttributeError:
42
- return False
43
-
44
46
 
45
47
  class LazyMapping(Mapping[_K, _V]):
46
48
  __slots__ = ("__lazy",)
47
49
 
48
- __lazy: Lazy[MappingProxyType[_K, _V]]
49
-
50
50
  def __init__(self, iterator: Iterator[tuple[_K, _V]]):
51
51
  self.__lazy = Lazy(lambda: MappingProxyType(dict(iterator)))
52
52
 
@@ -58,3 +58,7 @@ class LazyMapping(Mapping[_K, _V]):
58
58
 
59
59
  def __len__(self) -> int:
60
60
  return len(~self.__lazy)
61
+
62
+ @property
63
+ def is_set(self) -> bool:
64
+ return self.__lazy.is_set
@@ -0,0 +1,28 @@
1
+ from collections.abc import Callable, Collection, Iterator
2
+ from functools import wraps
3
+ from threading import RLock
4
+ from typing import Any, TypeVar
5
+
6
+ __all__ = ("frozen_collection", "synchronized", "thread_lock")
7
+
8
+ _T = TypeVar("_T")
9
+ thread_lock = RLock()
10
+
11
+
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
@@ -16,12 +16,17 @@ from collections.abc import (
16
16
  from contextlib import ContextDecorator, contextmanager, suppress
17
17
  from dataclasses import dataclass, field
18
18
  from enum import Enum, auto
19
- from functools import partialmethod, singledispatchmethod, wraps
19
+ from functools import (
20
+ partialmethod,
21
+ singledispatchmethod,
22
+ update_wrapper,
23
+ wraps,
24
+ )
20
25
  from inspect import Signature, isclass
21
- from threading import RLock
22
- from types import MappingProxyType, UnionType
26
+ from types import UnionType
23
27
  from typing import (
24
28
  Any,
29
+ ClassVar,
25
30
  ContextManager,
26
31
  NamedTuple,
27
32
  NoReturn,
@@ -33,7 +38,12 @@ from typing import (
33
38
 
34
39
  from injection.common.event import Event, EventChannel, EventListener
35
40
  from injection.common.lazy import Lazy, LazyMapping
36
- from injection.common.tools import find_types, format_type, get_origins
41
+ from injection.common.tools.threading import (
42
+ frozen_collection,
43
+ synchronized,
44
+ thread_lock,
45
+ )
46
+ from injection.common.tools.type import find_types, format_type, get_origins
37
47
  from injection.exceptions import (
38
48
  InjectionError,
39
49
  ModuleError,
@@ -45,7 +55,6 @@ from injection.exceptions import (
45
55
  __all__ = ("Injectable", "Module", "ModulePriority")
46
56
 
47
57
  _logger = logging.getLogger(__name__)
48
- _thread_lock = RLock()
49
58
 
50
59
  _T = TypeVar("_T")
51
60
  Types = Iterable[type] | UnionType
@@ -166,7 +175,7 @@ class NewInjectable(BaseInjectable[_T]):
166
175
  class SingletonInjectable(BaseInjectable[_T]):
167
176
  __slots__ = ("__dict__",)
168
177
 
169
- __INSTANCE_KEY = "$instance"
178
+ __INSTANCE_KEY: ClassVar[str] = "$instance"
170
179
 
171
180
  @property
172
181
  def cache(self) -> MutableMapping[str, Any]:
@@ -183,7 +192,7 @@ class SingletonInjectable(BaseInjectable[_T]):
183
192
  with suppress(KeyError):
184
193
  return self.cache[self.__INSTANCE_KEY]
185
194
 
186
- with _thread_lock:
195
+ with thread_lock:
187
196
  instance = self.factory()
188
197
  self.cache[self.__INSTANCE_KEY] = instance
189
198
 
@@ -263,7 +272,7 @@ class Container(Broker):
263
272
  def update(self, classes: Iterable[type], injectable: Injectable, override: bool):
264
273
  classes = frozenset(get_origins(*classes))
265
274
 
266
- with _thread_lock:
275
+ with thread_lock:
267
276
  if not injectable:
268
277
  classes -= self.__classes
269
278
  override = True
@@ -279,6 +288,7 @@ class Container(Broker):
279
288
 
280
289
  return self
281
290
 
291
+ @synchronized
282
292
  def unlock(self):
283
293
  for injectable in self.__injectables:
284
294
  injectable.unlock()
@@ -349,7 +359,7 @@ class Module(EventListener, Broker):
349
359
 
350
360
  @property
351
361
  def __brokers(self) -> Iterator[Broker]:
352
- yield from tuple(self.__modules)
362
+ yield from frozen_collection(self.__modules)
353
363
  yield self.__container
354
364
 
355
365
  def injectable(
@@ -401,21 +411,15 @@ class Module(EventListener, Broker):
401
411
  wrapped: Callable[..., Any] = None,
402
412
  /,
403
413
  *,
404
- force: bool = False,
405
414
  return_factory: bool = False,
406
415
  ):
407
416
  def decorator(wp):
408
417
  if not return_factory and isclass(wp):
409
- wp.__init__ = self.inject(wp.__init__, force=force)
418
+ wp.__init__ = self.inject(wp.__init__)
410
419
  return wp
411
420
 
412
- lazy_binder = Lazy[Binder](lambda: self.__new_binder(wp))
413
-
414
- @wraps(wp)
415
- def wrapper(*args, **kwargs):
416
- arguments = (~lazy_binder).bind(args, kwargs, force)
417
- return wp(*arguments.args, **arguments.kwargs)
418
-
421
+ wrapper = InjectedFunction(wp).update(self)
422
+ self.add_listener(wrapper)
419
423
  return wrapper
420
424
 
421
425
  return decorator(wrapped) if wrapped else decorator
@@ -492,6 +496,7 @@ class Module(EventListener, Broker):
492
496
 
493
497
  return self
494
498
 
499
+ @synchronized
495
500
  def unlock(self):
496
501
  for broker in self.__brokers:
497
502
  broker.unlock()
@@ -530,44 +535,45 @@ class Module(EventListener, Broker):
530
535
  f"`{module}` can't be found in the modules used by `{self}`."
531
536
  ) from exc
532
537
 
533
- def __new_binder(self, target: Callable[..., Any]) -> Binder:
534
- signature = inspect.signature(target, eval_str=True)
535
- binder = Binder(signature).update(self)
536
- self.add_listener(binder)
537
- return binder
538
-
539
538
 
540
539
  """
541
- Binder
540
+ InjectedFunction
542
541
  """
543
542
 
544
543
 
545
544
  @dataclass(repr=False, frozen=True, slots=True)
546
545
  class Dependencies:
547
- __mapping: MappingProxyType[str, Injectable]
546
+ mapping: Mapping[str, Injectable]
548
547
 
549
548
  def __bool__(self) -> bool:
550
- return bool(self.__mapping)
549
+ return bool(self.mapping)
551
550
 
552
551
  def __iter__(self) -> Iterator[tuple[str, Any]]:
553
- for name, injectable in self.__mapping.items():
552
+ for name, injectable in self.mapping.items():
554
553
  yield name, injectable.get_instance()
555
554
 
555
+ @property
556
+ def are_resolved(self) -> bool:
557
+ if isinstance(self.mapping, LazyMapping) and not self.mapping.is_set:
558
+ return False
559
+
560
+ return bool(self)
561
+
556
562
  @property
557
563
  def arguments(self) -> OrderedDict[str, Any]:
558
564
  return OrderedDict(self)
559
565
 
560
566
  @classmethod
561
567
  def from_mapping(cls, mapping: Mapping[str, Injectable]):
562
- return cls(MappingProxyType(mapping))
568
+ return cls(mapping=mapping)
563
569
 
564
570
  @classmethod
565
571
  def empty(cls):
566
572
  return cls.from_mapping({})
567
573
 
568
574
  @classmethod
569
- def resolve(cls, signature: Signature, module: Module):
570
- dependencies = LazyMapping(cls.__resolver(signature, module))
575
+ def resolve(cls, signature: Signature, module: Module, owner: type = None):
576
+ dependencies = LazyMapping(cls.__resolver(signature, module, owner))
571
577
  return cls.from_mapping(dependencies)
572
578
 
573
579
  @classmethod
@@ -575,33 +581,88 @@ class Dependencies:
575
581
  cls,
576
582
  signature: Signature,
577
583
  module: Module,
584
+ owner: type = None,
578
585
  ) -> Iterator[tuple[str, Injectable]]:
579
- for name, parameter in signature.parameters.items():
586
+ for name, annotation in cls.__get_annotations(signature, owner):
580
587
  try:
581
- injectable = module[parameter.annotation]
588
+ injectable = module[annotation]
582
589
  except KeyError:
583
590
  continue
584
591
 
585
592
  yield name, injectable
586
593
 
594
+ @staticmethod
595
+ def __get_annotations(
596
+ signature: Signature,
597
+ owner: type = None,
598
+ ) -> Iterator[tuple[str, type | Any]]:
599
+ parameters = iter(signature.parameters.items())
600
+
601
+ if owner:
602
+ name, _ = next(parameters)
603
+ yield name, owner
604
+
605
+ for name, parameter in parameters:
606
+ yield name, parameter.annotation
607
+
587
608
 
588
609
  class Arguments(NamedTuple):
589
610
  args: Iterable[Any]
590
611
  kwargs: Mapping[str, Any]
591
612
 
592
613
 
593
- class Binder(EventListener):
594
- __slots__ = ("__signature", "__dependencies")
614
+ class InjectedFunction(EventListener):
615
+ __slots__ = ("__dict__", "__wrapper", "__dependencies", "__owner")
616
+
617
+ def __init__(self, wrapped: Callable[..., Any], /):
618
+ update_wrapper(self, wrapped)
619
+ self.__signature__ = Lazy[Signature](
620
+ lambda: inspect.signature(wrapped, eval_str=True)
621
+ )
595
622
 
596
- def __init__(self, signature: Signature):
597
- self.__signature = signature
623
+ @wraps(wrapped)
624
+ def wrapper(*args, **kwargs):
625
+ args, kwargs = self.bind(args, kwargs)
626
+ return wrapped(*args, **kwargs)
627
+
628
+ self.__wrapper = wrapper
598
629
  self.__dependencies = Dependencies.empty()
630
+ self.__owner = None
631
+
632
+ def __repr__(self) -> str:
633
+ return repr(self.__wrapper)
634
+
635
+ def __str__(self) -> str:
636
+ return str(self.__wrapper)
637
+
638
+ def __call__(self, /, *args, **kwargs) -> Any:
639
+ return self.__wrapper(*args, **kwargs)
640
+
641
+ def __get__(self, instance: object | None, owner: type):
642
+ if instance is None:
643
+ return self
644
+
645
+ return self.__wrapper.__get__(instance, owner)
646
+
647
+ 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
657
+
658
+ @property
659
+ def signature(self) -> Signature:
660
+ return self.__signature__()
599
661
 
600
662
  def bind(
601
663
  self,
602
664
  args: Iterable[Any] = (),
603
665
  kwargs: Mapping[str, Any] = None,
604
- force: bool = False,
605
666
  ) -> Arguments:
606
667
  if kwargs is None:
607
668
  kwargs = {}
@@ -609,19 +670,19 @@ class Binder(EventListener):
609
670
  if not self.__dependencies:
610
671
  return Arguments(args, kwargs)
611
672
 
612
- bound = self.__signature.bind_partial(*args, **kwargs)
673
+ bound = self.signature.bind_partial(*args, **kwargs)
613
674
  dependencies = self.__dependencies.arguments
614
-
615
- if force:
616
- bound.arguments |= dependencies
617
- else:
618
- bound.arguments = dependencies | bound.arguments
675
+ bound.arguments = dependencies | bound.arguments
619
676
 
620
677
  return Arguments(bound.args, bound.kwargs)
621
678
 
622
679
  def update(self, module: Module):
623
- with _thread_lock:
624
- self.__dependencies = Dependencies.resolve(self.__signature, module)
680
+ with thread_lock:
681
+ self.__dependencies = Dependencies.resolve(
682
+ self.signature,
683
+ module,
684
+ self.__owner,
685
+ )
625
686
 
626
687
  return self
627
688
 
@@ -1,4 +1,4 @@
1
- from injection.common.tools import format_type
1
+ from injection.common.tools.type import format_type
2
2
 
3
3
  __all__ = (
4
4
  "InjectionError",
@@ -1,6 +1,6 @@
1
1
  [tool.poetry]
2
2
  name = "python-injection"
3
- version = "0.7.4"
3
+ version = "0.7.5"
4
4
  description = "Fast and easy dependency injection framework."
5
5
  authors = ["remimd"]
6
6
  keywords = ["dependencies", "inject", "injection"]
@@ -1 +0,0 @@
1
- from ._type import *