python-injection 0.26.3__py3-none-any.whl → 0.27.0__py3-none-any.whl
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.
- injection/_core/common/asynchronous.py +3 -3
- injection/_core/common/threading.py +12 -3
- injection/_core/injectables.py +5 -5
- injection/_core/module.py +17 -9
- injection/_core/scope.py +6 -3
- injection/loaders.py +8 -0
- {python_injection-0.26.3.dist-info → python_injection-0.27.0.dist-info}/METADATA +8 -8
- {python_injection-0.26.3.dist-info → python_injection-0.27.0.dist-info}/RECORD +10 -10
- {python_injection-0.26.3.dist-info → python_injection-0.27.0.dist-info}/WHEEL +1 -1
- {python_injection-0.26.3.dist-info → python_injection-0.27.0.dist-info}/licenses/LICENSE +0 -0
|
@@ -4,7 +4,7 @@ from contextlib import AbstractAsyncContextManager
|
|
|
4
4
|
from dataclasses import dataclass
|
|
5
5
|
from typing import Any, NoReturn, Protocol, runtime_checkable
|
|
6
6
|
|
|
7
|
-
|
|
7
|
+
AsyncLock: Callable[[], AbstractAsyncContextManager[Any]]
|
|
8
8
|
|
|
9
9
|
try:
|
|
10
10
|
import anyio
|
|
@@ -12,10 +12,10 @@ try:
|
|
|
12
12
|
except ImportError: # pragma: no cover
|
|
13
13
|
import asyncio
|
|
14
14
|
|
|
15
|
-
|
|
15
|
+
AsyncLock = asyncio.Lock
|
|
16
16
|
|
|
17
17
|
else:
|
|
18
|
-
|
|
18
|
+
AsyncLock = anyio.Lock
|
|
19
19
|
|
|
20
20
|
|
|
21
21
|
@dataclass(repr=False, eq=False, frozen=True, slots=True)
|
|
@@ -1,6 +1,8 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
1
3
|
from contextlib import AbstractContextManager, nullcontext
|
|
2
4
|
from os import getenv
|
|
3
|
-
from threading import
|
|
5
|
+
from threading import Lock
|
|
4
6
|
from typing import Any, Final
|
|
5
7
|
|
|
6
8
|
_PYTHON_INJECTION_THREADSAFE: Final[bool] = bool(
|
|
@@ -8,6 +10,13 @@ _PYTHON_INJECTION_THREADSAFE: Final[bool] = bool(
|
|
|
8
10
|
)
|
|
9
11
|
|
|
10
12
|
|
|
11
|
-
def
|
|
13
|
+
def lock_if_threadsafe(
|
|
14
|
+
lock: AbstractContextManager[Any] | None = None,
|
|
15
|
+
threadsafe: bool | None = None,
|
|
16
|
+
) -> AbstractContextManager[Any]:
|
|
12
17
|
threadsafe = _PYTHON_INJECTION_THREADSAFE if threadsafe is None else threadsafe
|
|
13
|
-
|
|
18
|
+
|
|
19
|
+
if not threadsafe:
|
|
20
|
+
return nullcontext()
|
|
21
|
+
|
|
22
|
+
return lock or Lock()
|
injection/_core/injectables.py
CHANGED
|
@@ -12,7 +12,7 @@ from typing import (
|
|
|
12
12
|
runtime_checkable,
|
|
13
13
|
)
|
|
14
14
|
|
|
15
|
-
from injection._core.common.asynchronous import
|
|
15
|
+
from injection._core.common.asynchronous import AsyncLock, Caller
|
|
16
16
|
from injection._core.common.type import InputType
|
|
17
17
|
from injection._core.scope import Scope, get_first_scope, in_scope_cache
|
|
18
18
|
from injection._core.slots import SlotKey
|
|
@@ -51,12 +51,12 @@ class TransientInjectable[T](Injectable[T]):
|
|
|
51
51
|
|
|
52
52
|
|
|
53
53
|
class CacheLogic[T]:
|
|
54
|
-
__slots__ = ("
|
|
54
|
+
__slots__ = ("__lock",)
|
|
55
55
|
|
|
56
|
-
|
|
56
|
+
__lock: AbstractAsyncContextManager[Any]
|
|
57
57
|
|
|
58
58
|
def __init__(self) -> None:
|
|
59
|
-
self.
|
|
59
|
+
self.__lock = AsyncLock()
|
|
60
60
|
|
|
61
61
|
async def aget_or_create[K](
|
|
62
62
|
self,
|
|
@@ -64,7 +64,7 @@ class CacheLogic[T]:
|
|
|
64
64
|
key: K,
|
|
65
65
|
factory: Callable[..., Awaitable[T]],
|
|
66
66
|
) -> T:
|
|
67
|
-
async with self.
|
|
67
|
+
async with self.__lock:
|
|
68
68
|
with suppress(KeyError):
|
|
69
69
|
return cache[key]
|
|
70
70
|
|
injection/_core/module.py
CHANGED
|
@@ -35,6 +35,7 @@ from inspect import (
|
|
|
35
35
|
)
|
|
36
36
|
from inspect import signature as inspect_signature
|
|
37
37
|
from logging import Logger, getLogger
|
|
38
|
+
from threading import RLock
|
|
38
39
|
from types import MethodType
|
|
39
40
|
from typing import TYPE_CHECKING, Any, ClassVar, Literal, NamedTuple, Self, overload
|
|
40
41
|
|
|
@@ -44,7 +45,7 @@ from injection._core.common.asynchronous import Caller, SimpleAwaitable
|
|
|
44
45
|
from injection._core.common.event import Event, EventChannel, EventListener
|
|
45
46
|
from injection._core.common.invertible import Invertible, SimpleInvertible
|
|
46
47
|
from injection._core.common.lazy import Lazy
|
|
47
|
-
from injection._core.common.threading import
|
|
48
|
+
from injection._core.common.threading import lock_if_threadsafe
|
|
48
49
|
from injection._core.common.type import (
|
|
49
50
|
InputType,
|
|
50
51
|
TypeInfo,
|
|
@@ -180,6 +181,11 @@ class Module(EventListener, InjectionProvider): # type: ignore[misc]
|
|
|
180
181
|
init=False,
|
|
181
182
|
repr=False,
|
|
182
183
|
)
|
|
184
|
+
__lock: RLock = field(
|
|
185
|
+
default_factory=RLock,
|
|
186
|
+
init=False,
|
|
187
|
+
repr=False,
|
|
188
|
+
)
|
|
183
189
|
__loggers: list[Logger] = field(
|
|
184
190
|
default_factory=lambda: [getLogger("python-injection")],
|
|
185
191
|
init=False,
|
|
@@ -350,7 +356,7 @@ class Module(EventListener, InjectionProvider): # type: ignore[misc]
|
|
|
350
356
|
/,
|
|
351
357
|
threadsafe: bool | None = None,
|
|
352
358
|
) -> InjectMetadata[P, T]:
|
|
353
|
-
return InjectMetadata(wrapped, threadsafe).listen(self)
|
|
359
|
+
return InjectMetadata(wrapped, self.__lock, threadsafe).listen(self)
|
|
354
360
|
|
|
355
361
|
if TYPE_CHECKING: # pragma: no cover
|
|
356
362
|
|
|
@@ -397,7 +403,7 @@ class Module(EventListener, InjectionProvider): # type: ignore[misc]
|
|
|
397
403
|
*,
|
|
398
404
|
threadsafe: bool | None = None,
|
|
399
405
|
) -> T:
|
|
400
|
-
with
|
|
406
|
+
with lock_if_threadsafe(self.__lock, threadsafe):
|
|
401
407
|
injectable = self[cls]
|
|
402
408
|
return await injectable.aget_instance()
|
|
403
409
|
|
|
@@ -407,7 +413,7 @@ class Module(EventListener, InjectionProvider): # type: ignore[misc]
|
|
|
407
413
|
*,
|
|
408
414
|
threadsafe: bool | None = None,
|
|
409
415
|
) -> T:
|
|
410
|
-
with
|
|
416
|
+
with lock_if_threadsafe(self.__lock, threadsafe):
|
|
411
417
|
injectable = self[cls]
|
|
412
418
|
return injectable.get_instance()
|
|
413
419
|
|
|
@@ -598,11 +604,12 @@ class Module(EventListener, InjectionProvider): # type: ignore[misc]
|
|
|
598
604
|
return self
|
|
599
605
|
|
|
600
606
|
def stop_using(self, module: Module) -> Self:
|
|
601
|
-
|
|
607
|
+
if module in self.__modules:
|
|
608
|
+
event = ModuleRemoved(self, module)
|
|
602
609
|
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
610
|
+
with self.dispatch(event):
|
|
611
|
+
self.__modules.pop(module)
|
|
612
|
+
module.remove_listener(self)
|
|
606
613
|
|
|
607
614
|
return self
|
|
608
615
|
|
|
@@ -833,11 +840,12 @@ class InjectMetadata[**P, T](Caller[P, T], EventListener):
|
|
|
833
840
|
def __init__(
|
|
834
841
|
self,
|
|
835
842
|
wrapped: Callable[P, T],
|
|
843
|
+
lock: RLock,
|
|
836
844
|
/,
|
|
837
845
|
threadsafe: bool | None = None,
|
|
838
846
|
) -> None:
|
|
839
847
|
self.__dependencies = Dependencies.empty()
|
|
840
|
-
self.__lock =
|
|
848
|
+
self.__lock = lock_if_threadsafe(lock, threadsafe)
|
|
841
849
|
self.__owner = None
|
|
842
850
|
self.__tasks = deque()
|
|
843
851
|
self.__wrapped = wrapped
|
injection/_core/scope.py
CHANGED
|
@@ -16,6 +16,7 @@ from contextvars import ContextVar
|
|
|
16
16
|
from dataclasses import dataclass, field
|
|
17
17
|
from enum import StrEnum
|
|
18
18
|
from functools import partial
|
|
19
|
+
from threading import Lock
|
|
19
20
|
from types import EllipsisType, TracebackType
|
|
20
21
|
from typing import (
|
|
21
22
|
TYPE_CHECKING,
|
|
@@ -29,7 +30,7 @@ from typing import (
|
|
|
29
30
|
runtime_checkable,
|
|
30
31
|
)
|
|
31
32
|
|
|
32
|
-
from injection._core.common.threading import
|
|
33
|
+
from injection._core.common.threading import lock_if_threadsafe
|
|
33
34
|
from injection._core.slots import SlotKey
|
|
34
35
|
from injection.exceptions import (
|
|
35
36
|
InjectionError,
|
|
@@ -38,6 +39,8 @@ from injection.exceptions import (
|
|
|
38
39
|
ScopeUndefinedError,
|
|
39
40
|
)
|
|
40
41
|
|
|
42
|
+
__thread_lock: Final[Lock] = Lock()
|
|
43
|
+
|
|
41
44
|
|
|
42
45
|
class ScopeKind(StrEnum):
|
|
43
46
|
CONTEXTUAL = "contextual"
|
|
@@ -197,7 +200,7 @@ def _bind_scope(
|
|
|
197
200
|
kind: ScopeKind | ScopeKindStr,
|
|
198
201
|
threadsafe: bool | None,
|
|
199
202
|
) -> Iterator[ScopeFacade]:
|
|
200
|
-
lock =
|
|
203
|
+
lock = lock_if_threadsafe(__thread_lock, threadsafe)
|
|
201
204
|
|
|
202
205
|
with lock:
|
|
203
206
|
if get_first_scope(name, default=None):
|
|
@@ -209,7 +212,7 @@ def _bind_scope(
|
|
|
209
212
|
stack.enter_context(__scope_resolvers[kind][name].bind(scope))
|
|
210
213
|
|
|
211
214
|
try:
|
|
212
|
-
yield _UserScope(scope,
|
|
215
|
+
yield _UserScope(scope, lock_if_threadsafe(threadsafe=threadsafe))
|
|
213
216
|
|
|
214
217
|
finally:
|
|
215
218
|
with lock:
|
injection/loaders.py
CHANGED
|
@@ -5,6 +5,7 @@ import sys
|
|
|
5
5
|
from abc import abstractmethod
|
|
6
6
|
from collections.abc import Callable, Iterator, Mapping, Sequence
|
|
7
7
|
from dataclasses import dataclass, field
|
|
8
|
+
from fnmatch import fnmatchcase
|
|
8
9
|
from importlib import import_module
|
|
9
10
|
from importlib.util import find_spec
|
|
10
11
|
from os.path import isfile
|
|
@@ -99,6 +100,13 @@ class PythonModuleLoader:
|
|
|
99
100
|
module = import_module(name) if self.predicate(name) else None
|
|
100
101
|
yield name, module
|
|
101
102
|
|
|
103
|
+
@classmethod
|
|
104
|
+
def fnmatch(cls, *patterns: str) -> Self:
|
|
105
|
+
def predicate(module_name: str) -> bool:
|
|
106
|
+
return any(fnmatchcase(module_name, pattern) for pattern in patterns)
|
|
107
|
+
|
|
108
|
+
return cls(predicate)
|
|
109
|
+
|
|
102
110
|
@classmethod
|
|
103
111
|
def from_keywords(cls, *keywords: str) -> Self:
|
|
104
112
|
"""
|
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
Metadata-Version: 2.
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
2
|
Name: python-injection
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.27.0
|
|
4
4
|
Summary: Dead-simple dependency injection framework for Python.
|
|
5
5
|
Project-URL: Documentation, https://python-injection.remimd.dev
|
|
6
6
|
Project-URL: Repository, https://github.com/100nm/python-injection
|
|
@@ -32,12 +32,12 @@ Description-Content-Type: text/markdown
|
|
|
32
32
|
|
|
33
33
|
# python-injection
|
|
34
34
|
|
|
35
|
-
[](https://pypi.org/project/python-injection)
|
|
36
|
+
[](https://pypistats.org/packages/python-injection)
|
|
37
37
|
|
|
38
38
|
## Project motivations
|
|
39
39
|
|
|
40
|
-
Dependency injection in Python has long been a source of frustration.
|
|
40
|
+
Dependency injection in Python has long been a source of frustration.
|
|
41
41
|
|
|
42
42
|
Existing solutions are often verbose, require extensive boilerplate, or fail to leverage Python's type hints effectively. `python-injection` was created to solve these problems once and for all by providing a simple, elegant, and powerful dependency injection framework that feels natural to Python developers.
|
|
43
43
|
|
|
@@ -47,13 +47,13 @@ The goal is straightforward: make dependency injection so easy that you'll wonde
|
|
|
47
47
|
|
|
48
48
|
- **Type-driven resolution**: Dependencies are automatically resolved using Python's type annotations.
|
|
49
49
|
- **Decorator-based registration**: Register your dependencies with simple, readable decorators.
|
|
50
|
-
- **Flexible lifetimes**: Choose from
|
|
50
|
+
- **Flexible lifetimes**: Choose from four types of lifetimes to match your needs:
|
|
51
51
|
- **Transient**: A new instance every time
|
|
52
52
|
- **Singleton**: One instance for the entire application
|
|
53
53
|
- **Scoped**: One instance per scope, with context manager support
|
|
54
|
-
- **Constant**:
|
|
54
|
+
- **Constant**: A value created once and never re-resolved, either pre-existing or computed lazily
|
|
55
55
|
- **Profile support**: Ability to swap certain dependencies based on a profile.
|
|
56
|
-
- **Pull-based instantiation**: Dependencies are only created when needed, improving startup time and resource usage.
|
|
56
|
+
- **Pull-based instantiation**: Dependencies registered with decorators are only created when needed, improving startup time and resource usage.
|
|
57
57
|
- **Full sync/async support**: Works seamlessly with both synchronous and asynchronous code.
|
|
58
58
|
|
|
59
59
|
## Installation
|
|
@@ -2,29 +2,29 @@ injection/__init__.py,sha256=8nYCdToksoBIRTiJFrEbAUa_g8--dFL--4pfSGlGDP4,1350
|
|
|
2
2
|
injection/__init__.pyi,sha256=XzOmrlk_6HNAiQVU4aUDoy3414enMaKTNPpKpXHLYyI,13659
|
|
3
3
|
injection/entrypoint.py,sha256=-pQuu2pM6CIs-GBNYhlqRe2bPBvShbi_q-AKeOw0Iko,6698
|
|
4
4
|
injection/exceptions.py,sha256=v57yMujiq6H_zwwn30A8UYEZX9R9k-bY8FnsdaimPM4,1025
|
|
5
|
-
injection/loaders.py,sha256=
|
|
5
|
+
injection/loaders.py,sha256=P3O6T_yTDpWF5bSviPZdHkjul07qsf3fhGfwoTtroNY,7652
|
|
6
6
|
injection/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
7
7
|
injection/_core/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
8
8
|
injection/_core/asfunction.py,sha256=GY228WHQWvA-9rg3unR8ttyIJlhwxy0bC3LUO5K8Erg,1864
|
|
9
9
|
injection/_core/descriptors.py,sha256=OBuGGPPn9b2dLTfFA1tdUD2zv11F4_1h1Vsm_cNaF-M,3602
|
|
10
|
-
injection/_core/injectables.py,sha256=
|
|
10
|
+
injection/_core/injectables.py,sha256=MXl6sNKWAWvwcY7BnDf8fXhlIxc_DadzNB2RzhO7WA4,6436
|
|
11
11
|
injection/_core/locator.py,sha256=zSzLN7WiEPlYyrWra8siVnqx78jEW7AZ0rn5hZSmF3o,8270
|
|
12
|
-
injection/_core/module.py,sha256=
|
|
13
|
-
injection/_core/scope.py,sha256=
|
|
12
|
+
injection/_core/module.py,sha256=Ss562rKUfm9y7sHRknc-2Pa-YX0QD5zvEBDp3AtuAtA,29189
|
|
13
|
+
injection/_core/scope.py,sha256=wFosijGOCyFJ5nbk8sclL47GbiI5gpYHegaJ0HloclU,9196
|
|
14
14
|
injection/_core/slots.py,sha256=g9TG6CbqRzCsjg01iPyfRtTTUCJnnJOwcj9mJabH0dc,37
|
|
15
15
|
injection/_core/common/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
16
|
-
injection/_core/common/asynchronous.py,sha256=
|
|
16
|
+
injection/_core/common/asynchronous.py,sha256=GFJHzUDoq0TnD3BWQ4669HhQ4kEsZH-B97dFoBzCft0,1757
|
|
17
17
|
injection/_core/common/event.py,sha256=oZj1Uw4-XAcdBFzy7xmn9CC6HoIMxbdVXkHICLSORqk,1252
|
|
18
18
|
injection/_core/common/invertible.py,sha256=gA_vw5nBjgp_w9MrDK5jMO8lhuOQWON8BbPpKzEuIY0,502
|
|
19
19
|
injection/_core/common/lazy.py,sha256=jTv7mYChvRp6WLWILLbEejrHq24gFRKOkkUuZjnwYlE,796
|
|
20
|
-
injection/_core/common/threading.py,sha256
|
|
20
|
+
injection/_core/common/threading.py,sha256=m4AQsE_uTLpL8r0XzZ0q6-PZzcTH2izmXQF1Ojm-TVg,592
|
|
21
21
|
injection/_core/common/type.py,sha256=d1CtWyu--7Ru7vi9uqAQucO1oXFmAsuUhP2j-dCYkfM,1533
|
|
22
22
|
injection/ext/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
23
23
|
injection/ext/fastapi.py,sha256=wpiNyHKEPwQ7sAJt30XpkgiCi4-gfmyqkQWw9BejXwg,1633
|
|
24
24
|
injection/ext/fastapi.pyi,sha256=HLs7mfruIEFRrN_Xf8oCvSa4qwHWfwm6HHU_KMedXkE,185
|
|
25
25
|
injection/testing/__init__.py,sha256=ZWMacZuGhK8Edq1L0Ng0PaRlgKXEunyFZkFvx7-IYIQ,935
|
|
26
26
|
injection/testing/__init__.pyi,sha256=ZFQMUZmvBVkV6Ch7jiYE9zeJhnqHAQnt3vWQ1Th0zDk,646
|
|
27
|
-
python_injection-0.
|
|
28
|
-
python_injection-0.
|
|
29
|
-
python_injection-0.
|
|
30
|
-
python_injection-0.
|
|
27
|
+
python_injection-0.27.0.dist-info/METADATA,sha256=k4HS5jEhEwHJ-E6V0YnE2f_26ViKF8NfdOZSP-1Qwgo,3756
|
|
28
|
+
python_injection-0.27.0.dist-info/WHEEL,sha256=2iZrJkDIkeDh0k9SikKMqFuRP-ua6mb4Ir1AsER4BEw,87
|
|
29
|
+
python_injection-0.27.0.dist-info/licenses/LICENSE,sha256=oC77BOa9kaaQni5rW-Z-ytz3E5h4EVg248BHg9UFgyg,1063
|
|
30
|
+
python_injection-0.27.0.dist-info/RECORD,,
|
|
File without changes
|