python-injection 0.13.2__tar.gz → 0.14.0__tar.gz

Sign up to get free protection for your applications and to get access to all the features.
Files changed (26) hide show
  1. {python_injection-0.13.2 → python_injection-0.14.0}/PKG-INFO +1 -1
  2. {python_injection-0.13.2 → python_injection-0.14.0}/injection/__init__.pyi +12 -1
  3. {python_injection-0.13.2 → python_injection-0.14.0}/injection/_core/module.py +44 -11
  4. {python_injection-0.13.2 → python_injection-0.14.0}/injection/_core/scope.py +11 -14
  5. {python_injection-0.13.2 → python_injection-0.14.0}/injection/utils.py +11 -26
  6. {python_injection-0.13.2 → python_injection-0.14.0}/pyproject.toml +1 -1
  7. {python_injection-0.13.2 → python_injection-0.14.0}/.gitignore +0 -0
  8. {python_injection-0.13.2 → python_injection-0.14.0}/README.md +0 -0
  9. {python_injection-0.13.2 → python_injection-0.14.0}/injection/__init__.py +0 -0
  10. {python_injection-0.13.2 → python_injection-0.14.0}/injection/_core/__init__.py +0 -0
  11. {python_injection-0.13.2 → python_injection-0.14.0}/injection/_core/common/__init__.py +0 -0
  12. {python_injection-0.13.2 → python_injection-0.14.0}/injection/_core/common/asynchronous.py +0 -0
  13. {python_injection-0.13.2 → python_injection-0.14.0}/injection/_core/common/event.py +0 -0
  14. {python_injection-0.13.2 → python_injection-0.14.0}/injection/_core/common/invertible.py +0 -0
  15. {python_injection-0.13.2 → python_injection-0.14.0}/injection/_core/common/key.py +0 -0
  16. {python_injection-0.13.2 → python_injection-0.14.0}/injection/_core/common/lazy.py +0 -0
  17. {python_injection-0.13.2 → python_injection-0.14.0}/injection/_core/common/type.py +0 -0
  18. {python_injection-0.13.2 → python_injection-0.14.0}/injection/_core/descriptors.py +0 -0
  19. {python_injection-0.13.2 → python_injection-0.14.0}/injection/_core/hook.py +0 -0
  20. {python_injection-0.13.2 → python_injection-0.14.0}/injection/_core/injectables.py +0 -0
  21. {python_injection-0.13.2 → python_injection-0.14.0}/injection/exceptions.py +0 -0
  22. {python_injection-0.13.2 → python_injection-0.14.0}/injection/integrations/__init__.py +0 -0
  23. {python_injection-0.13.2 → python_injection-0.14.0}/injection/integrations/fastapi.py +0 -0
  24. {python_injection-0.13.2 → python_injection-0.14.0}/injection/py.typed +0 -0
  25. {python_injection-0.13.2 → python_injection-0.14.0}/injection/testing/__init__.py +0 -0
  26. {python_injection-0.13.2 → python_injection-0.14.0}/injection/testing/__init__.pyi +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: python-injection
3
- Version: 0.13.2
3
+ Version: 0.14.0
4
4
  Summary: Fast and easy dependency injection framework.
5
5
  Project-URL: Repository, https://github.com/100nm/python-injection
6
6
  Author: remimd
@@ -74,11 +74,19 @@ class Module:
74
74
  def __contains__(self, cls: _InputType[Any], /) -> bool: ...
75
75
  @property
76
76
  def is_locked(self) -> bool: ...
77
- def inject[**P, T](self, wrapped: Callable[P, T] = ..., /) -> Any:
77
+ def inject[**P, T](
78
+ self,
79
+ wrapped: Callable[P, T] = ...,
80
+ /,
81
+ *,
82
+ threadsafe: bool = ...,
83
+ ) -> Any:
78
84
  """
79
85
  Decorator applicable to a class or function. Inject function dependencies using
80
86
  parameter type annotations. If applied to a class, the dependencies resolved
81
87
  will be those of the `__init__` method.
88
+
89
+ With `threadsafe=True`, the injection logic is wrapped in a `threading.Lock`.
82
90
  """
83
91
 
84
92
  def injectable[**P, T](
@@ -166,6 +174,7 @@ class Module:
166
174
  self,
167
175
  wrapped: Callable[P, T],
168
176
  /,
177
+ threadsafe: bool = ...,
169
178
  ) -> Callable[P, T]: ...
170
179
  async def afind_instance[T](self, cls: _InputType[T]) -> T: ...
171
180
  def find_instance[T](self, cls: _InputType[T]) -> T:
@@ -295,6 +304,8 @@ class Module:
295
304
  Function to unlock the module by deleting cached instances of singletons.
296
305
  """
297
306
 
307
+ @contextmanager
308
+ def load_profile(self, *names: str) -> Iterator[None]: ...
298
309
  async def all_ready(self) -> None: ...
299
310
  def add_logger(self, logger: Logger) -> Self: ...
300
311
  @classmethod
@@ -11,7 +11,7 @@ from collections.abc import (
11
11
  Iterator,
12
12
  Mapping,
13
13
  )
14
- from contextlib import asynccontextmanager, contextmanager, suppress
14
+ from contextlib import asynccontextmanager, contextmanager, nullcontext, suppress
15
15
  from dataclasses import dataclass, field
16
16
  from enum import StrEnum
17
17
  from functools import partialmethod, singledispatchmethod, update_wrapper
@@ -25,6 +25,7 @@ from inspect import (
25
25
  )
26
26
  from inspect import signature as inspect_signature
27
27
  from logging import Logger, getLogger
28
+ from threading import Lock
28
29
  from types import MethodType
29
30
  from typing import (
30
31
  Any,
@@ -542,13 +543,19 @@ class Module(Broker, EventListener):
542
543
  )
543
544
  return self
544
545
 
545
- def inject[**P, T](self, wrapped: Callable[P, T] | None = None, /) -> Any:
546
+ def inject[**P, T](
547
+ self,
548
+ wrapped: Callable[P, T] | None = None,
549
+ /,
550
+ *,
551
+ threadsafe: bool = False,
552
+ ) -> Any:
546
553
  def decorator(wp: Callable[P, T]) -> Callable[P, T]:
547
554
  if isclass(wp):
548
- wp.__init__ = self.inject(wp.__init__)
555
+ wp.__init__ = self.inject(wp.__init__, threadsafe=threadsafe)
549
556
  return wp
550
557
 
551
- return self.make_injected_function(wp)
558
+ return self.make_injected_function(wp, threadsafe)
552
559
 
553
560
  return decorator(wrapped) if wrapped else decorator
554
561
 
@@ -557,6 +564,7 @@ class Module(Broker, EventListener):
557
564
  self,
558
565
  wrapped: Callable[P, T],
559
566
  /,
567
+ threadsafe: bool = ...,
560
568
  ) -> SyncInjectedFunction[P, T]: ...
561
569
 
562
570
  @overload
@@ -564,10 +572,11 @@ class Module(Broker, EventListener):
564
572
  self,
565
573
  wrapped: Callable[P, Awaitable[T]],
566
574
  /,
575
+ threadsafe: bool = ...,
567
576
  ) -> AsyncInjectedFunction[P, T]: ...
568
577
 
569
- def make_injected_function(self, wrapped, /): # type: ignore[no-untyped-def]
570
- metadata = InjectMetadata(wrapped)
578
+ def make_injected_function(self, wrapped, /, threadsafe=False): # type: ignore[no-untyped-def]
579
+ metadata = InjectMetadata(wrapped, threadsafe)
571
580
 
572
581
  @metadata.task
573
582
  def listen() -> None:
@@ -753,6 +762,23 @@ class Module(Broker, EventListener):
753
762
 
754
763
  return self
755
764
 
765
+ def load_profile(self, *names: str) -> ContextManager[None]:
766
+ modules = tuple(self.from_name(name) for name in names)
767
+
768
+ for module in modules:
769
+ module.unlock()
770
+
771
+ self.unlock().init_modules(*modules)
772
+
773
+ del module, modules
774
+
775
+ @contextmanager
776
+ def cleaner() -> Iterator[None]:
777
+ yield
778
+ self.unlock().init_modules()
779
+
780
+ return cleaner()
781
+
756
782
  async def all_ready(self) -> None:
757
783
  for broker in self.__brokers:
758
784
  await broker.all_ready()
@@ -913,6 +939,7 @@ class Arguments(NamedTuple):
913
939
  class InjectMetadata[**P, T](Caller[P, T], EventListener):
914
940
  __slots__ = (
915
941
  "__dependencies",
942
+ "__lock",
916
943
  "__owner",
917
944
  "__signature",
918
945
  "__tasks",
@@ -920,13 +947,15 @@ class InjectMetadata[**P, T](Caller[P, T], EventListener):
920
947
  )
921
948
 
922
949
  __dependencies: Dependencies
950
+ __lock: ContextManager[Any]
923
951
  __owner: type | None
924
952
  __signature: Signature
925
953
  __tasks: deque[Callable[..., Any]]
926
954
  __wrapped: Callable[P, T]
927
955
 
928
- def __init__(self, wrapped: Callable[P, T], /) -> None:
956
+ def __init__(self, wrapped: Callable[P, T], /, threadsafe: bool) -> None:
929
957
  self.__dependencies = Dependencies.empty()
958
+ self.__lock = Lock() if threadsafe else nullcontext()
930
959
  self.__owner = None
931
960
  self.__tasks = deque()
932
961
  self.__wrapped = wrapped
@@ -961,13 +990,17 @@ class InjectMetadata[**P, T](Caller[P, T], EventListener):
961
990
  return self.__bind(args, kwargs, additional_arguments)
962
991
 
963
992
  async def acall(self, /, *args: P.args, **kwargs: P.kwargs) -> T:
964
- self.__run_tasks()
965
- arguments = await self.abind(args, kwargs)
993
+ with self.__lock:
994
+ self.__run_tasks()
995
+ arguments = await self.abind(args, kwargs)
996
+
966
997
  return self.wrapped(*arguments.args, **arguments.kwargs)
967
998
 
968
999
  def call(self, /, *args: P.args, **kwargs: P.kwargs) -> T:
969
- self.__run_tasks()
970
- arguments = self.bind(args, kwargs)
1000
+ with self.__lock:
1001
+ self.__run_tasks()
1002
+ arguments = self.bind(args, kwargs)
1003
+
971
1004
  return self.wrapped(*arguments.args, **arguments.kwargs)
972
1005
 
973
1006
  def set_owner(self, owner: type) -> Self:
@@ -34,12 +34,12 @@ class _ScopeState:
34
34
  default_factory=lambda: ContextVar(f"scope@{new_short_key()}"),
35
35
  init=False,
36
36
  )
37
- __references: set[Scope] = field(
38
- default_factory=set,
37
+ __default: Scope | None = field(
38
+ default=None,
39
39
  init=False,
40
40
  )
41
- __shared_value: Scope | None = field(
42
- default=None,
41
+ __references: set[Scope] = field(
42
+ default_factory=set,
43
43
  init=False,
44
44
  )
45
45
 
@@ -47,8 +47,8 @@ class _ScopeState:
47
47
  def active_scopes(self) -> Iterator[Scope]:
48
48
  yield from self.__references
49
49
 
50
- if shared_value := self.__shared_value:
51
- yield shared_value
50
+ if default := self.__default:
51
+ yield default
52
52
 
53
53
  @contextmanager
54
54
  def bind_contextual_scope(self, scope: Scope) -> Iterator[None]:
@@ -69,15 +69,15 @@ class _ScopeState:
69
69
  "are defined on the same name."
70
70
  )
71
71
 
72
- self.__shared_value = scope
72
+ self.__default = scope
73
73
 
74
74
  try:
75
75
  yield
76
76
  finally:
77
- self.__shared_value = None
77
+ self.__default = None
78
78
 
79
79
  def get_scope(self) -> Scope | None:
80
- return self.__context_var.get(self.__shared_value)
80
+ return self.__context_var.get(self.__default)
81
81
 
82
82
 
83
83
  __SCOPES: Final[defaultdict[str, _ScopeState]] = defaultdict(_ScopeState)
@@ -125,11 +125,8 @@ def _bind_scope(name: str, scope: Scope, shared: bool) -> Iterator[None]:
125
125
  state.bind_shared_scope(scope) if shared else state.bind_contextual_scope(scope)
126
126
  )
127
127
 
128
- try:
129
- with strategy:
130
- yield
131
- finally:
132
- scope.cache.clear()
128
+ with strategy:
129
+ yield
133
130
 
134
131
 
135
132
  @runtime_checkable
@@ -1,5 +1,4 @@
1
- from collections.abc import Callable, Iterable, Iterator
2
- from contextlib import contextmanager
1
+ from collections.abc import Callable, Collection, Iterator
3
2
  from importlib import import_module
4
3
  from importlib.util import find_spec
5
4
  from pkgutil import walk_packages
@@ -18,26 +17,12 @@ def load_profile(*names: str) -> ContextManager[None]:
18
17
  A profile name is equivalent to an injection module name.
19
18
  """
20
19
 
21
- modules = tuple(mod(module_name) for module_name in names)
22
-
23
- for module in modules:
24
- module.unlock()
25
-
26
- target = mod().unlock().init_modules(*modules)
27
-
28
- del module, modules
29
-
30
- @contextmanager
31
- def cleaner() -> Iterator[None]:
32
- yield
33
- target.unlock().init_modules()
34
-
35
- return cleaner()
20
+ return mod().load_profile(*names)
36
21
 
37
22
 
38
23
  def load_modules_with_keywords(
39
24
  *packages: PythonModule | str,
40
- keywords: Iterable[str] | None = None,
25
+ keywords: Collection[str] | None = None,
41
26
  ) -> dict[str, PythonModule]:
42
27
  """
43
28
  Function to import modules from a Python package if one of the keywords is contained in the Python script.
@@ -54,16 +39,16 @@ def load_modules_with_keywords(
54
39
  f"import {injection_package_name}",
55
40
  )
56
41
 
57
- b_keywords = tuple(keyword.encode() for keyword in keywords)
58
-
59
42
  def predicate(module_name: str) -> bool:
60
- if (spec := find_spec(module_name)) and (module_path := spec.origin):
61
- with open(module_path, "rb") as script:
62
- for line in script:
63
- line = b" ".join(line.split(b" ")).strip()
43
+ spec = find_spec(module_name)
44
+
45
+ if spec and (module_path := spec.origin):
46
+ with open(module_path, "r") as file:
47
+ python_script = file.read()
64
48
 
65
- if line and any(keyword in line for keyword in b_keywords):
66
- return True
49
+ return bool(python_script) and any(
50
+ keyword in python_script for keyword in keywords
51
+ )
67
52
 
68
53
  return False
69
54
 
@@ -24,7 +24,7 @@ test = [
24
24
 
25
25
  [project]
26
26
  name = "python-injection"
27
- version = "0.13.2"
27
+ version = "0.14.0"
28
28
  description = "Fast and easy dependency injection framework."
29
29
  license = { text = "MIT" }
30
30
  readme = "README.md"