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 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, TestInterface, is_marker
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
- instance, created = scoped_context.get_or_create(provider)
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
- instance, created = await scoped_context.aget_or_create(provider)
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, Scope, TestInterface, is_event_type
13
- from ._utils import get_full_qualname, run_async
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 = TestInterface(interface=parameter.annotation)
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 = TestInterface(interface=parameter.annotation)
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 hasattr(instance, "__enter__") and hasattr(instance, "__exit__"):
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 hasattr(instance, "__aenter__") and hasattr(instance, "__aexit__"):
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 TestInterface:
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
- from typing_extensions import ParamSpec, get_args, get_origin
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()
@@ -105,8 +105,10 @@ def _anydi_inject(
105
105
 
106
106
  try:
107
107
  request.node.funcargs[argname] = container.resolve(interface)
108
- except Exception: # noqa
109
- logger.warning(f"Failed to resolve dependency for argument '{argname}'.")
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: # noqa
135
- logger.warning(f"Failed to resolve dependency for argument '{argname}'.")
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.33.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) ; extra == "async"
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=M1V7OMYp1BGSj0RQInS6xXYi3HgDGQ9CwjDezvC-fQc,21538
3
- anydi/_context.py,sha256=vBbL0POdkdL-kr2CEp35gQvzIBlFN2q7-wCNhr6gJqg,12708
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=GK6grYnmcCQODhtDvqn224IU0FXqV9Grm6oj4MDQCPY,902
9
- anydi/_utils.py,sha256=guw4sFCvsisJmneKWlZi18YDYll_CjlO_f2cH97rDFQ,3280
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=Df0pFgAOk-44UFsdZGAOSxElJTJLchs4sk2UZuV-KVk,4212
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.33.0.dist-info/LICENSE,sha256=V6rU8a8fv6o2jQ-7ODHs0XfDFimot8Q6Km6CylRIDTo,1069
29
- anydi-0.33.0.dist-info/METADATA,sha256=NASeDNxy6DOKFk8hNXO7QAnOmWLgqFvJ0qNK8qlL7BE,5112
30
- anydi-0.33.0.dist-info/WHEEL,sha256=sP946D7jFCHeNz5Iq4fL4Lu-PrWrFsgfLXbbkciIZwg,88
31
- anydi-0.33.0.dist-info/entry_points.txt,sha256=GmQblwzxFg42zva1HyBYJJ7TvrTIcSAGBHmyi3bvsi4,42
32
- anydi-0.33.0.dist-info/RECORD,,
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