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