anydi 0.33.0__py3-none-any.whl → 0.34.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.
- anydi/_container.py +38 -26
- anydi/_context.py +15 -8
- anydi/_types.py +7 -1
- anydi/_utils.py +49 -12
- anydi/ext/pytest_plugin.py +8 -4
- {anydi-0.33.0.dist-info → anydi-0.34.0.dist-info}/METADATA +2 -3
- {anydi-0.33.0.dist-info → anydi-0.34.0.dist-info}/RECORD +10 -10
- {anydi-0.33.0.dist-info → anydi-0.34.0.dist-info}/LICENSE +0 -0
- {anydi-0.33.0.dist-info → anydi-0.34.0.dist-info}/WHEEL +0 -0
- {anydi-0.33.0.dist-info → anydi-0.34.0.dist-info}/entry_points.txt +0 -0
anydi/_container.py
CHANGED
|
@@ -5,6 +5,7 @@ from __future__ import annotations
|
|
|
5
5
|
import contextlib
|
|
6
6
|
import functools
|
|
7
7
|
import inspect
|
|
8
|
+
import threading
|
|
8
9
|
import types
|
|
9
10
|
from collections import defaultdict
|
|
10
11
|
from collections.abc import AsyncIterator, Iterable, Iterator, Sequence
|
|
@@ -25,8 +26,8 @@ from ._logger import logger
|
|
|
25
26
|
from ._module import Module, ModuleRegistry
|
|
26
27
|
from ._provider import Provider
|
|
27
28
|
from ._scanner import Scanner
|
|
28
|
-
from ._types import AnyInterface, Interface, Scope,
|
|
29
|
-
from ._utils import get_full_qualname, get_typed_parameters, is_builtin_type
|
|
29
|
+
from ._types import AnyInterface, DependencyWrapper, Interface, Scope, is_marker
|
|
30
|
+
from ._utils import AsyncRLock, get_full_qualname, get_typed_parameters, is_builtin_type
|
|
30
31
|
|
|
31
32
|
T = TypeVar("T", bound=Any)
|
|
32
33
|
P = ParamSpec("P")
|
|
@@ -54,6 +55,8 @@ class Container:
|
|
|
54
55
|
self._providers: dict[type[Any], Provider] = {}
|
|
55
56
|
self._resource_cache: dict[Scope, list[type[Any]]] = defaultdict(list)
|
|
56
57
|
self._singleton_context = SingletonContext(self)
|
|
58
|
+
self._singleton_lock = threading.RLock()
|
|
59
|
+
self._singleton_async_lock = AsyncRLock()
|
|
57
60
|
self._transient_context = TransientContext(self)
|
|
58
61
|
self._request_context_var: ContextVar[RequestContext | None] = ContextVar(
|
|
59
62
|
"request_context", default=None
|
|
@@ -366,33 +369,15 @@ class Container:
|
|
|
366
369
|
|
|
367
370
|
provider = self._get_or_register_provider(interface)
|
|
368
371
|
scoped_context = self._get_scoped_context(provider.scope)
|
|
369
|
-
|
|
372
|
+
if provider.scope == "singleton":
|
|
373
|
+
with self._singleton_lock:
|
|
374
|
+
instance, created = scoped_context.get_or_create(provider)
|
|
375
|
+
else:
|
|
376
|
+
instance, created = scoped_context.get_or_create(provider)
|
|
370
377
|
if self.testing and created:
|
|
371
378
|
self._patch_test_resolver(instance)
|
|
372
379
|
return cast(T, instance)
|
|
373
380
|
|
|
374
|
-
def _patch_test_resolver(self, instance: Any) -> None:
|
|
375
|
-
"""Patch the test resolver for the instance."""
|
|
376
|
-
|
|
377
|
-
def _resolver(_self: Any, _name: str) -> Any:
|
|
378
|
-
try:
|
|
379
|
-
test_interfaces = object.__getattribute__(_self, "__test_interfaces__")
|
|
380
|
-
except AttributeError:
|
|
381
|
-
test_interfaces = {
|
|
382
|
-
name: value.interface
|
|
383
|
-
for name, value in object.__getattribute__(
|
|
384
|
-
_self, "__dict__"
|
|
385
|
-
).items()
|
|
386
|
-
if isinstance(value, TestInterface)
|
|
387
|
-
}
|
|
388
|
-
object.__setattr__(_self, "__test_interfaces__", test_interfaces)
|
|
389
|
-
if _name in test_interfaces:
|
|
390
|
-
return self.resolve(test_interfaces[_name])
|
|
391
|
-
return object.__getattribute__(_self, _name)
|
|
392
|
-
|
|
393
|
-
if hasattr(instance, "__class__") and not is_builtin_type(instance.__class__):
|
|
394
|
-
instance.__class__.__getattribute__ = _resolver
|
|
395
|
-
|
|
396
381
|
@overload
|
|
397
382
|
async def aresolve(self, interface: Interface[T]) -> T: ...
|
|
398
383
|
|
|
@@ -406,11 +391,38 @@ class Container:
|
|
|
406
391
|
|
|
407
392
|
provider = self._get_or_register_provider(interface)
|
|
408
393
|
scoped_context = self._get_scoped_context(provider.scope)
|
|
409
|
-
|
|
394
|
+
if provider.scope == "singleton":
|
|
395
|
+
async with self._singleton_async_lock:
|
|
396
|
+
instance, created = await scoped_context.aget_or_create(provider)
|
|
397
|
+
else:
|
|
398
|
+
instance, created = await scoped_context.aget_or_create(provider)
|
|
410
399
|
if self.testing and created:
|
|
411
400
|
self._patch_test_resolver(instance)
|
|
412
401
|
return cast(T, instance)
|
|
413
402
|
|
|
403
|
+
def _patch_test_resolver(self, instance: Any) -> None:
|
|
404
|
+
"""Patch the test resolver for the instance."""
|
|
405
|
+
if not hasattr(instance, "__dict__"):
|
|
406
|
+
return
|
|
407
|
+
|
|
408
|
+
wrapped = {
|
|
409
|
+
name: value.interface
|
|
410
|
+
for name, value in instance.__dict__.items()
|
|
411
|
+
if isinstance(value, DependencyWrapper)
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
# Custom resolver function
|
|
415
|
+
def _resolver(_self: Any, _name: str) -> Any:
|
|
416
|
+
if _name in wrapped:
|
|
417
|
+
# Resolve the dependency if it's wrapped
|
|
418
|
+
return self.resolve(wrapped[_name])
|
|
419
|
+
# Fall back to default behavior
|
|
420
|
+
return object.__getattribute__(_self, _name)
|
|
421
|
+
|
|
422
|
+
# Apply the patched resolver if wrapped attributes exist
|
|
423
|
+
if wrapped:
|
|
424
|
+
instance.__class__.__getattribute__ = _resolver
|
|
425
|
+
|
|
414
426
|
def is_resolved(self, interface: AnyInterface) -> bool:
|
|
415
427
|
"""Check if an instance by interface exists."""
|
|
416
428
|
try:
|
anydi/_context.py
CHANGED
|
@@ -9,8 +9,13 @@ from typing import TYPE_CHECKING, Any, Callable, ClassVar
|
|
|
9
9
|
from typing_extensions import Self, final
|
|
10
10
|
|
|
11
11
|
from ._provider import CallableKind, Provider
|
|
12
|
-
from ._types import AnyInterface,
|
|
13
|
-
from ._utils import
|
|
12
|
+
from ._types import AnyInterface, DependencyWrapper, Scope, is_event_type
|
|
13
|
+
from ._utils import (
|
|
14
|
+
get_full_qualname,
|
|
15
|
+
is_async_context_manager,
|
|
16
|
+
is_context_manager,
|
|
17
|
+
run_async,
|
|
18
|
+
)
|
|
14
19
|
|
|
15
20
|
if TYPE_CHECKING:
|
|
16
21
|
from ._container import Container
|
|
@@ -99,7 +104,9 @@ class ScopedContext(abc.ABC):
|
|
|
99
104
|
instance = parameter.default
|
|
100
105
|
else:
|
|
101
106
|
if self.container.testing:
|
|
102
|
-
instance =
|
|
107
|
+
instance = DependencyWrapper(
|
|
108
|
+
interface=parameter.annotation, instance=instance
|
|
109
|
+
)
|
|
103
110
|
if parameter.kind == parameter.POSITIONAL_ONLY:
|
|
104
111
|
args.append(instance)
|
|
105
112
|
else:
|
|
@@ -127,7 +134,9 @@ class ScopedContext(abc.ABC):
|
|
|
127
134
|
instance = parameter.default
|
|
128
135
|
else:
|
|
129
136
|
if self.container.testing:
|
|
130
|
-
instance =
|
|
137
|
+
instance = DependencyWrapper(
|
|
138
|
+
interface=parameter.annotation, instance=instance
|
|
139
|
+
)
|
|
131
140
|
if parameter.kind == parameter.POSITIONAL_ONLY:
|
|
132
141
|
args.append(instance)
|
|
133
142
|
else:
|
|
@@ -184,7 +193,7 @@ class ResourceScopedContext(ScopedContext):
|
|
|
184
193
|
"""Create an instance using the provider."""
|
|
185
194
|
instance = super()._create_instance(provider)
|
|
186
195
|
# Enter the context manager if the instance is closable.
|
|
187
|
-
if
|
|
196
|
+
if is_context_manager(instance):
|
|
188
197
|
self._stack.enter_context(instance)
|
|
189
198
|
return instance
|
|
190
199
|
|
|
@@ -198,7 +207,7 @@ class ResourceScopedContext(ScopedContext):
|
|
|
198
207
|
"""Create an instance asynchronously using the provider."""
|
|
199
208
|
instance = await super()._acreate_instance(provider)
|
|
200
209
|
# Enter the context manager if the instance is closable.
|
|
201
|
-
if
|
|
210
|
+
if is_async_context_manager(instance):
|
|
202
211
|
await self._async_stack.enter_async_context(instance)
|
|
203
212
|
return instance
|
|
204
213
|
|
|
@@ -229,8 +238,6 @@ class ResourceScopedContext(ScopedContext):
|
|
|
229
238
|
@abc.abstractmethod
|
|
230
239
|
def start(self) -> None:
|
|
231
240
|
"""Start the scoped context."""
|
|
232
|
-
for interface in self.container._resource_cache.get(self.scope, []): # noqa
|
|
233
|
-
self.container.resolve(interface)
|
|
234
241
|
|
|
235
242
|
def close(self) -> None:
|
|
236
243
|
"""Close the scoped context."""
|
anydi/_types.py
CHANGED
|
@@ -39,5 +39,11 @@ def is_event_type(obj: Any) -> bool:
|
|
|
39
39
|
|
|
40
40
|
|
|
41
41
|
@dataclass(frozen=True)
|
|
42
|
-
class
|
|
42
|
+
class DependencyWrapper:
|
|
43
43
|
interface: type[Any]
|
|
44
|
+
instance: Any
|
|
45
|
+
|
|
46
|
+
def __getattribute__(self, name: str) -> Any:
|
|
47
|
+
if name in {"interface", "instance"}:
|
|
48
|
+
return object.__getattribute__(self, name)
|
|
49
|
+
return getattr(self.instance, name)
|
anydi/_utils.py
CHANGED
|
@@ -8,15 +8,11 @@ import importlib
|
|
|
8
8
|
import inspect
|
|
9
9
|
import re
|
|
10
10
|
import sys
|
|
11
|
+
from types import TracebackType
|
|
11
12
|
from typing import Any, Callable, ForwardRef, TypeVar
|
|
12
13
|
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
try:
|
|
16
|
-
import anyio # noqa
|
|
17
|
-
except ImportError:
|
|
18
|
-
anyio = None # type: ignore[assignment]
|
|
19
|
-
|
|
14
|
+
import anyio
|
|
15
|
+
from typing_extensions import ParamSpec, Self, get_args, get_origin
|
|
20
16
|
|
|
21
17
|
T = TypeVar("T")
|
|
22
18
|
P = ParamSpec("P")
|
|
@@ -48,6 +44,16 @@ def is_builtin_type(tp: type[Any]) -> bool:
|
|
|
48
44
|
return tp.__module__ == builtins.__name__
|
|
49
45
|
|
|
50
46
|
|
|
47
|
+
def is_context_manager(obj: Any) -> bool:
|
|
48
|
+
"""Check if the given object is a context manager."""
|
|
49
|
+
return hasattr(obj, "__enter__") and hasattr(obj, "__exit__")
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def is_async_context_manager(obj: Any) -> bool:
|
|
53
|
+
"""Check if the given object is an async context manager."""
|
|
54
|
+
return hasattr(obj, "__aenter__") and hasattr(obj, "__aexit__")
|
|
55
|
+
|
|
56
|
+
|
|
51
57
|
def get_typed_annotation(
|
|
52
58
|
annotation: Any, globalns: dict[str, Any], module: Any = None
|
|
53
59
|
) -> Any:
|
|
@@ -82,11 +88,6 @@ async def run_async(
|
|
|
82
88
|
**kwargs: P.kwargs,
|
|
83
89
|
) -> T:
|
|
84
90
|
"""Runs the given function asynchronously using the `anyio` library."""
|
|
85
|
-
if not anyio:
|
|
86
|
-
raise ImportError(
|
|
87
|
-
"`anyio` library is not currently installed. Please make sure to install "
|
|
88
|
-
"it first, or consider using `anydi[full]` instead."
|
|
89
|
-
)
|
|
90
91
|
return await anyio.to_thread.run_sync(functools.partial(func, *args, **kwargs))
|
|
91
92
|
|
|
92
93
|
|
|
@@ -103,3 +104,39 @@ def import_string(dotted_path: str) -> Any:
|
|
|
103
104
|
return importlib.import_module(attribute_name)
|
|
104
105
|
except (ImportError, AttributeError) as exc:
|
|
105
106
|
raise ImportError(f"Cannot import '{dotted_path}': {exc}") from exc
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
class AsyncRLock:
|
|
110
|
+
def __init__(self) -> None:
|
|
111
|
+
self._lock = anyio.Lock()
|
|
112
|
+
self._owner: anyio.TaskInfo | None = None
|
|
113
|
+
self._count = 0
|
|
114
|
+
|
|
115
|
+
async def acquire(self) -> None:
|
|
116
|
+
current_task = anyio.get_current_task()
|
|
117
|
+
if self._owner == current_task:
|
|
118
|
+
self._count += 1
|
|
119
|
+
else:
|
|
120
|
+
await self._lock.acquire()
|
|
121
|
+
self._owner = current_task
|
|
122
|
+
self._count = 1
|
|
123
|
+
|
|
124
|
+
def release(self) -> None:
|
|
125
|
+
if self._owner != anyio.get_current_task():
|
|
126
|
+
raise RuntimeError("Lock can only be released by the owner")
|
|
127
|
+
self._count -= 1
|
|
128
|
+
if self._count == 0:
|
|
129
|
+
self._owner = None
|
|
130
|
+
self._lock.release()
|
|
131
|
+
|
|
132
|
+
async def __aenter__(self) -> Self:
|
|
133
|
+
await self.acquire()
|
|
134
|
+
return self
|
|
135
|
+
|
|
136
|
+
async def __aexit__(
|
|
137
|
+
self,
|
|
138
|
+
exc_type: type[BaseException] | None,
|
|
139
|
+
exc_val: BaseException | None,
|
|
140
|
+
exc_tb: TracebackType | None,
|
|
141
|
+
) -> Any:
|
|
142
|
+
self.release()
|
anydi/ext/pytest_plugin.py
CHANGED
|
@@ -105,8 +105,10 @@ def _anydi_inject(
|
|
|
105
105
|
|
|
106
106
|
try:
|
|
107
107
|
request.node.funcargs[argname] = container.resolve(interface)
|
|
108
|
-
except Exception:
|
|
109
|
-
logger.warning(
|
|
108
|
+
except Exception as exc:
|
|
109
|
+
logger.warning(
|
|
110
|
+
f"Failed to resolve dependency for argument '{argname}'.", exc_info=exc
|
|
111
|
+
)
|
|
110
112
|
_anydi_unresolved.append(interface)
|
|
111
113
|
|
|
112
114
|
|
|
@@ -131,6 +133,8 @@ async def _anydi_ainject(
|
|
|
131
133
|
|
|
132
134
|
try:
|
|
133
135
|
request.node.funcargs[argname] = await container.aresolve(interface)
|
|
134
|
-
except Exception:
|
|
135
|
-
logger.warning(
|
|
136
|
+
except Exception as exc:
|
|
137
|
+
logger.warning(
|
|
138
|
+
f"Failed to resolve dependency for argument '{argname}'.", exc_info=exc
|
|
139
|
+
)
|
|
136
140
|
_anydi_unresolved.append(interface)
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.1
|
|
2
2
|
Name: anydi
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.34.0
|
|
4
4
|
Summary: Dependency Injection library
|
|
5
5
|
Home-page: https://github.com/antonrh/anydi
|
|
6
6
|
License: MIT
|
|
@@ -28,9 +28,8 @@ Classifier: Topic :: Software Development :: Libraries
|
|
|
28
28
|
Classifier: Topic :: Software Development :: Libraries :: Application Frameworks
|
|
29
29
|
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
30
30
|
Classifier: Typing :: Typed
|
|
31
|
-
Provides-Extra: async
|
|
32
31
|
Provides-Extra: docs
|
|
33
|
-
Requires-Dist: anyio (>=3.6.2,<4.0.0)
|
|
32
|
+
Requires-Dist: anyio (>=3.6.2,<4.0.0)
|
|
34
33
|
Requires-Dist: mkdocs (>=1.4.2,<2.0.0) ; extra == "docs"
|
|
35
34
|
Requires-Dist: mkdocs-material (>=9.5.29,<10.0.0) ; extra == "docs"
|
|
36
35
|
Requires-Dist: typing-extensions (>=4.12.1,<5.0.0)
|
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
anydi/__init__.py,sha256=EsR-HiMe8cWS9PQbY23ibc91STK1WTn02DFMPV-TNU4,509
|
|
2
|
-
anydi/_container.py,sha256=
|
|
3
|
-
anydi/_context.py,sha256=
|
|
2
|
+
anydi/_container.py,sha256=p1e8Lrh5JIzrtSsvqYWe8sP-W7dmlpxERspOGS4iivE,21884
|
|
3
|
+
anydi/_context.py,sha256=2QvG4fDnfafXu7yF2EbY8kds96hkKyd8vni_NVmUrRs,12734
|
|
4
4
|
anydi/_logger.py,sha256=UpubJUnW83kffFxkhUlObm2DmZX1Pjqoz9YFKS-JOPg,52
|
|
5
5
|
anydi/_module.py,sha256=cgojC7Z7oMtsUnkfSc65cRYOfZ8Q6KwjusNJzx_VSbk,2729
|
|
6
6
|
anydi/_provider.py,sha256=w_GnRo324aqNORRJwuURexA54c1M3smj34Q8EaV0QGE,6213
|
|
7
7
|
anydi/_scanner.py,sha256=F2sHgJvkRYXYnu4F5iSrnIPVzwnNeS7tRPXziirh4NI,4898
|
|
8
|
-
anydi/_types.py,sha256=
|
|
9
|
-
anydi/_utils.py,sha256=
|
|
8
|
+
anydi/_types.py,sha256=90xdbH2NrFXbridFf9mjOknhcXMW5L0jm92zP_LvKrg,1120
|
|
9
|
+
anydi/_utils.py,sha256=INI0jNIXrJ6LS4zqJymMO2yUEobpxmBGASf4G_vR6AU,4378
|
|
10
10
|
anydi/ext/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
11
11
|
anydi/ext/_utils.py,sha256=U6sRqWzccWUu7eMhbXX1NrwcaxitQF9cO1KxnKF37gw,2566
|
|
12
12
|
anydi/ext/django/__init__.py,sha256=QI1IABCVgSDTUoh7M9WMECKXwB3xvh04HfQ9TOWw1Mk,223
|
|
@@ -21,12 +21,12 @@ anydi/ext/django/ninja/_signature.py,sha256=2cSzKxBIxXLqtwNuH6GSlmjVJFftoGmleWfy
|
|
|
21
21
|
anydi/ext/fastapi.py,sha256=AEL3ubu-LxUPHMMt1YIn3En_JZC7nyBKmKxmhka3O3c,2436
|
|
22
22
|
anydi/ext/faststream.py,sha256=qXnNGvAqWWp9kbhbQUE6EF_OPUiYQmtOH211_O7BI_0,1898
|
|
23
23
|
anydi/ext/pydantic_settings.py,sha256=8IXXLuG_OvKbvKlBkBRQUHcXgbTpgQUxeWyoMcRIUQM,1488
|
|
24
|
-
anydi/ext/pytest_plugin.py,sha256=
|
|
24
|
+
anydi/ext/pytest_plugin.py,sha256=3x_ZYFcLp4ZCRrs7neoohmWz56O9ydm92jxi_LnyD7w,4298
|
|
25
25
|
anydi/ext/starlette/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
26
26
|
anydi/ext/starlette/middleware.py,sha256=PKip_omFZDgg7h2OY-nnV2OIS1MbbmrrOJBwG7_Peuw,793
|
|
27
27
|
anydi/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
28
|
-
anydi-0.
|
|
29
|
-
anydi-0.
|
|
30
|
-
anydi-0.
|
|
31
|
-
anydi-0.
|
|
32
|
-
anydi-0.
|
|
28
|
+
anydi-0.34.0.dist-info/LICENSE,sha256=V6rU8a8fv6o2jQ-7ODHs0XfDFimot8Q6Km6CylRIDTo,1069
|
|
29
|
+
anydi-0.34.0.dist-info/METADATA,sha256=VwSg8GNsINsMsEnlpRkFPxzGDCqYuqfyt5yJ1sL3pR8,5071
|
|
30
|
+
anydi-0.34.0.dist-info/WHEEL,sha256=sP946D7jFCHeNz5Iq4fL4Lu-PrWrFsgfLXbbkciIZwg,88
|
|
31
|
+
anydi-0.34.0.dist-info/entry_points.txt,sha256=GmQblwzxFg42zva1HyBYJJ7TvrTIcSAGBHmyi3bvsi4,42
|
|
32
|
+
anydi-0.34.0.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|