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.
- {python_injection-0.13.2 → python_injection-0.14.0.post0}/PKG-INFO +4 -1
- {python_injection-0.13.2 → python_injection-0.14.0.post0}/README.md +3 -0
- {python_injection-0.13.2 → python_injection-0.14.0.post0}/injection/__init__.pyi +13 -2
- {python_injection-0.13.2 → python_injection-0.14.0.post0}/injection/_core/hook.py +4 -3
- {python_injection-0.13.2 → python_injection-0.14.0.post0}/injection/_core/module.py +46 -13
- {python_injection-0.13.2 → python_injection-0.14.0.post0}/injection/_core/scope.py +12 -18
- {python_injection-0.13.2 → python_injection-0.14.0.post0}/injection/testing/__init__.py +2 -2
- {python_injection-0.13.2 → python_injection-0.14.0.post0}/injection/testing/__init__.pyi +1 -1
- {python_injection-0.13.2 → python_injection-0.14.0.post0}/injection/utils.py +13 -28
- {python_injection-0.13.2 → python_injection-0.14.0.post0}/pyproject.toml +1 -1
- {python_injection-0.13.2 → python_injection-0.14.0.post0}/.gitignore +0 -0
- {python_injection-0.13.2 → python_injection-0.14.0.post0}/injection/__init__.py +0 -0
- {python_injection-0.13.2 → python_injection-0.14.0.post0}/injection/_core/__init__.py +0 -0
- {python_injection-0.13.2 → python_injection-0.14.0.post0}/injection/_core/common/__init__.py +0 -0
- {python_injection-0.13.2 → python_injection-0.14.0.post0}/injection/_core/common/asynchronous.py +0 -0
- {python_injection-0.13.2 → python_injection-0.14.0.post0}/injection/_core/common/event.py +0 -0
- {python_injection-0.13.2 → python_injection-0.14.0.post0}/injection/_core/common/invertible.py +0 -0
- {python_injection-0.13.2 → python_injection-0.14.0.post0}/injection/_core/common/key.py +0 -0
- {python_injection-0.13.2 → python_injection-0.14.0.post0}/injection/_core/common/lazy.py +0 -0
- {python_injection-0.13.2 → python_injection-0.14.0.post0}/injection/_core/common/type.py +0 -0
- {python_injection-0.13.2 → python_injection-0.14.0.post0}/injection/_core/descriptors.py +0 -0
- {python_injection-0.13.2 → python_injection-0.14.0.post0}/injection/_core/injectables.py +0 -0
- {python_injection-0.13.2 → python_injection-0.14.0.post0}/injection/exceptions.py +0 -0
- {python_injection-0.13.2 → python_injection-0.14.0.post0}/injection/integrations/__init__.py +0 -0
- {python_injection-0.13.2 → python_injection-0.14.0.post0}/injection/integrations/fastapi.py +0 -0
- {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.
|
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](
|
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[
|
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:
|
17
|
-
default_factory=
|
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.
|
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](
|
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:
|
@@ -730,11 +739,11 @@ class Module(Broker, EventListener):
|
|
730
739
|
module: Module,
|
731
740
|
*,
|
732
741
|
priority: Priority | PriorityStr = Priority.get_default(),
|
733
|
-
) -> Iterator[
|
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],
|
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)
|
@@ -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
|
-
|
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[
|
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[
|
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,
|
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[
|
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
|
-
|
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
|
{python_injection-0.13.2 → python_injection-0.14.0.post0}/injection/_core/common/__init__.py
RENAMED
File without changes
|
{python_injection-0.13.2 → python_injection-0.14.0.post0}/injection/_core/common/asynchronous.py
RENAMED
File without changes
|
File without changes
|
{python_injection-0.13.2 → python_injection-0.14.0.post0}/injection/_core/common/invertible.py
RENAMED
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
{python_injection-0.13.2 → python_injection-0.14.0.post0}/injection/integrations/__init__.py
RENAMED
File without changes
|
File without changes
|
File without changes
|