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.
- {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
|