anydi 0.25.1__py3-none-any.whl → 0.26.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
@@ -6,6 +6,7 @@ import contextlib
6
6
  import inspect
7
7
  import types
8
8
  import uuid
9
+ from collections import defaultdict
9
10
  from contextvars import ContextVar
10
11
  from functools import wraps
11
12
  from typing import (
@@ -24,7 +25,7 @@ from typing import (
24
25
  overload,
25
26
  )
26
27
 
27
- from typing_extensions import ParamSpec, final, get_args, get_origin
28
+ from typing_extensions import ParamSpec, Self, final, get_args, get_origin
28
29
 
29
30
  try:
30
31
  from types import NoneType
@@ -85,6 +86,7 @@ class Container:
85
86
  strict: Whether to enable strict mode. Defaults to False.
86
87
  """
87
88
  self._providers: dict[type[Any], Provider] = {}
89
+ self._providers_cache: dict[Scope, list[type[Any]]] = defaultdict(list)
88
90
  self._singleton_context = SingletonContext(self)
89
91
  self._transient_context = TransientContext(self)
90
92
  self._request_context_var: ContextVar[RequestContext | None] = ContextVar(
@@ -170,11 +172,11 @@ class Container:
170
172
 
171
173
  # Create Event type
172
174
  if provider.is_resource and (interface is NoneType or interface is None):
173
- interface = type(f"Event{uuid.uuid4().hex}", (), {})
175
+ interface = type(f"Event_{uuid.uuid4().hex}", (), {})
174
176
 
175
177
  if interface in self._providers:
176
178
  if override:
177
- self._providers[interface] = provider
179
+ self._set_provider(interface, provider)
178
180
  return provider
179
181
 
180
182
  raise LookupError(
@@ -187,7 +189,7 @@ class Container:
187
189
  self._validate_provider_type(provider)
188
190
  self._validate_provider_match_scopes(interface, provider)
189
191
 
190
- self._providers[interface] = provider
192
+ self._set_provider(interface, provider)
191
193
  return provider
192
194
 
193
195
  def unregister(self, interface: AnyInterface) -> None:
@@ -275,6 +277,17 @@ class Container:
275
277
  return self.register(interface, interface, scope=scope or "transient")
276
278
  raise
277
279
 
280
+ def _set_provider(self, interface: AnyInterface, provider: Provider) -> None:
281
+ """Set a provider by interface.
282
+
283
+ Args:
284
+ interface: The interface for which to set the provider.
285
+ provider: The provider object to set.
286
+ """
287
+ self._providers[interface] = provider
288
+ if provider.is_resource:
289
+ self._providers_cache[provider.scope].append(interface)
290
+
278
291
  def _validate_provider_scope(self, provider: Provider) -> None:
279
292
  """Validate the scope of a provider.
280
293
 
@@ -398,11 +411,23 @@ class Container:
398
411
  """
399
412
  self._modules.register(module)
400
413
 
414
+ def __enter__(self) -> Self:
415
+ """Enter the singleton context."""
416
+ self.start()
417
+ return self
418
+
419
+ def __exit__(
420
+ self,
421
+ exc_type: type[BaseException] | None,
422
+ exc_val: BaseException | None,
423
+ exc_tb: types.TracebackType | None,
424
+ ) -> None:
425
+ """Exit the singleton context."""
426
+ self.close()
427
+
401
428
  def start(self) -> None:
402
429
  """Start the singleton context."""
403
- for interface, provider in self._providers.items():
404
- if provider.scope == "singleton":
405
- self.resolve(interface) # noqa
430
+ self._singleton_context.start()
406
431
 
407
432
  def close(self) -> None:
408
433
  """Close the singleton context."""
@@ -428,6 +453,20 @@ class Container:
428
453
  yield
429
454
  self._request_context_var.reset(token)
430
455
 
456
+ async def __aenter__(self) -> Self:
457
+ """Enter the singleton context."""
458
+ await self.astart()
459
+ return self
460
+
461
+ async def __aexit__(
462
+ self,
463
+ exc_type: type[BaseException] | None,
464
+ exc_val: BaseException | None,
465
+ exc_tb: types.TracebackType | None,
466
+ ) -> None:
467
+ """Exit the singleton context."""
468
+ await self.aclose()
469
+
431
470
  async def astart(self) -> None:
432
471
  """Start the singleton context asynchronously."""
433
472
  for interface, provider in self._providers.items():
anydi/_context.py CHANGED
@@ -3,11 +3,11 @@ from __future__ import annotations
3
3
  import abc
4
4
  import contextlib
5
5
  from types import TracebackType
6
- from typing import TYPE_CHECKING, Any, TypeVar, cast
6
+ from typing import TYPE_CHECKING, Any, ClassVar, TypeVar, cast
7
7
 
8
8
  from typing_extensions import Self, final
9
9
 
10
- from ._types import AnyInterface, Interface, Provider
10
+ from ._types import AnyInterface, Interface, Provider, Scope
11
11
  from ._utils import run_async
12
12
 
13
13
  if TYPE_CHECKING:
@@ -19,6 +19,8 @@ T = TypeVar("T")
19
19
  class ScopedContext(abc.ABC):
20
20
  """ScopedContext base class."""
21
21
 
22
+ scope: ClassVar[Scope]
23
+
22
24
  def __init__(self, container: Container) -> None:
23
25
  self.container = container
24
26
 
@@ -233,6 +235,7 @@ class ResourceScopedContext(ScopedContext):
233
235
  Returns:
234
236
  The scoped context.
235
237
  """
238
+ self.start()
236
239
  return self
237
240
 
238
241
  def __exit__(
@@ -251,6 +254,11 @@ class ResourceScopedContext(ScopedContext):
251
254
  self.close()
252
255
  return
253
256
 
257
+ def start(self) -> None:
258
+ """Start the scoped context."""
259
+ for interface in self.container._providers_cache.get(self.scope, []): # noqa
260
+ self.container.resolve(interface)
261
+
254
262
  def close(self) -> None:
255
263
  """Close the scoped context."""
256
264
  self._stack.close()
@@ -261,6 +269,7 @@ class ResourceScopedContext(ScopedContext):
261
269
  Returns:
262
270
  The scoped context.
263
271
  """
272
+ await self.astart()
264
273
  return self
265
274
 
266
275
  async def __aexit__(
@@ -279,6 +288,11 @@ class ResourceScopedContext(ScopedContext):
279
288
  await self.aclose()
280
289
  return
281
290
 
291
+ async def astart(self) -> None:
292
+ """Start the scoped context asynchronously."""
293
+ for interface in self.container._providers_cache.get(self.scope, []): # noqa
294
+ await self.container.aresolve(interface)
295
+
282
296
  async def aclose(self) -> None:
283
297
  """Close the scoped context asynchronously."""
284
298
  await run_async(self._stack.close)
@@ -289,16 +303,22 @@ class ResourceScopedContext(ScopedContext):
289
303
  class SingletonContext(ResourceScopedContext):
290
304
  """A scoped context representing the "singleton" scope."""
291
305
 
306
+ scope = "singleton"
307
+
292
308
 
293
309
  @final
294
310
  class RequestContext(ResourceScopedContext):
295
311
  """A scoped context representing the "request" scope."""
296
312
 
313
+ scope = "request"
314
+
297
315
 
298
316
  @final
299
317
  class TransientContext(ScopedContext):
300
318
  """A scoped context representing the "transient" scope."""
301
319
 
320
+ scope = "transient"
321
+
302
322
  def get(self, interface: Interface[T], provider: Provider) -> T:
303
323
  """Get an instance of a dependency from the transient context.
304
324
 
anydi/_utils.py CHANGED
@@ -8,7 +8,7 @@ import inspect
8
8
  import sys
9
9
  from typing import Any, AsyncIterator, Callable, ForwardRef, Iterator, TypeVar, cast
10
10
 
11
- from typing_extensions import Annotated, ParamSpec, get_origin
11
+ from typing_extensions import ParamSpec, get_args, get_origin
12
12
 
13
13
  try:
14
14
  import anyio # noqa
@@ -33,27 +33,28 @@ P = ParamSpec("P")
33
33
 
34
34
  def get_full_qualname(obj: Any) -> str:
35
35
  """Get the fully qualified name of an object."""
36
- origin = get_origin(obj)
37
- if origin is Annotated:
38
- metadata = ", ".join(
39
- [
40
- f'"{arg}"' if isinstance(arg, str) else str(arg)
41
- for arg in obj.__metadata__
42
- ]
43
- )
44
- return f"Annotated[{get_full_qualname(obj.__args__[0])}, {metadata}]]"
45
-
46
36
  qualname = getattr(obj, "__qualname__", None)
47
- module_name = getattr(obj, "__module__", None)
37
+ module = getattr(obj, "__module__", None)
38
+
48
39
  if qualname is None:
49
40
  qualname = type(obj).__qualname__
50
41
 
51
- if module_name is None:
52
- module_name = type(obj).__module__
42
+ if module is None:
43
+ module = type(obj).__module__
53
44
 
54
- if module_name == builtins.__name__:
45
+ if module == builtins.__name__:
55
46
  return qualname
56
- return f"{module_name}.{qualname}"
47
+
48
+ origin = get_origin(obj)
49
+
50
+ if origin:
51
+ args = ", ".join(
52
+ get_full_qualname(arg) if not isinstance(arg, str) else f'"{arg}"'
53
+ for arg in get_args(obj)
54
+ )
55
+ return f"{get_full_qualname(origin)}[{args}]"
56
+
57
+ return f"{module}.{qualname}"
57
58
 
58
59
 
59
60
  def is_builtin_type(tp: type[Any]) -> bool:
@@ -61,17 +62,19 @@ def is_builtin_type(tp: type[Any]) -> bool:
61
62
  return tp.__module__ == builtins.__name__
62
63
 
63
64
 
64
- def make_forwardref(annotation: str, globalns: dict[str, Any]) -> Any:
65
- """Create a forward reference from a string annotation."""
66
- forward_ref = ForwardRef(annotation)
67
- return evaluate_forwardref(forward_ref, globalns, globalns)
68
-
69
-
70
- def get_typed_annotation(annotation: Any, globalns: dict[str, Any]) -> Any:
65
+ def get_typed_annotation(
66
+ annotation: Any,
67
+ globalns: dict[str, Any],
68
+ module: Any = None,
69
+ is_class: bool = False,
70
+ ) -> Any:
71
71
  """Get the typed annotation of a parameter."""
72
72
  if isinstance(annotation, str):
73
- annotation = ForwardRef(annotation)
74
- annotation = evaluate_forwardref(annotation, globalns, globalns)
73
+ if sys.version_info < (3, 9):
74
+ annotation = ForwardRef(annotation)
75
+ else:
76
+ annotation = ForwardRef(annotation, module=module, is_class=is_class)
77
+ annotation = evaluate_forwardref(annotation, globalns, {})
75
78
  return annotation
76
79
 
77
80
 
@@ -82,15 +85,24 @@ def get_typed_return_annotation(obj: Callable[..., Any]) -> Any:
82
85
  if annotation is inspect.Signature.empty:
83
86
  return None
84
87
  globalns = getattr(obj, "__globals__", {})
85
- return get_typed_annotation(annotation, globalns)
88
+ module = getattr(obj, "__module__", None)
89
+ is_class = inspect.isclass(obj)
90
+ return get_typed_annotation(annotation, globalns, module=module, is_class=is_class)
86
91
 
87
92
 
88
93
  def get_typed_parameters(obj: Callable[..., Any]) -> list[inspect.Parameter]:
89
94
  """Get the typed parameters of a callable object."""
90
95
  globalns = getattr(obj, "__globals__", {})
96
+ module = getattr(obj, "__module__", None)
97
+ is_class = inspect.isclass(obj)
91
98
  return [
92
99
  parameter.replace(
93
- annotation=get_typed_annotation(parameter.annotation, globalns)
100
+ annotation=get_typed_annotation(
101
+ parameter.annotation,
102
+ globalns,
103
+ module=module,
104
+ is_class=is_class,
105
+ )
94
106
  )
95
107
  for name, parameter in inspect.signature(obj).parameters.items()
96
108
  ]
@@ -6,6 +6,7 @@ from typing import Any, Callable, Iterator, cast
6
6
  import pytest
7
7
 
8
8
  from anydi import Container
9
+ from anydi._utils import get_typed_parameters
9
10
 
10
11
 
11
12
  def pytest_configure(config: pytest.Config) -> None:
@@ -63,14 +64,14 @@ def _anydi_injected_parameter_iterator(
63
64
  _anydi_unresolved: list[str],
64
65
  ) -> Callable[[], Iterator[tuple[str, Any]]]:
65
66
  def _iterator() -> Iterator[tuple[str, inspect.Parameter]]:
66
- for name, parameter in inspect.signature(request.function).parameters.items():
67
+ for parameter in get_typed_parameters(request.function):
67
68
  if (
68
69
  ((interface := parameter.annotation) is parameter.empty)
69
70
  or interface in _anydi_unresolved
70
- or name in request.node.funcargs
71
+ or parameter.name in request.node.funcargs
71
72
  ):
72
73
  continue
73
- yield name, interface
74
+ yield parameter.name, interface
74
75
 
75
76
  return _iterator
76
77
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: anydi
3
- Version: 0.25.1
3
+ Version: 0.26.0
4
4
  Summary: Dependency Injection library
5
5
  Home-page: https://github.com/antonrh/anydi
6
6
  License: MIT
@@ -1,11 +1,11 @@
1
1
  anydi/__init__.py,sha256=aeaBp5vq09sG-e9sqqs9qpUtUIDNfOdFPrlAfE5Ku9E,584
2
- anydi/_container.py,sha256=rZ0HgWFC7jJuZo7iLjMYnTm4utWBMOeiaPThz8a5sbY,27996
3
- anydi/_context.py,sha256=k956mFE_pfPdU0fxOJ8YRHBZx7sU_ln8fheYNofbmSs,10215
2
+ anydi/_container.py,sha256=7G0z8NkVzwZA_kXhyPymuGN-xGPWoCoXccIT4brGWLQ,29168
3
+ anydi/_context.py,sha256=e0VX0fiflzW_2O9w3HvUH3YRCwXHsruQjf3Lu-zXgDw,10815
4
4
  anydi/_logger.py,sha256=UpubJUnW83kffFxkhUlObm2DmZX1Pjqoz9YFKS-JOPg,52
5
5
  anydi/_module.py,sha256=E1TfLud_Af-MPB83PxIzHVA1jlDW2FGaRP_il1a6y3Y,3675
6
6
  anydi/_scanner.py,sha256=cyEk-K2Q8ssZStq8GrxMeEcCuAZMw-RXrjlgWEevKCs,6667
7
7
  anydi/_types.py,sha256=vQTrFjsYhlMxfo1nOFem05x2QUJMQkVh4ZaC7W0XZJY,3434
8
- anydi/_utils.py,sha256=XHVNkd-__SKlWlyeGE2e1Yi-DBr4DPWzZOIVbTrQyMI,3692
8
+ anydi/_utils.py,sha256=zP4UvO1aVQJTB8pFNUWAcncvSiuhcg4xNdRU7CoLrqw,3871
9
9
  anydi/ext/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
10
10
  anydi/ext/django/__init__.py,sha256=QI1IABCVgSDTUoh7M9WMECKXwB3xvh04HfQ9TOWw1Mk,223
11
11
  anydi/ext/django/_container.py,sha256=cxVoYQG16WP0S_Yv4TnLwuaaT7NVEOhLWO-YdALJUb4,418
@@ -17,12 +17,12 @@ anydi/ext/django/ninja/__init__.py,sha256=kW3grUgWp_nkWSG_-39ADHMrZLGNcj9TsJ9OW8
17
17
  anydi/ext/django/ninja/_operation.py,sha256=wSWa7D73XTVlOibmOciv2l6JHPe1ERZcXrqI8W-oO2w,2696
18
18
  anydi/ext/django/ninja/_signature.py,sha256=2cSzKxBIxXLqtwNuH6GSlmjVJFftoGmleWfyk_NVEWw,2207
19
19
  anydi/ext/fastapi.py,sha256=kVUKVKtqCx1Nfnm1oh2BMyB0G7qQKPw6OGfxFlqUqtc,5305
20
- anydi/ext/pytest_plugin.py,sha256=vtjQCwQ0_saG8qhYAYn2wQzXVrXfwXOEhJlTjGqtXA8,3999
20
+ anydi/ext/pytest_plugin.py,sha256=nDNqjblVQufLha6P8J8hZw4Q0EuwC71bOI_bdaGm-OQ,4043
21
21
  anydi/ext/starlette/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
22
22
  anydi/ext/starlette/middleware.py,sha256=Ni0BQaPjs_Ha6zcLZYYJ3-XkslTCnL9aCSa06rnRDMI,1139
23
23
  anydi/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
24
- anydi-0.25.1.dist-info/LICENSE,sha256=V6rU8a8fv6o2jQ-7ODHs0XfDFimot8Q6Km6CylRIDTo,1069
25
- anydi-0.25.1.dist-info/METADATA,sha256=fB_AJZKJ6uqYM9Sd4O5ULmFJWuyJq1i1Vi1zV5AyBJE,5160
26
- anydi-0.25.1.dist-info/WHEEL,sha256=FMvqSimYX_P7y0a7UY-_Mc83r5zkBZsCYPm7Lr0Bsq4,88
27
- anydi-0.25.1.dist-info/entry_points.txt,sha256=GmQblwzxFg42zva1HyBYJJ7TvrTIcSAGBHmyi3bvsi4,42
28
- anydi-0.25.1.dist-info/RECORD,,
24
+ anydi-0.26.0.dist-info/LICENSE,sha256=V6rU8a8fv6o2jQ-7ODHs0XfDFimot8Q6Km6CylRIDTo,1069
25
+ anydi-0.26.0.dist-info/METADATA,sha256=7Y5qjNdubHH60T1MZa3UL_3xDUam-vXKI_LS7M0uRzE,5160
26
+ anydi-0.26.0.dist-info/WHEEL,sha256=FMvqSimYX_P7y0a7UY-_Mc83r5zkBZsCYPm7Lr0Bsq4,88
27
+ anydi-0.26.0.dist-info/entry_points.txt,sha256=GmQblwzxFg42zva1HyBYJJ7TvrTIcSAGBHmyi3bvsi4,42
28
+ anydi-0.26.0.dist-info/RECORD,,
File without changes