anydi 0.55.0__py3-none-any.whl → 0.56.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
@@ -339,6 +339,7 @@ class Container:
339
339
  default=default,
340
340
  has_default=has_default,
341
341
  provider=sub_provider,
342
+ shared_scope=sub_provider.scope == scope and scope != "transient",
342
343
  )
343
344
  )
344
345
 
@@ -389,10 +390,10 @@ class Container:
389
390
  @staticmethod
390
391
  def _validate_provider_scope(scope: Scope, name: str, is_resource: bool) -> None:
391
392
  """Validate the provider scope."""
392
- if scope not in (allowed_scopes := get_args(Scope)):
393
+ if scope not in ALLOWED_SCOPES:
393
394
  raise ValueError(
394
395
  f"The provider `{name}` scope is invalid. Only the following "
395
- f"scopes are supported: {', '.join(allowed_scopes)}. "
396
+ f"scopes are supported: {', '.join(ALLOWED_SCOPES)}. "
396
397
  "Please use one of the supported scopes when registering a provider."
397
398
  )
398
399
  if scope == "transient" and is_resource:
@@ -670,13 +671,17 @@ class Container:
670
671
  ) -> None:
671
672
  self._scanner.scan(packages=packages, tags=tags)
672
673
 
673
- # == Testing ==
674
+ # == Testing / Override Support ==
674
675
 
675
676
  @contextlib.contextmanager
676
677
  def override(self, interface: Any, instance: Any) -> Iterator[None]:
677
- raise RuntimeError(
678
- "Dependency overriding is not supported in this container.\n"
679
- "Wrap your container with `anydi.testing.Container` instead.\n"
680
- "Example:\n\n"
681
- " container = TestContainer.from_container(container)"
682
- )
678
+ """Override a dependency with a specific instance for testing."""
679
+ if not self.has_provider_for(interface):
680
+ raise LookupError(
681
+ f"The provider interface `{type_repr(interface)}` not registered."
682
+ )
683
+ self._resolver.add_override(interface, instance)
684
+ try:
685
+ yield
686
+ finally:
687
+ self._resolver.remove_override(interface)
anydi/_provider.py CHANGED
@@ -40,6 +40,7 @@ class ProviderParameter:
40
40
  default: Any
41
41
  has_default: bool
42
42
  provider: Provider | None = None
43
+ shared_scope: bool = False
43
44
 
44
45
 
45
46
  @dataclass(frozen=True, slots=True)
anydi/_resolver.py CHANGED
@@ -6,6 +6,7 @@ import contextlib
6
6
  from typing import TYPE_CHECKING, Any, NamedTuple
7
7
 
8
8
  import anyio.to_thread
9
+ import wrapt # type: ignore
9
10
  from typing_extensions import type_repr
10
11
 
11
12
  from ._provider import Provider
@@ -15,6 +16,18 @@ if TYPE_CHECKING:
15
16
  from ._container import Container
16
17
 
17
18
 
19
+ class InstanceProxy(wrapt.ObjectProxy): # type: ignore
20
+ """Proxy for dependency instances to enable override support."""
21
+
22
+ def __init__(self, wrapped: Any, *, interface: type[Any]) -> None:
23
+ super().__init__(wrapped) # type: ignore
24
+ self._self_interface = interface
25
+
26
+ @property
27
+ def interface(self) -> type[Any]:
28
+ return self._self_interface
29
+
30
+
18
31
  class CompiledResolver(NamedTuple):
19
32
  resolve: Any
20
33
  create: Any
@@ -24,32 +37,46 @@ class Resolver:
24
37
  def __init__(self, container: Container) -> None:
25
38
  self._container = container
26
39
  self._unresolved_interfaces: set[Any] = set()
40
+ # Normal caches (fast path, no override checks)
27
41
  self._cache: dict[Any, CompiledResolver] = {}
28
42
  self._async_cache: dict[Any, CompiledResolver] = {}
43
+ # Override caches (with override support)
44
+ self._override_cache: dict[Any, CompiledResolver] = {}
45
+ self._async_override_cache: dict[Any, CompiledResolver] = {}
46
+ # Override instances storage
47
+ self._override_instances: dict[Any, Any] = {}
29
48
 
30
- # Determine compilation flags based on whether methods are overridden
31
- self._has_override_support = callable(
32
- getattr(self._container, "_hook_override_for", None)
33
- )
34
- self._wrap_dependencies = callable(
35
- getattr(self._container, "_hook_wrap_dependency", None)
36
- )
37
- self._wrap_instance = callable(
38
- getattr(self._container, "_hook_post_resolve", None)
39
- )
49
+ @property
50
+ def override_mode(self) -> bool:
51
+ """Check if override mode is enabled."""
52
+ return bool(self._override_instances)
53
+
54
+ def add_override(self, interface: Any, instance: Any) -> None:
55
+ """Add an override instance for an interface."""
56
+ self._override_instances[interface] = instance
57
+
58
+ def remove_override(self, interface: Any) -> None:
59
+ """Remove an override instance for an interface."""
60
+ self._override_instances.pop(interface, None)
40
61
 
41
62
  def add_unresolved(self, interface: Any) -> None:
42
63
  self._unresolved_interfaces.add(interface)
43
64
 
44
65
  def get_cached(self, interface: Any, *, is_async: bool) -> CompiledResolver | None:
45
66
  """Get cached resolver if it exists."""
46
- cache = self._async_cache if is_async else self._cache
67
+ if self.override_mode:
68
+ cache = self._async_override_cache if is_async else self._override_cache
69
+ else:
70
+ cache = self._async_cache if is_async else self._cache
47
71
  return cache.get(interface)
48
72
 
49
73
  def compile(self, provider: Provider, *, is_async: bool) -> CompiledResolver:
50
74
  """Compile an optimized resolver function for the given provider."""
51
- # Select the appropriate cache based on sync/async mode
52
- cache = self._async_cache if is_async else self._cache
75
+ # Select the appropriate cache based on sync/async mode and override mode
76
+ if self.override_mode:
77
+ cache = self._async_override_cache if is_async else self._override_cache
78
+ else:
79
+ cache = self._async_cache if is_async else self._cache
53
80
 
54
81
  # Check if already compiled in cache
55
82
  if provider.interface in cache:
@@ -61,35 +88,79 @@ class Resolver:
61
88
  self.compile(p.provider, is_async=is_async)
62
89
 
63
90
  # Compile the resolver and creator functions
64
- compiled = self._compile_resolver(provider, is_async=is_async)
91
+ compiled = self._compile_resolver(
92
+ provider, is_async=is_async, with_override=self.override_mode
93
+ )
65
94
 
66
95
  # Store the compiled functions in the cache
67
96
  cache[provider.interface] = compiled
68
97
 
69
98
  return compiled
70
99
 
100
+ def _add_override_check(
101
+ self, lines: list[str], *, include_not_set: bool = False
102
+ ) -> None:
103
+ """Add override checking code to generated resolver."""
104
+ lines.append(" override_mode = resolver.override_mode")
105
+ lines.append(" if override_mode:")
106
+ if include_not_set:
107
+ lines.append(" NOT_SET_ = _NOT_SET")
108
+ lines.append(" override = resolver._get_override_for(_interface)")
109
+ lines.append(" if override is not NOT_SET_:")
110
+ lines.append(" return override")
111
+
112
+ def _add_create_call(
113
+ self,
114
+ lines: list[str],
115
+ *,
116
+ is_async: bool,
117
+ with_override: bool,
118
+ context: str,
119
+ store: bool,
120
+ defaults: str = "None",
121
+ indent: str = " ",
122
+ ) -> None:
123
+ """Add _create_instance call to generated resolver."""
124
+ override_arg = "override_mode" if with_override else "False"
125
+ context_arg = context if context else "None"
126
+ store_arg = "True" if store else "False"
127
+
128
+ if is_async:
129
+ lines.append(
130
+ f"{indent}return await _create_instance("
131
+ f"container, {context_arg}, {store_arg}, {defaults}, {override_arg})"
132
+ )
133
+ else:
134
+ lines.append(
135
+ f"{indent}return _create_instance("
136
+ f"container, {context_arg}, {store_arg}, {defaults}, {override_arg})"
137
+ )
138
+
71
139
  def _compile_resolver( # noqa: C901
72
- self, provider: Provider, *, is_async: bool
140
+ self, provider: Provider, *, is_async: bool, with_override: bool = False
73
141
  ) -> CompiledResolver:
74
142
  """Compile optimized resolver functions for the given provider."""
75
- has_override_support = self._has_override_support
76
- wrap_dependencies = self._wrap_dependencies
77
- wrap_instance = self._wrap_instance
78
143
  num_params = len(provider.parameters)
79
144
  param_resolvers: list[Any] = [None] * num_params
80
145
  param_annotations: list[Any] = [None] * num_params
81
146
  param_defaults: list[Any] = [None] * num_params
82
147
  param_has_default: list[bool] = [False] * num_params
83
148
  param_names: list[str] = [""] * num_params
149
+ param_shared_scopes: list[bool] = [False] * num_params
84
150
  unresolved_messages: list[str] = [""] * num_params
85
151
 
86
- cache = self._async_cache if is_async else self._cache
152
+ cache = (
153
+ (self._async_override_cache if is_async else self._override_cache)
154
+ if with_override
155
+ else (self._async_cache if is_async else self._cache)
156
+ )
87
157
 
88
158
  for idx, p in enumerate(provider.parameters):
89
159
  param_annotations[idx] = p.annotation
90
160
  param_defaults[idx] = p.default
91
161
  param_has_default[idx] = p.has_default
92
162
  param_names[idx] = p.name
163
+ param_shared_scopes[idx] = p.shared_scope
93
164
 
94
165
  if p.provider is not None:
95
166
  compiled = cache.get(p.provider.interface)
@@ -98,13 +169,13 @@ class Resolver:
98
169
  cache[p.provider.interface] = compiled
99
170
  param_resolvers[idx] = compiled.resolve
100
171
 
101
- msg = (
172
+ unresolved_message = (
102
173
  f"You are attempting to get the parameter `{p.name}` with the "
103
174
  f"annotation `{type_repr(p.annotation)}` as a dependency into "
104
175
  f"`{type_repr(provider.call)}` which is not registered or set in the "
105
176
  "scoped context."
106
177
  )
107
- unresolved_messages[idx] = msg
178
+ unresolved_messages[idx] = unresolved_message
108
179
 
109
180
  scope = provider.scope
110
181
  is_generator = provider.is_generator
@@ -115,11 +186,13 @@ class Resolver:
115
186
  create_lines: list[str] = []
116
187
  if is_async:
117
188
  create_lines.append(
118
- "async def _create_instance(container, context, store, defaults):"
189
+ "async def _create_instance("
190
+ "container, context, store, defaults, override_mode):"
119
191
  )
120
192
  else:
121
193
  create_lines.append(
122
- "def _create_instance(container, context, store, defaults):"
194
+ "def _create_instance("
195
+ "container, context, store, defaults, override_mode):"
123
196
  )
124
197
 
125
198
  if no_params:
@@ -168,8 +241,10 @@ class Resolver:
168
241
  create_lines.append(
169
242
  f" raise LookupError(_unresolved_messages[{idx}])"
170
243
  )
171
- create_lines.append(f" resolver = _param_resolvers[{idx}]")
172
- create_lines.append(" if resolver is None:")
244
+ create_lines.append(
245
+ f" _dep_resolver = _param_resolvers[{idx}]"
246
+ )
247
+ create_lines.append(" if _dep_resolver is None:")
173
248
  create_lines.append(" try:")
174
249
  if is_async:
175
250
  create_lines.append(
@@ -191,7 +266,8 @@ class Resolver:
191
266
  )
192
267
  create_lines.append(
193
268
  f" arg_{idx} = "
194
- f"await compiled[0](container, context)"
269
+ f"await compiled[0](container, "
270
+ f"context if _param_shared_scopes[{idx}] else None)"
195
271
  )
196
272
  else:
197
273
  create_lines.append(
@@ -212,7 +288,8 @@ class Resolver:
212
288
  )
213
289
  create_lines.append(
214
290
  f" arg_{idx} = "
215
- f"compiled[0](container, context)"
291
+ f"compiled[0](container, "
292
+ f"context if _param_shared_scopes[{idx}] else None)"
216
293
  )
217
294
  create_lines.append(" except LookupError:")
218
295
  create_lines.append(
@@ -226,18 +303,23 @@ class Resolver:
226
303
  create_lines.append(" else:")
227
304
  if is_async:
228
305
  create_lines.append(
229
- f" arg_{idx} = await resolver("
230
- f"container, context)"
306
+ f" arg_{idx} = await _dep_resolver("
307
+ f"container, "
308
+ f"context if _param_shared_scopes[{idx}] else None)"
231
309
  )
232
310
  else:
233
311
  create_lines.append(
234
- f" arg_{idx} = resolver(container, context)"
312
+ f" arg_{idx} = _dep_resolver("
313
+ f"container, "
314
+ f"context if _param_shared_scopes[{idx}] else None)"
235
315
  )
236
316
  create_lines.append(" else:")
237
317
  create_lines.append(f" arg_{idx} = cached")
238
- if wrap_dependencies:
318
+ # Wrap dependencies if in override mode (only for override version)
319
+ if with_override:
320
+ create_lines.append(" if override_mode:")
239
321
  create_lines.append(
240
- f" arg_{idx} = container._hook_wrap_dependency("
322
+ f" arg_{idx} = resolver._wrap_for_override("
241
323
  f"_param_annotations[{idx}], arg_{idx})"
242
324
  )
243
325
 
@@ -373,9 +455,11 @@ class Resolver:
373
455
  create_lines.append(" if context is not None and store:")
374
456
  create_lines.append(" context.set(_interface, inst)")
375
457
 
376
- if wrap_instance:
458
+ # Wrap instance if in override mode (only for override version)
459
+ if with_override:
460
+ create_lines.append(" if override_mode:")
377
461
  create_lines.append(
378
- " inst = container._hook_post_resolve(_interface, inst)"
462
+ " inst = resolver._post_resolve_override(_interface, inst)"
379
463
  )
380
464
  create_lines.append(" return inst")
381
465
 
@@ -386,7 +470,7 @@ class Resolver:
386
470
  resolver_lines.append("def _resolver(container, context=None):")
387
471
 
388
472
  # Only define NOT_SET_ if we actually need it
389
- needs_not_set = has_override_support or scope in ("singleton", "request")
473
+ needs_not_set = scope in ("singleton", "request")
390
474
  if needs_not_set:
391
475
  resolver_lines.append(" NOT_SET_ = _NOT_SET")
392
476
 
@@ -399,22 +483,14 @@ class Resolver:
399
483
  else:
400
484
  resolver_lines.append(" context = None")
401
485
 
402
- if has_override_support:
403
- resolver_lines.append(
404
- " override = container._hook_override_for(_interface)"
405
- )
406
- resolver_lines.append(" if override is not NOT_SET_:")
407
- resolver_lines.append(" return override")
408
-
409
486
  if scope == "singleton":
487
+ if with_override:
488
+ self._add_override_check(resolver_lines)
489
+
490
+ # Fast path: check cached instance
410
491
  resolver_lines.append(" inst = context.get(_interface)")
411
492
  resolver_lines.append(" if inst is not NOT_SET_:")
412
- if wrap_instance:
413
- resolver_lines.append(
414
- " return container._hook_post_resolve(_provider, inst)"
415
- )
416
- else:
417
- resolver_lines.append(" return inst")
493
+ resolver_lines.append(" return inst")
418
494
 
419
495
  if is_async:
420
496
  resolver_lines.append(" async with context.alock():")
@@ -422,47 +498,43 @@ class Resolver:
422
498
  resolver_lines.append(" with context.lock():")
423
499
  resolver_lines.append(" inst = context.get(_interface)")
424
500
  resolver_lines.append(" if inst is not NOT_SET_:")
425
- if wrap_instance:
426
- resolver_lines.append(
427
- " return container._hook_post_resolve(_provider, inst)"
428
- )
429
- else:
430
- resolver_lines.append(" return inst")
431
- if is_async:
432
- resolver_lines.append(
433
- " return await "
434
- "_create_instance(container, context, True, None)"
435
- )
436
- else:
437
- resolver_lines.append(
438
- " return _create_instance(container, context, True, None)"
439
- )
501
+ resolver_lines.append(" return inst")
502
+ self._add_create_call(
503
+ resolver_lines,
504
+ is_async=is_async,
505
+ with_override=with_override,
506
+ context="context",
507
+ store=True,
508
+ indent=" ",
509
+ )
440
510
  elif scope == "request":
511
+ if with_override:
512
+ self._add_override_check(resolver_lines)
513
+
514
+ # Fast path: check cached instance
441
515
  resolver_lines.append(" inst = context.get(_interface)")
442
516
  resolver_lines.append(" if inst is not NOT_SET_:")
443
- if wrap_instance:
444
- resolver_lines.append(
445
- " return container._hook_post_resolve(_provider, inst)"
446
- )
447
- else:
448
- resolver_lines.append(" return inst")
449
- if is_async:
450
- resolver_lines.append(
451
- " return await _create_instance(container, context, True, None)"
452
- )
453
- else:
454
- resolver_lines.append(
455
- " return _create_instance(container, context, True, None)"
456
- )
517
+ resolver_lines.append(" return inst")
518
+
519
+ self._add_create_call(
520
+ resolver_lines,
521
+ is_async=is_async,
522
+ with_override=with_override,
523
+ context="context",
524
+ store=True,
525
+ )
457
526
  else:
458
- if is_async:
459
- resolver_lines.append(
460
- " return await _create_instance(container, None, False, None)"
461
- )
462
- else:
463
- resolver_lines.append(
464
- " return _create_instance(container, None, False, None)"
465
- )
527
+ # Transient scope
528
+ if with_override:
529
+ self._add_override_check(resolver_lines, include_not_set=True)
530
+
531
+ self._add_create_call(
532
+ resolver_lines,
533
+ is_async=is_async,
534
+ with_override=with_override,
535
+ context="",
536
+ store=False,
537
+ )
466
538
 
467
539
  create_resolver_lines: list[str] = []
468
540
  if is_async:
@@ -474,9 +546,9 @@ class Resolver:
474
546
  "def _resolver_create(container, defaults=None):"
475
547
  )
476
548
 
477
- # Only define NOT_SET_ if needed for override support
478
- if has_override_support:
479
- create_resolver_lines.append(" NOT_SET_ = _NOT_SET")
549
+ if with_override:
550
+ # Cache override mode check
551
+ create_resolver_lines.append(" override_mode = resolver.override_mode")
480
552
 
481
553
  if scope == "singleton":
482
554
  create_resolver_lines.append(" context = container._singleton_context")
@@ -487,43 +559,20 @@ class Resolver:
487
559
  else:
488
560
  create_resolver_lines.append(" context = None")
489
561
 
490
- if has_override_support:
491
- create_resolver_lines.append(
492
- " override = container._hook_override_for(_interface)"
493
- )
494
- create_resolver_lines.append(" if override is not NOT_SET_:")
495
- create_resolver_lines.append(" return override")
562
+ if with_override:
563
+ self._add_override_check(create_resolver_lines, include_not_set=True)
496
564
 
497
- if scope == "singleton":
498
- if is_async:
499
- create_resolver_lines.append(
500
- " return await "
501
- "_create_instance(container, context, False, defaults)"
502
- )
503
- else:
504
- create_resolver_lines.append(
505
- " return _create_instance(container, context, False, defaults)"
506
- )
507
- elif scope == "request":
508
- if is_async:
509
- create_resolver_lines.append(
510
- " return await "
511
- "_create_instance(container, context, False, defaults)"
512
- )
513
- else:
514
- create_resolver_lines.append(
515
- " return _create_instance(container, context, False, defaults)"
516
- )
517
- else:
518
- if is_async:
519
- create_resolver_lines.append(
520
- " return await "
521
- "_create_instance(container, None, False, defaults)"
522
- )
523
- else:
524
- create_resolver_lines.append(
525
- " return _create_instance(container, None, False, defaults)"
526
- )
565
+ # Determine context for create call
566
+ context_arg = "context" if scope in ("singleton", "request") else ""
567
+
568
+ self._add_create_call(
569
+ create_resolver_lines,
570
+ is_async=is_async,
571
+ with_override=with_override,
572
+ context=context_arg,
573
+ store=False,
574
+ defaults="defaults",
575
+ )
527
576
 
528
577
  lines = create_lines + [""] + resolver_lines + [""] + create_resolver_lines
529
578
 
@@ -539,13 +588,19 @@ class Resolver:
539
588
  "_param_defaults": param_defaults,
540
589
  "_param_has_default": param_has_default,
541
590
  "_param_resolvers": param_resolvers,
591
+ "_param_shared_scopes": param_shared_scopes,
542
592
  "_unresolved_messages": unresolved_messages,
543
593
  "_unresolved_interfaces": self._unresolved_interfaces,
544
594
  "_NOT_SET": NOT_SET,
545
595
  "_contextmanager": contextlib.contextmanager,
546
596
  "_is_cm": is_context_manager,
547
- "_cache": self._async_cache if is_async else self._cache,
597
+ "_cache": (
598
+ (self._async_override_cache if is_async else self._override_cache)
599
+ if with_override
600
+ else (self._async_cache if is_async else self._cache)
601
+ ),
548
602
  "_compile": self._compile_resolver,
603
+ "resolver": self,
549
604
  }
550
605
 
551
606
  # Add async-specific namespace entries
@@ -561,3 +616,59 @@ class Resolver:
561
616
  creator = ns["_resolver_create"]
562
617
 
563
618
  return CompiledResolver(resolver, creator)
619
+
620
+ def _get_override_for(self, interface: Any) -> Any:
621
+ """Hook for checking if an interface has an override."""
622
+ return self._override_instances.get(interface, NOT_SET)
623
+
624
+ def _wrap_for_override(self, annotation: Any, value: Any) -> Any:
625
+ """Hook for wrapping dependencies to enable override patching."""
626
+ return InstanceProxy(value, interface=annotation)
627
+
628
+ def _post_resolve_override(self, interface: Any, instance: Any) -> Any: # noqa: C901
629
+ """Hook for patching resolved instances to support override."""
630
+ if interface in self._override_instances:
631
+ return self._override_instances[interface]
632
+
633
+ if not hasattr(instance, "__dict__") or hasattr(
634
+ instance, "__resolver_getter__"
635
+ ):
636
+ return instance
637
+
638
+ wrapped = {
639
+ name: value.interface
640
+ for name, value in instance.__dict__.items()
641
+ if isinstance(value, InstanceProxy)
642
+ }
643
+
644
+ def __resolver_getter__(name: str) -> Any:
645
+ if name in wrapped:
646
+ _interface = wrapped[name]
647
+ # Resolve the dependency if it's wrapped
648
+ return self._container.resolve(_interface)
649
+ raise LookupError
650
+
651
+ # Attach the resolver getter to the instance
652
+ instance.__resolver_getter__ = __resolver_getter__
653
+
654
+ if not hasattr(instance.__class__, "__getattribute_patched__"):
655
+
656
+ def __getattribute__(_self: Any, name: str) -> Any:
657
+ # Skip the resolver getter
658
+ if name in {"__resolver_getter__", "__class__"}:
659
+ return object.__getattribute__(_self, name)
660
+
661
+ if hasattr(_self, "__resolver_getter__"):
662
+ try:
663
+ return _self.__resolver_getter__(name)
664
+ except LookupError:
665
+ pass
666
+
667
+ # Fall back to default behavior
668
+ return object.__getattribute__(_self, name)
669
+
670
+ # Apply the patched resolver if wrapped attributes exist
671
+ instance.__class__.__getattribute__ = __getattribute__
672
+ instance.__class__.__getattribute_patched__ = True
673
+
674
+ return instance
anydi/testing.py CHANGED
@@ -1,125 +1,46 @@
1
- import contextlib
2
- import logging
3
- from collections.abc import Iterable, Iterator, Sequence
4
- from typing import Any
1
+ """Testing utilities for AnyDI.
5
2
 
6
- import wrapt # type: ignore
7
- from typing_extensions import Self, type_repr
3
+ .. deprecated:: 0.56.0
4
+ TestContainer is deprecated. Use Container with override() instead.
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ import warnings
8
10
 
9
11
  from ._container import Container
10
- from ._module import ModuleDef
11
- from ._provider import ProviderDef
12
- from ._types import NOT_SET
13
12
 
14
13
 
15
14
  class TestContainer(Container):
15
+ """Test container for dependency injection.
16
+
17
+ .. deprecated:: 0.56.0
18
+ TestContainer is deprecated and will be removed in a future version.
19
+ Use regular Container with override() method instead.
20
+ """
21
+
16
22
  __test__ = False
17
23
 
18
- def __init__(
19
- self,
20
- *,
21
- providers: Sequence[ProviderDef] | None = None,
22
- modules: Iterable[ModuleDef] | None = None,
23
- logger: logging.Logger | None = None,
24
- ) -> None:
25
- super().__init__(providers=providers, modules=modules, logger=logger)
26
- self._override_instances: dict[Any, Any] = {}
27
- self._revisions: dict[Any, int] = {}
24
+ def __init__(self, *args, **kwargs): # type: ignore
25
+ warnings.warn(
26
+ "TestContainer is deprecated and will be removed in a future version. "
27
+ "Use regular Container with override() method instead.",
28
+ DeprecationWarning,
29
+ stacklevel=2,
30
+ )
31
+ super().__init__(*args, **kwargs)
28
32
 
29
33
  @classmethod
30
- def from_container(cls, container: Container) -> Self:
31
- return cls(
32
- providers=[
33
- ProviderDef(
34
- interface=provider.interface,
35
- call=provider.call,
36
- scope=provider.scope,
37
- )
38
- for provider in container.providers.values()
39
- ],
40
- logger=container.logger,
41
- )
34
+ def from_container(cls, container: Container) -> Container:
35
+ """Create a test container from an existing container.
42
36
 
43
- @contextlib.contextmanager
44
- def override(self, interface: Any, instance: Any) -> Iterator[None]:
45
- """
46
- Override the provider for the specified interface with a specific instance.
37
+ .. deprecated:: 0.56.0
38
+ This method is deprecated. Just use the container directly.
47
39
  """
48
- if not self.has_provider_for(interface):
49
- raise LookupError(
50
- f"The provider interface `{type_repr(interface)}` not registered."
51
- )
52
- self._override_instances[interface] = instance
53
- self._touch_revision(interface)
54
- try:
55
- yield
56
- finally:
57
- self._override_instances.pop(interface, None)
58
- self._touch_revision(interface)
59
-
60
- def _touch_revision(self, interface: Any) -> None:
61
- self._revisions[interface] = self._revisions.get(interface, 0) + 1
62
-
63
- def _hook_override_for(self, interface: Any) -> Any:
64
- return self._override_instances.get(interface, NOT_SET)
65
-
66
- def _hook_wrap_dependency(self, annotation: Any, value: Any) -> Any:
67
- return InstanceProxy(value, interface=annotation)
68
-
69
- def _hook_post_resolve(self, interface: Any, instance: Any) -> Any: # noqa: C901
70
- """Patch the test resolver for the instance."""
71
- if interface in self._override_instances:
72
- return self._override_instances[interface]
73
-
74
- if not hasattr(instance, "__dict__") or hasattr(
75
- instance, "__resolver_getter__"
76
- ):
77
- return instance
78
-
79
- wrapped = {
80
- name: value.interface
81
- for name, value in instance.__dict__.items()
82
- if isinstance(value, InstanceProxy)
83
- }
84
-
85
- def __resolver_getter__(name: str) -> Any:
86
- if name in wrapped:
87
- _interface = wrapped[name]
88
- # Resolve the dependency if it's wrapped
89
- return self.resolve(_interface)
90
- raise LookupError
91
-
92
- # Attach the resolver getter to the instance
93
- instance.__resolver_getter__ = __resolver_getter__
94
-
95
- if not hasattr(instance.__class__, "__getattribute_patched__"):
96
-
97
- def __getattribute__(_self: Any, name: str) -> Any:
98
- # Skip the resolver getter
99
- if name in {"__resolver_getter__", "__class__"}:
100
- return object.__getattribute__(_self, name)
101
-
102
- if hasattr(_self, "__resolver_getter__"):
103
- try:
104
- return _self.__resolver_getter__(name)
105
- except LookupError:
106
- pass
107
-
108
- # Fall back to default behavior
109
- return object.__getattribute__(_self, name)
110
-
111
- # Apply the patched resolver if wrapped attributes exist
112
- instance.__class__.__getattribute__ = __getattribute__
113
- instance.__class__.__getattribute_patched__ = True
114
-
115
- return instance
116
-
117
-
118
- class InstanceProxy(wrapt.ObjectProxy): # type: ignore
119
- def __init__(self, wrapped: Any, *, interface: type[Any]) -> None:
120
- super().__init__(wrapped) # type: ignore
121
- self._self_interface = interface
122
-
123
- @property
124
- def interface(self) -> type[Any]:
125
- return self._self_interface
40
+ warnings.warn(
41
+ "TestContainer.from_container() is deprecated. "
42
+ "Use the container directly with override() method.",
43
+ DeprecationWarning,
44
+ stacklevel=2,
45
+ )
46
+ return container
@@ -0,0 +1,267 @@
1
+ Metadata-Version: 2.4
2
+ Name: anydi
3
+ Version: 0.56.0
4
+ Summary: Dependency Injection library
5
+ Keywords: dependency injection,dependencies,di,async,asyncio,application
6
+ Author: Anton Ruhlov
7
+ Author-email: Anton Ruhlov <antonruhlov@gmail.com>
8
+ License-Expression: MIT
9
+ Classifier: Intended Audience :: Information Technology
10
+ Classifier: Intended Audience :: System Administrators
11
+ Classifier: Operating System :: OS Independent
12
+ Classifier: Development Status :: 5 - Production/Stable
13
+ Classifier: Topic :: Internet
14
+ Classifier: Topic :: Software Development :: Libraries :: Application Frameworks
15
+ Classifier: Topic :: Software Development :: Libraries :: Python Modules
16
+ Classifier: Topic :: Software Development :: Libraries
17
+ Classifier: Topic :: Software Development
18
+ Classifier: Typing :: Typed
19
+ Classifier: Environment :: Web Environment
20
+ Classifier: Intended Audience :: Developers
21
+ Classifier: License :: OSI Approved :: MIT License
22
+ Classifier: Programming Language :: Python :: 3
23
+ Classifier: Programming Language :: Python :: 3.10
24
+ Classifier: Programming Language :: Python :: 3.11
25
+ Classifier: Programming Language :: Python :: 3.12
26
+ Classifier: Programming Language :: Python :: 3.13
27
+ Classifier: Programming Language :: Python :: 3.14
28
+ Classifier: Programming Language :: Python :: 3 :: Only
29
+ Requires-Dist: typing-extensions>=4.15.0,<5
30
+ Requires-Dist: anyio>=3.7.1
31
+ Requires-Dist: wrapt>=1.17.0
32
+ Requires-Python: >=3.10.0, <3.15
33
+ Project-URL: Repository, https://github.com/antonrh/anydi
34
+ Description-Content-Type: text/markdown
35
+
36
+ # AnyDI
37
+
38
+ <div style="text-align: center;">
39
+
40
+ Modern, lightweight Dependency Injection library using type annotations.
41
+
42
+ [![CI](https://github.com/antonrh/anydi/actions/workflows/ci.yml/badge.svg)](https://github.com/antonrh/anydi/actions/workflows/ci.yml)
43
+ [![Coverage](https://codecov.io/gh/antonrh/anydi/branch/main/graph/badge.svg)](https://codecov.io/gh/antonrh/anydi)
44
+ [![Documentation](https://readthedocs.org/projects/anydi/badge/?version=latest)](https://anydi.readthedocs.io/en/latest/)
45
+
46
+ </div>
47
+
48
+ ---
49
+ Documentation
50
+
51
+ http://anydi.readthedocs.io/
52
+
53
+ ---
54
+
55
+ `AnyDI` is a modern, lightweight Dependency Injection library suitable for any synchronous or asynchronous applications with Python 3.10+, based on type annotations ([PEP 484](https://peps.python.org/pep-0484/)).
56
+
57
+ The key features are:
58
+
59
+ * **Type-safe**: Dependency resolution is driven by type hints.
60
+ * **Async-ready**: Works the same for sync and async providers or injections.
61
+ * **Scoped**: Built-in singleton, transient, and request lifetimes.
62
+ * **Simple**: Small surface area keeps boilerplate low.
63
+ * **Fast**: Resolver still adds only microseconds of overhead.
64
+ * **Named**: `Annotated[...]` makes multiple bindings per type simple.
65
+ * **Managed**: Providers can open/close resources via context managers.
66
+ * **Modular**: Compose containers or modules for large apps.
67
+ * **Scanning**: Auto-discovers injectable callables.
68
+ * **Integrated**: Extensions for popular frameworks.
69
+ * **Testable**: Override providers directly in tests.
70
+
71
+ ## Installation
72
+
73
+ ```shell
74
+ pip install anydi
75
+ ```
76
+
77
+ ## Comprehensive Example
78
+
79
+ ### Define a Service (`app/services.py`)
80
+
81
+ ```python
82
+ class GreetingService:
83
+ def greet(self, name: str) -> str:
84
+ return f"Hello, {name}!"
85
+ ```
86
+
87
+ ### Create the Container and Providers (`app/container.py`)
88
+
89
+ ```python
90
+ from anydi import Container
91
+
92
+ from app.services import GreetingService
93
+
94
+
95
+ container = Container()
96
+
97
+
98
+ @container.provider(scope="singleton")
99
+ def service() -> GreetingService:
100
+ return GreetingService()
101
+ ```
102
+
103
+ ### Resolve Dependencies Directly
104
+
105
+ ```python
106
+ from app.container import container
107
+ from app.services import GreetingService
108
+
109
+
110
+ service = container.resolve(GreetingService)
111
+
112
+ if __name__ == "__main__":
113
+ print(service.greet("World"))
114
+ ```
115
+
116
+ ### Inject Into Functions (`app/main.py`)
117
+
118
+ ```python
119
+ from anydi import Inject
120
+
121
+ from app.container import container
122
+ from app.services import GreetingService
123
+
124
+
125
+ @container.inject
126
+ def greet(service: GreetingService = Inject()) -> str:
127
+ return service.greet("World")
128
+
129
+
130
+ if __name__ == "__main__":
131
+ print(greet())
132
+ ```
133
+
134
+ ### Test with Overrides (`tests/test_app.py`)
135
+
136
+ ```python
137
+ from unittest import mock
138
+
139
+ from app.container import container
140
+ from app.services import GreetingService
141
+ from app.main import greet
142
+
143
+
144
+ def test_greet() -> None:
145
+ service_mock = mock.Mock(spec=GreetingService)
146
+ service_mock.greet.return_value = "Mocked"
147
+
148
+ with container.override(GreetingService, service_mock):
149
+ result = greet()
150
+
151
+ assert result == "Mocked"
152
+ ```
153
+
154
+ ### Integrate with FastAPI (`app/api.py`)
155
+
156
+ ```python
157
+ from typing import Annotated
158
+
159
+ import anydi.ext.fastapi
160
+ from fastapi import FastAPI
161
+ from anydi.ext.fastapi import Inject
162
+
163
+ from app.container import container
164
+ from app.services import GreetingService
165
+
166
+
167
+ app = FastAPI()
168
+
169
+
170
+ @app.get("/greeting")
171
+ async def greet(
172
+ service: Annotated[GreetingService, Inject()]
173
+ ) -> dict[str, str]:
174
+ return {"greeting": service.greet("World")}
175
+
176
+
177
+ anydi.ext.fastapi.install(app, container)
178
+ ```
179
+
180
+ ### Test the FastAPI Integration (`test_api.py`)
181
+
182
+ ```python
183
+ from unittest import mock
184
+
185
+ from fastapi.testclient import TestClient
186
+
187
+ from app.api import app
188
+ from app.container import container
189
+ from app.services import GreetingService
190
+
191
+
192
+ client = TestClient(app)
193
+
194
+
195
+ def test_api_greeting() -> None:
196
+ service_mock = mock.Mock(spec=GreetingService)
197
+ service_mock.greet.return_value = "Mocked"
198
+
199
+ with container.override(GreetingService, service_mock):
200
+ response = client.get("/greeting")
201
+
202
+ assert response.json() == {"greeting": "Mocked"}
203
+ ```
204
+
205
+ ### Integrate with Django Ninja
206
+
207
+ Install the Django integration extras:
208
+
209
+ ```sh
210
+ pip install 'anydi-django[ninja]'
211
+ ```
212
+
213
+ Expose the container factory (`app/container.py`):
214
+
215
+ ```python
216
+ from anydi import Container
217
+
218
+ from app.services import GreetingService
219
+
220
+
221
+ container = Container()
222
+
223
+
224
+ @container.provider(scope="singleton")
225
+ def service() -> GreetingService:
226
+ return GreetingService()
227
+ ```
228
+
229
+ Configure Django (`settings.py`):
230
+
231
+ ```python
232
+ INSTALLED_APPS = [
233
+ ...
234
+ "anydi_django",
235
+ ]
236
+
237
+ ANYDI = {
238
+ "CONTAINER_FACTORY": "app.container.container",
239
+ "PATCH_NINJA": True,
240
+ }
241
+ ```
242
+
243
+ Wire Django Ninja (`urls.py`):
244
+
245
+ ```python
246
+ from typing import Annotated, Any
247
+
248
+ from anydi import Inject
249
+ from django.http import HttpRequest
250
+ from django.urls import path
251
+ from ninja import NinjaAPI
252
+
253
+ from app.services import GreetingService
254
+
255
+
256
+ api = NinjaAPI()
257
+
258
+
259
+ @api.get("/greeting")
260
+ def greet(request: HttpRequest, service: Annotated[GreetingService, Inject()]) -> Any:
261
+ return {"greeting": service.greet("World")}
262
+
263
+
264
+ urlpatterns = [
265
+ path("api/", api.urls),
266
+ ]
267
+ ```
@@ -1,11 +1,11 @@
1
1
  anydi/__init__.py,sha256=Cz-beqReX0d05SFDmYcrzIs3FqQkWAwpy1Aqzd5db34,547
2
2
  anydi/_async_lock.py,sha256=3dwZr0KthXFYha0XKMyXf8jMmGb1lYoNC0O5w29V9ic,1104
3
- anydi/_container.py,sha256=NtxTUOSXPqTzdiFXJVAT9RkGhrHoKCbgVrjbO2pHhQg,24787
3
+ anydi/_container.py,sha256=dKRT4FB0ONyEt2-MRz4T0MZziwZAMb0XyulV6lVn04g,24997
4
4
  anydi/_context.py,sha256=-9QqeMWo9OpZVXZxZCQgIsswggl3Ch7lgx1KiFX_ezc,3752
5
5
  anydi/_decorators.py,sha256=J3W261ZAG7q4XKm4tbAv1wsWr9ysx9_5MUbUvSJB_MQ,2809
6
6
  anydi/_module.py,sha256=2kN5uEXLd2Dsc58gz5IWK43wJewr_QgIVGSO3iWp798,2609
7
- anydi/_provider.py,sha256=DjV4fqKGy1s4BSndUu8i97CvD_7Fp4bUwHGkkfIDpfA,1543
8
- anydi/_resolver.py,sha256=AVZGDGAzb-E9FrKtNlEMn7xiAB61sMFN8_aYmVCOIPY,24705
7
+ anydi/_provider.py,sha256=OV1WFHTYv7W2U0XDk_Kql1r551Vhq8o-pUV5ep1HQcU,1574
8
+ anydi/_resolver.py,sha256=-MF2KsERF5qzU6uqYPF1fI58isgsjxXPLERylzFFDHE,28787
9
9
  anydi/_scanner.py,sha256=oycIC9kw9fsIG9qgtRHeBkj3HjmcLK0FTqWLXTLLSWE,3636
10
10
  anydi/_types.py,sha256=l3xQ0Zn15gRAwvBoQ9PRfCBigi2rrtSqGV-C50xXrLw,1780
11
11
  anydi/ext/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
@@ -17,8 +17,8 @@ anydi/ext/pytest_plugin.py,sha256=Es1K1S6_2gIdTUYkbw2d1aZcHnjJutGFafVsLPGcVJc,46
17
17
  anydi/ext/starlette/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
18
18
  anydi/ext/starlette/middleware.py,sha256=MxnzshAs-CMvjJp0r457k52MzBL8O4KAuClnF6exBdU,803
19
19
  anydi/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
20
- anydi/testing.py,sha256=JNXA-XGjkU7PMturkzDzumDcLlnYOLzCtBUQ9HyQMLM,4309
21
- anydi-0.55.0.dist-info/WHEEL,sha256=eh7sammvW2TypMMMGKgsM83HyA_3qQ5Lgg3ynoecH3M,79
22
- anydi-0.55.0.dist-info/entry_points.txt,sha256=AgOcQYM5KyS4D37QcYb00tiid0QA-pD1VrjHHq4QAps,44
23
- anydi-0.55.0.dist-info/METADATA,sha256=2gt1okvagGZDs1PiCLaze0wnSmcrqLP3ujf6rERdMSE,4799
24
- anydi-0.55.0.dist-info/RECORD,,
20
+ anydi/testing.py,sha256=cHg3mMScZbEep9smRqSNQ81BZMQOkyugHe8TvKdPnEg,1347
21
+ anydi-0.56.0.dist-info/WHEEL,sha256=eh7sammvW2TypMMMGKgsM83HyA_3qQ5Lgg3ynoecH3M,79
22
+ anydi-0.56.0.dist-info/entry_points.txt,sha256=AgOcQYM5KyS4D37QcYb00tiid0QA-pD1VrjHHq4QAps,44
23
+ anydi-0.56.0.dist-info/METADATA,sha256=JkjqRSFZ-Vu22IYLnWS2uRTZA7uUbX41o2b-51yp0dA,6561
24
+ anydi-0.56.0.dist-info/RECORD,,
@@ -1,193 +0,0 @@
1
- Metadata-Version: 2.4
2
- Name: anydi
3
- Version: 0.55.0
4
- Summary: Dependency Injection library
5
- Keywords: dependency injection,dependencies,di,async,asyncio,application
6
- Author: Anton Ruhlov
7
- Author-email: Anton Ruhlov <antonruhlov@gmail.com>
8
- License-Expression: MIT
9
- Classifier: Intended Audience :: Information Technology
10
- Classifier: Intended Audience :: System Administrators
11
- Classifier: Operating System :: OS Independent
12
- Classifier: Development Status :: 5 - Production/Stable
13
- Classifier: Topic :: Internet
14
- Classifier: Topic :: Software Development :: Libraries :: Application Frameworks
15
- Classifier: Topic :: Software Development :: Libraries :: Python Modules
16
- Classifier: Topic :: Software Development :: Libraries
17
- Classifier: Topic :: Software Development
18
- Classifier: Typing :: Typed
19
- Classifier: Environment :: Web Environment
20
- Classifier: Intended Audience :: Developers
21
- Classifier: License :: OSI Approved :: MIT License
22
- Classifier: Programming Language :: Python :: 3
23
- Classifier: Programming Language :: Python :: 3.10
24
- Classifier: Programming Language :: Python :: 3.11
25
- Classifier: Programming Language :: Python :: 3.12
26
- Classifier: Programming Language :: Python :: 3.13
27
- Classifier: Programming Language :: Python :: 3.14
28
- Classifier: Programming Language :: Python :: 3 :: Only
29
- Requires-Dist: typing-extensions>=4.15.0,<5
30
- Requires-Dist: anyio>=3.7.1
31
- Requires-Dist: wrapt>=1.17.0,<2
32
- Requires-Python: >=3.10.0, <3.15
33
- Project-URL: Repository, https://github.com/antonrh/anydi
34
- Description-Content-Type: text/markdown
35
-
36
- # AnyDI
37
-
38
- <div style="text-align: center;">
39
-
40
- Modern, lightweight Dependency Injection library using type annotations.
41
-
42
- [![CI](https://github.com/antonrh/anydi/actions/workflows/ci.yml/badge.svg)](https://github.com/antonrh/anydi/actions/workflows/ci.yml)
43
- [![Coverage](https://codecov.io/gh/antonrh/anydi/branch/main/graph/badge.svg)](https://codecov.io/gh/antonrh/anydi)
44
- [![Documentation](https://readthedocs.org/projects/anydi/badge/?version=latest)](https://anydi.readthedocs.io/en/latest/)
45
-
46
- </div>
47
-
48
- ---
49
- Documentation
50
-
51
- http://anydi.readthedocs.io/
52
-
53
- ---
54
-
55
- `AnyDI` is a modern, lightweight Dependency Injection library suitable for any synchronous or asynchronous applications with Python 3.10+, based on type annotations ([PEP 484](https://peps.python.org/pep-0484/)).
56
-
57
- The key features are:
58
-
59
- * **Type-safe**: Resolves dependencies using type annotations.
60
- * **Async Support**: Compatible with both synchronous and asynchronous providers and injections.
61
- * **Scoping**: Supports singleton, transient, and request scopes.
62
- * **Easy to Use**: Designed for simplicity and minimal boilerplate.
63
- * **Named Dependencies**: Supports named dependencies using `Annotated` type.
64
- * **Resource Management**: Manages resources using context managers.
65
- * **Modular: Facilitates** a modular design with support for multiple modules.
66
- * **Scanning**: Automatically scans for injectable functions and classes.
67
- * **Integrations**: Provides easy integration with popular frameworks and libraries.
68
- * **Testing**: Simplifies testing by allowing provider overrides.
69
-
70
- ## Installation
71
-
72
- ```shell
73
- pip install anydi
74
- ```
75
-
76
- ## Quick Example
77
-
78
- *app.py*
79
-
80
- ```python
81
- from anydi import auto, Container
82
-
83
- container = Container()
84
-
85
-
86
- @container.provider(scope="singleton")
87
- def message() -> str:
88
- return "Hello, World!"
89
-
90
-
91
- @container.inject
92
- def say_hello(message: str = auto) -> None:
93
- print(message)
94
-
95
-
96
- if __name__ == "__main__":
97
- say_hello()
98
- ```
99
-
100
- ## FastAPI Example
101
-
102
- *app.py*
103
-
104
- ```python
105
- from typing import Annotated
106
-
107
- from fastapi import FastAPI
108
-
109
- import anydi.ext.fastapi
110
- from anydi import Container
111
- from anydi.ext.fastapi import Inject
112
-
113
- container = Container()
114
-
115
-
116
- @container.provider(scope="singleton")
117
- def message() -> str:
118
- return "Hello, World!"
119
-
120
-
121
- app = FastAPI()
122
-
123
-
124
- @app.get("/hello")
125
- def say_hello(message: Annotated[str, Inject()]) -> dict[str, str]:
126
- return {"message": message}
127
-
128
-
129
- # Install the container into the FastAPI app
130
- anydi.ext.fastapi.install(app, container)
131
- ```
132
-
133
-
134
-
135
- ## Django Ninja Example
136
-
137
- ### Install
138
-
139
- ```sh
140
- pip install 'anydi-django[ninja]'
141
- ```
142
-
143
- *container.py*
144
-
145
- ```python
146
- from anydi import Container
147
-
148
-
149
- def get_container() -> Container:
150
- container = Container()
151
-
152
- @container.provider(scope="singleton")
153
- def message() -> str:
154
- return "Hello, World!"
155
-
156
- return container
157
- ```
158
-
159
- *settings.py*
160
-
161
- ```python
162
- INSTALLED_APPS = [
163
- ...
164
- "anydi_django",
165
- ]
166
-
167
- ANYDI = {
168
- "CONTAINER_FACTORY": "myapp.container.get_container",
169
- "PATCH_NINJA": True,
170
- }
171
- ```
172
-
173
- *urls.py*
174
-
175
- ```python
176
- from django.http import HttpRequest
177
- from django.urls import path
178
- from ninja import NinjaAPI
179
-
180
- from anydi import auto
181
-
182
- api = NinjaAPI()
183
-
184
-
185
- @api.get("/hello")
186
- def say_hello(request: HttpRequest, message: str = auto) -> dict[str, str]:
187
- return {"message": message}
188
-
189
-
190
- urlpatterns = [
191
- path("api/", api.urls),
192
- ]
193
- ```
File without changes