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.
- {python_injection-0.13.2 → python_injection-0.14.0}/PKG-INFO +1 -1
- {python_injection-0.13.2 → python_injection-0.14.0}/injection/__init__.pyi +12 -1
- {python_injection-0.13.2 → python_injection-0.14.0}/injection/_core/module.py +44 -11
- {python_injection-0.13.2 → python_injection-0.14.0}/injection/_core/scope.py +11 -14
- {python_injection-0.13.2 → python_injection-0.14.0}/injection/utils.py +11 -26
- {python_injection-0.13.2 → python_injection-0.14.0}/pyproject.toml +1 -1
- {python_injection-0.13.2 → python_injection-0.14.0}/.gitignore +0 -0
- {python_injection-0.13.2 → python_injection-0.14.0}/README.md +0 -0
- {python_injection-0.13.2 → python_injection-0.14.0}/injection/__init__.py +0 -0
- {python_injection-0.13.2 → python_injection-0.14.0}/injection/_core/__init__.py +0 -0
- {python_injection-0.13.2 → python_injection-0.14.0}/injection/_core/common/__init__.py +0 -0
- {python_injection-0.13.2 → python_injection-0.14.0}/injection/_core/common/asynchronous.py +0 -0
- {python_injection-0.13.2 → python_injection-0.14.0}/injection/_core/common/event.py +0 -0
- {python_injection-0.13.2 → python_injection-0.14.0}/injection/_core/common/invertible.py +0 -0
- {python_injection-0.13.2 → python_injection-0.14.0}/injection/_core/common/key.py +0 -0
- {python_injection-0.13.2 → python_injection-0.14.0}/injection/_core/common/lazy.py +0 -0
- {python_injection-0.13.2 → python_injection-0.14.0}/injection/_core/common/type.py +0 -0
- {python_injection-0.13.2 → python_injection-0.14.0}/injection/_core/descriptors.py +0 -0
- {python_injection-0.13.2 → python_injection-0.14.0}/injection/_core/hook.py +0 -0
- {python_injection-0.13.2 → python_injection-0.14.0}/injection/_core/injectables.py +0 -0
- {python_injection-0.13.2 → python_injection-0.14.0}/injection/exceptions.py +0 -0
- {python_injection-0.13.2 → python_injection-0.14.0}/injection/integrations/__init__.py +0 -0
- {python_injection-0.13.2 → python_injection-0.14.0}/injection/integrations/fastapi.py +0 -0
- {python_injection-0.13.2 → python_injection-0.14.0}/injection/py.typed +0 -0
- {python_injection-0.13.2 → python_injection-0.14.0}/injection/testing/__init__.py +0 -0
- {python_injection-0.13.2 → python_injection-0.14.0}/injection/testing/__init__.pyi +0 -0
@@ -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](
|
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](
|
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,
|
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],
|
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.
|
965
|
-
|
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.
|
970
|
-
|
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
|
-
|
38
|
-
|
37
|
+
__default: Scope | None = field(
|
38
|
+
default=None,
|
39
39
|
init=False,
|
40
40
|
)
|
41
|
-
|
42
|
-
|
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
|
51
|
-
yield
|
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.
|
72
|
+
self.__default = scope
|
73
73
|
|
74
74
|
try:
|
75
75
|
yield
|
76
76
|
finally:
|
77
|
-
self.
|
77
|
+
self.__default = None
|
78
78
|
|
79
79
|
def get_scope(self) -> Scope | None:
|
80
|
-
return self.__context_var.get(self.
|
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
|
-
|
129
|
-
|
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,
|
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
|
-
|
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:
|
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
|
-
|
61
|
-
|
62
|
-
|
63
|
-
|
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
|
-
|
66
|
-
|
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
|
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|