anydi 0.33.1__tar.gz → 0.34.0__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.
Files changed (31) hide show
  1. {anydi-0.33.1 → anydi-0.34.0}/PKG-INFO +2 -3
  2. {anydi-0.33.1 → anydi-0.34.0}/anydi/_container.py +14 -3
  3. {anydi-0.33.1 → anydi-0.34.0}/anydi/_context.py +8 -5
  4. {anydi-0.33.1 → anydi-0.34.0}/anydi/_utils.py +49 -12
  5. {anydi-0.33.1 → anydi-0.34.0}/pyproject.toml +5 -6
  6. {anydi-0.33.1 → anydi-0.34.0}/LICENSE +0 -0
  7. {anydi-0.33.1 → anydi-0.34.0}/README.md +0 -0
  8. {anydi-0.33.1 → anydi-0.34.0}/anydi/__init__.py +0 -0
  9. {anydi-0.33.1 → anydi-0.34.0}/anydi/_logger.py +0 -0
  10. {anydi-0.33.1 → anydi-0.34.0}/anydi/_module.py +0 -0
  11. {anydi-0.33.1 → anydi-0.34.0}/anydi/_provider.py +0 -0
  12. {anydi-0.33.1 → anydi-0.34.0}/anydi/_scanner.py +0 -0
  13. {anydi-0.33.1 → anydi-0.34.0}/anydi/_types.py +0 -0
  14. {anydi-0.33.1 → anydi-0.34.0}/anydi/ext/__init__.py +0 -0
  15. {anydi-0.33.1 → anydi-0.34.0}/anydi/ext/_utils.py +0 -0
  16. {anydi-0.33.1 → anydi-0.34.0}/anydi/ext/django/__init__.py +0 -0
  17. {anydi-0.33.1 → anydi-0.34.0}/anydi/ext/django/_container.py +0 -0
  18. {anydi-0.33.1 → anydi-0.34.0}/anydi/ext/django/_settings.py +0 -0
  19. {anydi-0.33.1 → anydi-0.34.0}/anydi/ext/django/_utils.py +0 -0
  20. {anydi-0.33.1 → anydi-0.34.0}/anydi/ext/django/apps.py +0 -0
  21. {anydi-0.33.1 → anydi-0.34.0}/anydi/ext/django/middleware.py +0 -0
  22. {anydi-0.33.1 → anydi-0.34.0}/anydi/ext/django/ninja/__init__.py +0 -0
  23. {anydi-0.33.1 → anydi-0.34.0}/anydi/ext/django/ninja/_operation.py +0 -0
  24. {anydi-0.33.1 → anydi-0.34.0}/anydi/ext/django/ninja/_signature.py +0 -0
  25. {anydi-0.33.1 → anydi-0.34.0}/anydi/ext/fastapi.py +0 -0
  26. {anydi-0.33.1 → anydi-0.34.0}/anydi/ext/faststream.py +0 -0
  27. {anydi-0.33.1 → anydi-0.34.0}/anydi/ext/pydantic_settings.py +0 -0
  28. {anydi-0.33.1 → anydi-0.34.0}/anydi/ext/pytest_plugin.py +0 -0
  29. {anydi-0.33.1 → anydi-0.34.0}/anydi/ext/starlette/__init__.py +0 -0
  30. {anydi-0.33.1 → anydi-0.34.0}/anydi/ext/starlette/middleware.py +0 -0
  31. {anydi-0.33.1 → anydi-0.34.0}/anydi/py.typed +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: anydi
3
- Version: 0.33.1
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)
@@ -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
@@ -26,7 +27,7 @@ from ._module import Module, ModuleRegistry
26
27
  from ._provider import Provider
27
28
  from ._scanner import Scanner
28
29
  from ._types import AnyInterface, DependencyWrapper, Interface, Scope, is_marker
29
- from ._utils import get_full_qualname, get_typed_parameters, is_builtin_type
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,7 +369,11 @@ 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)
@@ -384,7 +391,11 @@ class Container:
384
391
 
385
392
  provider = self._get_or_register_provider(interface)
386
393
  scoped_context = self._get_scoped_context(provider.scope)
387
- 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)
388
399
  if self.testing and created:
389
400
  self._patch_test_resolver(instance)
390
401
  return cast(T, instance)
@@ -10,7 +10,12 @@ from typing_extensions import Self, final
10
10
 
11
11
  from ._provider import CallableKind, Provider
12
12
  from ._types import AnyInterface, DependencyWrapper, Scope, is_event_type
13
- from ._utils import get_full_qualname, run_async
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
@@ -188,7 +193,7 @@ class ResourceScopedContext(ScopedContext):
188
193
  """Create an instance using the provider."""
189
194
  instance = super()._create_instance(provider)
190
195
  # Enter the context manager if the instance is closable.
191
- if hasattr(instance, "__enter__") and hasattr(instance, "__exit__"):
196
+ if is_context_manager(instance):
192
197
  self._stack.enter_context(instance)
193
198
  return instance
194
199
 
@@ -202,7 +207,7 @@ class ResourceScopedContext(ScopedContext):
202
207
  """Create an instance asynchronously using the provider."""
203
208
  instance = await super()._acreate_instance(provider)
204
209
  # Enter the context manager if the instance is closable.
205
- if hasattr(instance, "__aenter__") and hasattr(instance, "__aexit__"):
210
+ if is_async_context_manager(instance):
206
211
  await self._async_stack.enter_async_context(instance)
207
212
  return instance
208
213
 
@@ -233,8 +238,6 @@ class ResourceScopedContext(ScopedContext):
233
238
  @abc.abstractmethod
234
239
  def start(self) -> None:
235
240
  """Start the scoped context."""
236
- for interface in self.container._resource_cache.get(self.scope, []): # noqa
237
- self.container.resolve(interface)
238
241
 
239
242
  def close(self) -> None:
240
243
  """Close the scoped context."""
@@ -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()
@@ -1,6 +1,6 @@
1
1
  [tool.poetry]
2
2
  name = "anydi"
3
- version = "0.33.1"
3
+ version = "0.34.0"
4
4
  description = "Dependency Injection library"
5
5
  authors = ["Anton Ruhlov <antonruhlov@gmail.com>"]
6
6
  license = "MIT"
@@ -36,19 +36,18 @@ packages = [
36
36
  [tool.poetry.dependencies]
37
37
  python = "^3.9"
38
38
  typing-extensions = "^4.12.1"
39
- anyio = { version = "^3.6.2", optional = true }
39
+ anyio = "^3.6.2"
40
40
  mkdocs = { version = "^1.4.2", optional = true }
41
41
  mkdocs-material = { version = "^9.5.29", optional = true }
42
42
 
43
43
  [tool.poetry.extras]
44
44
  docs = ["mkdocs", "mkdocs-material"]
45
- async = ["anyio"]
46
45
 
47
46
  [tool.poetry.group.dev.dependencies]
48
- mypy = { version = "^1.14.0", extras = ["faster-cache"] }
49
- ruff = "^0.8.4"
47
+ mypy = { version = "^1.14.1", extras = ["faster-cache"] }
48
+ ruff = "^0.8.5"
50
49
  pytest = "^8.3.1"
51
- pytest-cov = "^5.0.0"
50
+ pytest-cov = "^6.0.0"
52
51
  fastapi = "^0.100.0"
53
52
  httpx = "^0.26.0"
54
53
  django = "^4.2"
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes