python-injection 0.13.2__tar.gz → 0.14.0.post0__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.post0}/PKG-INFO +4 -1
  2. {python_injection-0.13.2 → python_injection-0.14.0.post0}/README.md +3 -0
  3. {python_injection-0.13.2 → python_injection-0.14.0.post0}/injection/__init__.pyi +13 -2
  4. {python_injection-0.13.2 → python_injection-0.14.0.post0}/injection/_core/hook.py +4 -3
  5. {python_injection-0.13.2 → python_injection-0.14.0.post0}/injection/_core/module.py +46 -13
  6. {python_injection-0.13.2 → python_injection-0.14.0.post0}/injection/_core/scope.py +12 -18
  7. {python_injection-0.13.2 → python_injection-0.14.0.post0}/injection/testing/__init__.py +2 -2
  8. {python_injection-0.13.2 → python_injection-0.14.0.post0}/injection/testing/__init__.pyi +1 -1
  9. {python_injection-0.13.2 → python_injection-0.14.0.post0}/injection/utils.py +13 -28
  10. {python_injection-0.13.2 → python_injection-0.14.0.post0}/pyproject.toml +1 -1
  11. {python_injection-0.13.2 → python_injection-0.14.0.post0}/.gitignore +0 -0
  12. {python_injection-0.13.2 → python_injection-0.14.0.post0}/injection/__init__.py +0 -0
  13. {python_injection-0.13.2 → python_injection-0.14.0.post0}/injection/_core/__init__.py +0 -0
  14. {python_injection-0.13.2 → python_injection-0.14.0.post0}/injection/_core/common/__init__.py +0 -0
  15. {python_injection-0.13.2 → python_injection-0.14.0.post0}/injection/_core/common/asynchronous.py +0 -0
  16. {python_injection-0.13.2 → python_injection-0.14.0.post0}/injection/_core/common/event.py +0 -0
  17. {python_injection-0.13.2 → python_injection-0.14.0.post0}/injection/_core/common/invertible.py +0 -0
  18. {python_injection-0.13.2 → python_injection-0.14.0.post0}/injection/_core/common/key.py +0 -0
  19. {python_injection-0.13.2 → python_injection-0.14.0.post0}/injection/_core/common/lazy.py +0 -0
  20. {python_injection-0.13.2 → python_injection-0.14.0.post0}/injection/_core/common/type.py +0 -0
  21. {python_injection-0.13.2 → python_injection-0.14.0.post0}/injection/_core/descriptors.py +0 -0
  22. {python_injection-0.13.2 → python_injection-0.14.0.post0}/injection/_core/injectables.py +0 -0
  23. {python_injection-0.13.2 → python_injection-0.14.0.post0}/injection/exceptions.py +0 -0
  24. {python_injection-0.13.2 → python_injection-0.14.0.post0}/injection/integrations/__init__.py +0 -0
  25. {python_injection-0.13.2 → python_injection-0.14.0.post0}/injection/integrations/fastapi.py +0 -0
  26. {python_injection-0.13.2 → python_injection-0.14.0.post0}/injection/py.typed +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.post0
4
4
  Summary: Fast and easy dependency injection framework.
5
5
  Project-URL: Repository, https://github.com/100nm/python-injection
6
6
  Author: remimd
@@ -78,6 +78,9 @@ if __name__ == "__main__":
78
78
 
79
79
  ## Resources
80
80
 
81
+ > ⚠️ The package isn't threadsafe, for better performance in single-threaded applications and those using `asyncio`.
82
+ > So remember to use `threading.Lock` if you're writing a multithreaded program.
83
+
81
84
  * [**Basic usage**](https://github.com/100nm/python-injection/tree/prod/documentation/basic-usage.md)
82
85
  * [**Scoped dependencies**](https://github.com/100nm/python-injection/tree/prod/documentation/scoped-dependencies.md)
83
86
  * [**Testing**](https://github.com/100nm/python-injection/tree/prod/documentation/testing.md)
@@ -55,6 +55,9 @@ if __name__ == "__main__":
55
55
 
56
56
  ## Resources
57
57
 
58
+ > ⚠️ The package isn't threadsafe, for better performance in single-threaded applications and those using `asyncio`.
59
+ > So remember to use `threading.Lock` if you're writing a multithreaded program.
60
+
58
61
  * [**Basic usage**](https://github.com/100nm/python-injection/tree/prod/documentation/basic-usage.md)
59
62
  * [**Scoped dependencies**](https://github.com/100nm/python-injection/tree/prod/documentation/scoped-dependencies.md)
60
63
  * [**Testing**](https://github.com/100nm/python-injection/tree/prod/documentation/testing.md)
@@ -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:
@@ -272,7 +281,7 @@ class Module:
272
281
  module: Module,
273
282
  *,
274
283
  priority: Priority | PriorityStr = ...,
275
- ) -> Iterator[None]:
284
+ ) -> Iterator[Self]:
276
285
  """
277
286
  Context manager or decorator for temporary use of a module.
278
287
  """
@@ -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[Self]: ...
298
309
  async def all_ready(self) -> None: ...
299
310
  def add_logger(self, logger: Logger) -> Self: ...
300
311
  @classmethod
@@ -1,4 +1,5 @@
1
1
  import itertools
2
+ from collections import deque
2
3
  from collections.abc import Callable, Generator, Iterator
3
4
  from dataclasses import dataclass, field
4
5
  from inspect import isclass, isgeneratorfunction
@@ -13,8 +14,8 @@ type HookFunction[**P, T] = Callable[P, T] | HookGeneratorFunction[P, T]
13
14
 
14
15
  @dataclass(eq=False, frozen=True, slots=True)
15
16
  class Hook[**P, T]:
16
- __functions: list[HookFunction[P, T]] = field(
17
- default_factory=list,
17
+ __functions: deque[HookFunction[P, T]] = field(
18
+ default_factory=deque,
18
19
  init=False,
19
20
  repr=False,
20
21
  )
@@ -35,7 +36,7 @@ class Hook[**P, T]:
35
36
  return iter(self.__functions)
36
37
 
37
38
  def add(self, *functions: HookFunction[P, T]) -> Self:
38
- self.__functions.extend(reversed(functions))
39
+ self.__functions.extendleft(functions)
39
40
  return self
40
41
 
41
42
  @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:
@@ -730,11 +739,11 @@ class Module(Broker, EventListener):
730
739
  module: Module,
731
740
  *,
732
741
  priority: Priority | PriorityStr = Priority.get_default(),
733
- ) -> Iterator[None]:
742
+ ) -> Iterator[Self]:
734
743
  self.use(module, priority=priority)
735
744
 
736
745
  try:
737
- yield
746
+ yield self
738
747
  finally:
739
748
  self.stop_using(module)
740
749
 
@@ -753,6 +762,23 @@ class Module(Broker, EventListener):
753
762
 
754
763
  return self
755
764
 
765
+ def load_profile(self, *names: str) -> ContextManager[Self]:
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[Self]:
777
+ yield self
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)
@@ -121,15 +121,9 @@ def _bind_scope(name: str, scope: Scope, shared: bool) -> Iterator[None]:
121
121
  f"Scope `{name}` is already defined in the current context."
122
122
  )
123
123
 
124
- strategy = (
125
- state.bind_shared_scope(scope) if shared else state.bind_contextual_scope(scope)
126
- )
127
-
128
- try:
129
- with strategy:
130
- yield
131
- finally:
132
- scope.cache.clear()
124
+ strategy = state.bind_shared_scope if shared else state.bind_contextual_scope
125
+ with strategy(scope):
126
+ yield
133
127
 
134
128
 
135
129
  @runtime_checkable
@@ -1,6 +1,6 @@
1
1
  from typing import ContextManager, Final
2
2
 
3
- from injection import mod
3
+ from injection import Module, mod
4
4
  from injection.utils import load_profile
5
5
 
6
6
  __all__ = (
@@ -23,5 +23,5 @@ test_scoped = mod(_TEST_PROFILE_NAME).scoped
23
23
  test_singleton = mod(_TEST_PROFILE_NAME).singleton
24
24
 
25
25
 
26
- def load_test_profile(*names: str) -> ContextManager[None]:
26
+ def load_test_profile(*names: str) -> ContextManager[Module]:
27
27
  return load_profile(_TEST_PROFILE_NAME, *names)
@@ -11,7 +11,7 @@ test_injectable = __MODULE.injectable
11
11
  test_scoped = __MODULE.scoped
12
12
  test_singleton = __MODULE.singleton
13
13
 
14
- def load_test_profile(*names: str) -> ContextManager[None]:
14
+ def load_test_profile(*names: str) -> ContextManager[Module]:
15
15
  """
16
16
  Context manager or decorator for temporary use test module.
17
17
  """
@@ -1,43 +1,28 @@
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
6
5
  from types import ModuleType as PythonModule
7
6
  from typing import ContextManager
8
7
 
8
+ from injection import Module, mod
9
9
  from injection import __name__ as injection_package_name
10
- from injection import mod
11
10
 
12
11
  __all__ = ("load_modules_with_keywords", "load_packages", "load_profile")
13
12
 
14
13
 
15
- def load_profile(*names: str) -> ContextManager[None]:
14
+ def load_profile(*names: str) -> ContextManager[Module]:
16
15
  """
17
16
  Injection module initialization function based on profile name.
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.post0"
28
28
  description = "Fast and easy dependency injection framework."
29
29
  license = { text = "MIT" }
30
30
  readme = "README.md"