python-injection 0.13.2__tar.gz → 0.14.0__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.
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"