anydi 0.67.2__py3-none-any.whl → 0.69.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
@@ -8,15 +8,17 @@ import inspect
8
8
  import logging
9
9
  import types
10
10
  import uuid
11
+ import warnings
11
12
  from collections import defaultdict
12
13
  from collections.abc import AsyncIterator, Callable, Iterable, Iterator, Sequence
13
14
  from contextvars import ContextVar
14
- from typing import Any, TypeVar, get_args, get_origin, overload
15
+ from typing import Any, Literal, TypeVar, get_args, get_origin, overload
15
16
 
16
17
  from typing_extensions import ParamSpec, Self, type_repr
17
18
 
18
19
  from ._context import InstanceContext
19
20
  from ._decorators import is_provided
21
+ from ._graph import Graph
20
22
  from ._injector import Injector
21
23
  from ._marker import Marker
22
24
  from ._module import ModuleDef, ModuleRegistrar
@@ -55,24 +57,30 @@ class Container:
55
57
  self._injector = Injector(self)
56
58
  self._modules = ModuleRegistrar(self)
57
59
  self._scanner = Scanner(self)
60
+ self._graph = Graph(self)
61
+
62
+ # Build state
63
+ self._ready = False
64
+
65
+ # Test mode (enables override support for all resolutions)
66
+ self._test_mode = False
58
67
 
59
68
  # Register default scopes
60
69
  self.register_scope("request")
61
70
 
62
71
  # Register self as provider
63
- self._register_provider(
64
- lambda: self,
65
- "singleton",
66
- Container,
67
- )
72
+ self.register(Container, lambda: self, scope="singleton")
68
73
 
69
74
  # Register providers
70
75
  providers = providers or []
71
76
  for provider in providers:
72
77
  self._register_provider(
73
- provider.call,
78
+ provider.dependency_type,
79
+ provider.factory,
74
80
  provider.scope,
75
- provider.interface,
81
+ provider.from_context,
82
+ False,
83
+ None,
76
84
  )
77
85
 
78
86
  # Register modules
@@ -87,6 +95,11 @@ class Container:
87
95
  """Get the registered providers."""
88
96
  return self._providers
89
97
 
98
+ @property
99
+ def ready(self) -> bool:
100
+ """Check if the container is ready."""
101
+ return self._ready
102
+
90
103
  @property
91
104
  def logger(self) -> logging.Logger:
92
105
  """Get the logger instance."""
@@ -111,8 +124,8 @@ class Container:
111
124
  def start(self) -> None:
112
125
  """Start the singleton context."""
113
126
  # Resolve all singleton resources
114
- for interface in self._resources.get("singleton", []):
115
- self.resolve(interface)
127
+ for dependency_type in self._resources.get("singleton", []):
128
+ self.resolve(dependency_type)
116
129
 
117
130
  def close(self) -> None:
118
131
  """Close the singleton context."""
@@ -134,8 +147,8 @@ class Container:
134
147
 
135
148
  async def astart(self) -> None:
136
149
  """Start the singleton context asynchronously."""
137
- for interface in self._resources.get("singleton", []):
138
- await self.aresolve(interface)
150
+ for dependency_type in self._resources.get("singleton", []):
151
+ await self.aresolve(dependency_type)
139
152
 
140
153
  async def aclose(self) -> None:
141
154
  """Close the singleton context asynchronously."""
@@ -158,10 +171,10 @@ class Container:
158
171
  token = context_var.set(context)
159
172
 
160
173
  # Resolve all request resources
161
- for interface in self._resources.get(scope, []):
162
- if not is_event_type(interface):
174
+ for dependency_type in self._resources.get(scope, []):
175
+ if not is_event_type(dependency_type):
163
176
  continue
164
- self.resolve(interface)
177
+ self.resolve(dependency_type)
165
178
 
166
179
  with context:
167
180
  yield context
@@ -184,10 +197,10 @@ class Container:
184
197
  token = context_var.set(context)
185
198
 
186
199
  # Resolve all request resources
187
- for interface in self._resources.get(scope, []):
188
- if not is_event_type(interface):
200
+ for dependency_type in self._resources.get(scope, []):
201
+ if not is_event_type(dependency_type):
189
202
  continue
190
- await self.aresolve(interface)
203
+ await self.aresolve(dependency_type)
191
204
 
192
205
  async with context:
193
206
  yield context
@@ -320,33 +333,59 @@ class Container:
320
333
 
321
334
  def register(
322
335
  self,
323
- interface: Any,
324
- call: Callable[..., Any] = NOT_SET,
336
+ dependency_type: Any = NOT_SET,
337
+ factory: Callable[..., Any] = NOT_SET,
325
338
  *,
326
339
  scope: Scope = "singleton",
340
+ from_context: bool = False,
327
341
  override: bool = False,
342
+ interface: Any = NOT_SET,
343
+ call: Callable[..., Any] = NOT_SET,
328
344
  ) -> Provider:
329
- """Register a provider for the specified interface."""
330
- if call is NOT_SET:
331
- call = interface
332
- return self._register_provider(call, scope, interface, override)
333
-
334
- def is_registered(self, interface: Any) -> bool:
335
- """Check if a provider is registered for the specified interface."""
336
- return interface in self._providers
337
-
338
- def has_provider_for(self, interface: Any) -> bool:
339
- """Check if a provider exists for the specified interface."""
340
- return self.is_registered(interface) or is_provided(interface)
341
-
342
- def unregister(self, interface: Any) -> None:
343
- """Unregister a provider by interface."""
344
- if not self.is_registered(interface):
345
+ """Register a provider for the specified dependency type."""
346
+ if self.ready and not override:
347
+ raise RuntimeError(
348
+ "Cannot register providers after build() has been called. "
349
+ "All providers must be registered before building the container."
350
+ )
351
+
352
+ if interface is not NOT_SET:
353
+ warnings.warn(
354
+ "The `interface` is deprecated. Use `dependency_type` instead.",
355
+ DeprecationWarning,
356
+ stacklevel=2,
357
+ )
358
+ if call is not NOT_SET:
359
+ warnings.warn(
360
+ "The `call` is deprecated. Use `factory` instead.",
361
+ DeprecationWarning,
362
+ stacklevel=2,
363
+ )
364
+
365
+ if dependency_type is NOT_SET:
366
+ dependency_type = interface
367
+ if factory is NOT_SET:
368
+ factory = call if call is not NOT_SET else dependency_type
369
+ return self._register_provider(
370
+ dependency_type, factory, scope, from_context, override, None
371
+ )
372
+
373
+ def is_registered(self, dependency_type: Any, /) -> bool:
374
+ """Check if a provider is registered for the specified dependency type."""
375
+ return dependency_type in self._providers
376
+
377
+ def has_provider_for(self, dependency_type: Any, /) -> bool:
378
+ """Check if a provider exists for the specified dependency type."""
379
+ return self.is_registered(dependency_type) or is_provided(dependency_type)
380
+
381
+ def unregister(self, dependency_type: Any, /) -> None:
382
+ """Unregister a provider by dependency type."""
383
+ if not self.is_registered(dependency_type):
345
384
  raise LookupError(
346
- f"The provider interface `{type_repr(interface)}` not registered."
385
+ f"The provider `{type_repr(dependency_type)}` is not registered."
347
386
  )
348
387
 
349
- provider = self._get_provider(interface)
388
+ provider = self._get_provider(dependency_type)
350
389
 
351
390
  # Cleanup instance context
352
391
  if provider.scope != "transient":
@@ -355,7 +394,7 @@ class Container:
355
394
  except LookupError:
356
395
  pass
357
396
  else:
358
- del context[interface]
397
+ del context[dependency_type]
359
398
 
360
399
  # Cleanup provider references
361
400
  self._delete_provider(provider)
@@ -366,336 +405,461 @@ class Container:
366
405
  """Decorator to register a provider function with the specified scope."""
367
406
 
368
407
  def decorator(call: Callable[P, T]) -> Callable[P, T]:
369
- self._register_provider(call, scope, NOT_SET, override)
408
+ self._register_provider(NOT_SET, call, scope, False, override, None)
370
409
  return call
371
410
 
372
411
  return decorator
373
412
 
374
413
  def _register_provider( # noqa: C901
375
414
  self,
376
- call: Callable[..., Any],
415
+ dependency_type: Any,
416
+ factory: Callable[..., Any],
377
417
  scope: Scope,
378
- interface: Any = NOT_SET,
379
- override: bool = False,
380
- defaults: dict[str, Any] | None = None,
418
+ from_context: bool,
419
+ override: bool,
420
+ defaults: dict[str, Any] | None,
381
421
  ) -> Provider:
382
422
  """Register a provider with the specified scope."""
383
- name = type_repr(call)
384
- kind = ProviderKind.from_call(call)
385
- is_class = kind == ProviderKind.CLASS
386
- is_coroutine = kind == ProviderKind.COROUTINE
387
- is_generator = kind == ProviderKind.GENERATOR
388
- is_async_generator = kind == ProviderKind.ASYNC_GENERATOR
389
- is_resource = is_generator or is_async_generator
390
-
391
- # Validate scope
392
- self._validate_provider_scope(scope, name, is_resource)
393
-
394
- # Get signature and detect interface
395
- signature = inspect.signature(call, eval_str=True)
396
-
397
- if interface is NOT_SET:
398
- interface = call if is_class else signature.return_annotation
399
- if interface is inspect.Signature.empty:
400
- interface = None
401
-
402
- # Handle iterator types for resources
403
- interface_origin = get_origin(interface)
404
- if is_iterator_type(interface) or is_iterator_type(interface_origin):
405
- if args := get_args(interface):
406
- interface = args[0]
407
- if is_none_type(interface):
408
- interface = type(f"Event_{uuid.uuid4().hex}", (Event,), {})
409
- else:
423
+ # Validate scope is registered
424
+ if scope not in self._scopes:
425
+ raise ValueError(
426
+ f"The scope `{scope}` is not registered. "
427
+ "Please register the scope first using register_scope()."
428
+ )
429
+
430
+ # Default factory to dependency_type if not set
431
+ if not from_context and factory is NOT_SET:
432
+ factory = dependency_type
433
+
434
+ # Handle from_context providers (context-provided dependencies)
435
+ if from_context:
436
+ if scope in ("singleton", "transient"):
437
+ raise ValueError(
438
+ f"The `from_context=True` option cannot be used with "
439
+ f"`{scope}` scope. Use a scoped context like 'request' instead."
440
+ )
441
+ if dependency_type is NOT_SET:
410
442
  raise TypeError(
411
- f"Cannot use `{name}` resource type annotation "
412
- "without actual type argument."
443
+ "The `dependency_type` parameter is required when using "
444
+ "`from_context=True`."
445
+ )
446
+ if dependency_type in self._providers and not override:
447
+ raise LookupError(
448
+ f"The provider `{type_repr(dependency_type)}` is already "
449
+ "registered."
413
450
  )
414
451
 
415
- # Validate interface
416
- if is_none_type(interface):
417
- raise TypeError(f"Missing `{name}` provider return annotation.")
418
-
419
- if interface in self._providers and not override:
420
- raise LookupError(
421
- f"The provider interface `{type_repr(interface)}` already registered."
452
+ provider = Provider(
453
+ dependency_type=dependency_type,
454
+ factory=lambda: None,
455
+ scope=scope,
456
+ from_context=True,
457
+ parameters=(),
458
+ is_class=False,
459
+ is_coroutine=False,
460
+ is_generator=False,
461
+ is_async_generator=False,
462
+ is_async=False,
463
+ is_resource=False,
422
464
  )
465
+ else:
466
+ # Regular provider registration
467
+ name = type_repr(factory)
468
+ kind = ProviderKind.from_call(factory)
469
+ is_class = kind == ProviderKind.CLASS
470
+ is_coroutine = kind == ProviderKind.COROUTINE
471
+ is_generator = kind == ProviderKind.GENERATOR
472
+ is_async_generator = kind == ProviderKind.ASYNC_GENERATOR
473
+ is_resource = is_generator or is_async_generator
474
+
475
+ if scope == "transient" and is_resource:
476
+ raise TypeError(
477
+ f"The resource provider `{name}` is attempting to register "
478
+ "with a transient scope, which is not allowed."
479
+ )
423
480
 
424
- # Process parameters
425
- parameters: list[ProviderParameter] = []
426
- scope_provider: dict[Scope, Provider] = {}
427
- unresolved_parameter = None
428
- unresolved_exc: LookupError | None = None
429
- is_scoped = scope not in ("singleton", "transient")
430
- scope_hierarchy = self._scopes.get(scope, ()) if scope != "transient" else ()
481
+ signature = inspect.signature(factory, eval_str=True)
482
+
483
+ # Detect dependency_type from factory or return annotation
484
+ if dependency_type is NOT_SET:
485
+ dependency_type = factory if is_class else signature.return_annotation
486
+ if dependency_type is inspect.Signature.empty:
487
+ dependency_type = None
488
+
489
+ # Unwrap iterator types for resources
490
+ type_origin = get_origin(dependency_type)
491
+ if is_iterator_type(dependency_type) or is_iterator_type(type_origin):
492
+ args = get_args(dependency_type)
493
+ if not args:
494
+ raise TypeError(
495
+ f"Cannot use `{name}` resource type annotation "
496
+ "without actual type argument."
497
+ )
498
+ dependency_type = args[0]
499
+ if is_none_type(dependency_type):
500
+ dependency_type = type(f"Event_{uuid.uuid4().hex}", (Event,), {})
431
501
 
432
- for parameter in signature.parameters.values():
433
- annotation = parameter.annotation
502
+ if is_none_type(dependency_type):
503
+ raise TypeError(f"Missing `{name}` provider return annotation.")
434
504
 
435
- if annotation is inspect.Parameter.empty:
436
- raise TypeError(
437
- f"Missing provider `{name}` "
438
- f"dependency `{parameter.name}` annotation."
439
- )
440
- if parameter.kind == inspect.Parameter.POSITIONAL_ONLY:
441
- raise TypeError(
442
- "Positional-only parameters "
443
- f"are not allowed in the provider `{name}`."
505
+ if dependency_type in self._providers and not override:
506
+ raise LookupError(
507
+ f"The provider `{type_repr(dependency_type)}` is already "
508
+ "registered."
444
509
  )
445
510
 
446
- has_default = parameter.default is not inspect.Parameter.empty
447
- default = parameter.default if has_default else NOT_SET
511
+ # Process parameters (lazy - store without resolving dependencies)
512
+ parameters: list[ProviderParameter] = []
448
513
 
449
- try:
450
- sub_provider = self._get_or_register_provider(annotation)
451
- except LookupError as exc:
452
- if (defaults and parameter.name in defaults) or has_default:
453
- continue
454
- # For scoped dependencies, allow unresolved parameters via context.set()
455
- if is_scoped:
456
- self._resolver.add_unresolved(annotation)
457
- parameters.append(
458
- ProviderParameter(
459
- name=parameter.name,
460
- annotation=annotation,
461
- default=default,
462
- has_default=has_default,
463
- provider=None,
464
- shared_scope=True,
465
- )
514
+ for param in signature.parameters.values():
515
+ if param.annotation is inspect.Parameter.empty:
516
+ raise TypeError(
517
+ f"Missing provider `{name}` "
518
+ f"dependency `{param.name}` annotation."
519
+ )
520
+ if param.kind == inspect.Parameter.POSITIONAL_ONLY:
521
+ raise TypeError(
522
+ f"Positional-only parameters "
523
+ f"are not allowed in the provider `{name}`."
466
524
  )
525
+
526
+ has_default = param.default is not inspect.Parameter.empty
527
+ # Markers are injection markers, not real defaults
528
+ if has_default and isinstance(param.default, Marker):
529
+ has_default = False
530
+ default = param.default if has_default else NOT_SET
531
+
532
+ # Skip parameters provided via defaults (for create() method)
533
+ if defaults and param.name in defaults:
467
534
  continue
468
- unresolved_parameter = parameter
469
- unresolved_exc = exc
470
- continue
471
535
 
472
- # Track scope providers for validation
473
- scope_provider.setdefault(sub_provider.scope, sub_provider)
474
-
475
- # For scoped dependencies with same scope having unresolved params,
476
- # defer to context.set() instead
477
- if (
478
- is_scoped
479
- and sub_provider.scope == scope
480
- and any(p.provider is None for p in sub_provider.parameters)
481
- ):
482
- self._resolver.add_unresolved(annotation)
536
+ # Lazy registration: Store parameter without resolving dependencies
483
537
  parameters.append(
484
538
  ProviderParameter(
485
- name=parameter.name,
486
- annotation=annotation,
539
+ dependency_type=param.annotation,
540
+ name=param.name,
487
541
  default=default,
488
542
  has_default=has_default,
489
- provider=None,
490
- shared_scope=True,
543
+ provider=None, # Lazy - will be resolved in build()
544
+ shared_scope=False, # Lazy - will be computed in build()
491
545
  )
492
546
  )
493
- continue
494
547
 
495
- # Validate scope compatibility inline
496
- if scope_hierarchy and sub_provider.scope not in scope_hierarchy:
497
- raise ValueError(
498
- f"The provider `{name}` with a `{scope}` scope "
499
- f"cannot depend on `{sub_provider}` with a "
500
- f"`{sub_provider.scope}` scope. Please ensure all "
501
- "providers are registered with matching scopes."
502
- )
503
-
504
- parameters.append(
505
- ProviderParameter(
506
- name=parameter.name,
507
- annotation=annotation,
508
- default=default,
509
- has_default=has_default,
510
- provider=sub_provider,
511
- shared_scope=sub_provider.scope == scope and scope != "transient",
512
- )
548
+ provider = Provider(
549
+ dependency_type=dependency_type,
550
+ factory=factory,
551
+ scope=scope,
552
+ from_context=False,
553
+ parameters=tuple(parameters),
554
+ is_class=is_class,
555
+ is_coroutine=is_coroutine,
556
+ is_generator=is_generator,
557
+ is_async_generator=is_async_generator,
558
+ is_async=is_coroutine or is_async_generator,
559
+ is_resource=is_resource,
513
560
  )
514
561
 
515
- # Handle unresolved parameters
516
- if unresolved_parameter:
517
- if is_scoped: # pragma: no cover
518
- # Note: This branch is currently unreachable because
519
- # unresolved_parameter is only set when is_scoped=False
520
- self._resolver.add_unresolved(interface)
521
- else:
522
- raise LookupError(
523
- f"The provider `{name}` depends on `{unresolved_parameter.name}` "
524
- f"of type `{type_repr(unresolved_parameter.annotation)}`, "
525
- "which has not been registered or set. To resolve this, ensure "
526
- f"that `{unresolved_parameter.name}` is registered before "
527
- f"attempting to use it."
528
- ) from unresolved_exc
529
-
530
- # Create and register provider
531
- provider = Provider(
532
- call=call,
533
- scope=scope,
534
- interface=interface,
535
- name=name,
536
- parameters=tuple(parameters),
537
- is_class=is_class,
538
- is_coroutine=is_coroutine,
539
- is_generator=is_generator,
540
- is_async_generator=is_async_generator,
541
- is_async=is_coroutine or is_async_generator,
542
- is_resource=is_resource,
543
- )
544
-
545
562
  self._set_provider(provider)
546
-
547
563
  if override:
548
564
  self._resolver.clear_caches()
549
565
 
550
- return provider
566
+ # Resolve dependencies for providers registered after build()
567
+ if self.ready:
568
+ provider = self._ensure_provider_resolved(provider, set())
551
569
 
552
- def _validate_provider_scope(
553
- self, scope: Scope, name: str, is_resource: bool
554
- ) -> None:
555
- """Validate the provider scope."""
556
- if scope not in self._scopes:
557
- raise ValueError(
558
- f"The provider `{name}` scope is invalid. Only the following "
559
- f"scopes are supported: {', '.join(self._scopes.keys())}. "
560
- "Please use one of the supported scopes when registering a provider."
561
- )
562
- if scope == "transient" and is_resource:
563
- raise TypeError(
564
- f"The resource provider `{name}` is attempting to register "
565
- "with a transient scope, which is not allowed."
566
- )
570
+ return provider
567
571
 
568
- def _get_provider(self, interface: Any) -> Provider:
569
- """Get provider by interface."""
572
+ def _get_provider(self, dependency_type: Any) -> Provider:
573
+ """Get provider by dependency type."""
570
574
  try:
571
- return self._providers[interface]
575
+ return self._providers[dependency_type]
572
576
  except KeyError:
573
577
  raise LookupError(
574
- f"The provider interface for `{type_repr(interface)}` has "
575
- "not been registered. Please ensure that the provider interface is "
578
+ f"The provider for `{type_repr(dependency_type)}` has "
579
+ "not been registered. Please ensure that the provider is "
576
580
  "properly registered before attempting to use it."
577
581
  ) from None
578
582
 
579
583
  def _get_or_register_provider(
580
- self, interface: Any, defaults: dict[str, Any] | None = None
584
+ self, dependency_type: Any, defaults: dict[str, Any] | None = None
581
585
  ) -> Provider:
582
- """Get or register a provider by interface."""
586
+ """Get or register a provider by dependency type."""
587
+ registered = False
583
588
  try:
584
- return self._get_provider(interface)
589
+ provider = self._get_provider(dependency_type)
585
590
  except LookupError:
586
- if inspect.isclass(interface) and is_provided(interface):
587
- return self._register_provider(
588
- interface,
589
- interface.__provided__["scope"],
590
- NOT_SET,
591
+ if inspect.isclass(dependency_type) and is_provided(dependency_type):
592
+ provider = self._register_provider(
593
+ dependency_type,
594
+ dependency_type,
595
+ dependency_type.__provided__.get("scope", "singleton"),
596
+ dependency_type.__provided__.get("from_context", False),
591
597
  False,
592
598
  defaults,
593
599
  )
594
- raise LookupError(
595
- f"The provider interface `{type_repr(interface)}` is either not "
596
- "registered, not provided, or not set in the scoped context. "
597
- "Please ensure that the provider interface is properly registered and "
598
- "that the class is decorated with a scope before attempting to use it."
599
- ) from None
600
+ registered = True
601
+ else:
602
+ raise LookupError(
603
+ f"The provider `{type_repr(dependency_type)}` is either not "
604
+ "registered, not provided, or not set in the scoped context. "
605
+ "Please ensure that the provider is properly registered and "
606
+ "that the class is decorated with a scope before attempting to "
607
+ "use it."
608
+ ) from None
609
+
610
+ # Resolve dependencies:
611
+ # - For existing providers: only if container is not built yet
612
+ # - For newly auto-registered providers: always (even after build)
613
+ if registered or not self.ready:
614
+ provider = self._ensure_provider_resolved(provider, set())
615
+
616
+ return provider
617
+
618
+ def _ensure_provider_resolved( # noqa: C901
619
+ self, provider: Provider, resolving: set[Any]
620
+ ) -> Provider:
621
+ """Ensure dependencies are resolved, resolving on-the-fly if needed."""
622
+ # Check if we're already resolving this provider (circular dependency)
623
+ if provider.dependency_type in resolving:
624
+ return provider
625
+
626
+ # Check if already resolved by examining parameters
627
+ # A provider is resolved if all its parameters either:
628
+ # 1. Have a provider set (provider is not None), OR
629
+ # 2. Have a default value, OR
630
+ # 3. Are marked for context.set() (provider=None but in unresolved list)
631
+ all_resolved = all(
632
+ param.provider is not None
633
+ or param.has_default
634
+ or (
635
+ param.provider is None and param.shared_scope
636
+ ) # Unresolved for context.set()
637
+ for param in provider.parameters
638
+ )
639
+ if all_resolved:
640
+ return provider
641
+
642
+ # Mark as currently being resolved
643
+ resolving = resolving | {provider.dependency_type}
644
+
645
+ # Resolve dependencies for this provider
646
+ resolved_params: list[ProviderParameter] = []
647
+
648
+ for param in provider.parameters:
649
+ if param.provider is not None:
650
+ # Already resolved
651
+ resolved_params.append(param)
652
+ continue
653
+
654
+ dependency_type = param.dependency_type
655
+
656
+ # Try to resolve the dependency
657
+ # First check if this would create a circular dependency
658
+ if dependency_type in resolving:
659
+ raise ValueError(
660
+ f"Circular dependency detected: {provider} depends on "
661
+ f"{type_repr(dependency_type)}"
662
+ )
663
+
664
+ try:
665
+ dep_provider = self._get_provider(dependency_type)
666
+ except LookupError:
667
+ # Check if it's a @provided class
668
+ if inspect.isclass(dependency_type) and is_provided(dependency_type):
669
+ provided_scope = dependency_type.__provided__["scope"]
670
+
671
+ # Auto-register @provided class
672
+ dep_provider = self._register_provider(
673
+ dependency_type,
674
+ dependency_type,
675
+ provided_scope,
676
+ False,
677
+ False,
678
+ None,
679
+ )
680
+ # Recursively ensure the @provided class is resolved
681
+ dep_provider = self._ensure_provider_resolved(
682
+ dep_provider, resolving
683
+ )
684
+ elif param.has_default:
685
+ # Has default, can be missing
686
+ resolved_params.append(param)
687
+ continue
688
+ else:
689
+ # Required dependency is missing
690
+ raise LookupError(
691
+ f"The provider `{provider}` depends on `{param.name}` of type "
692
+ f"`{type_repr(dependency_type)}`, which has not been "
693
+ f"registered or set. To resolve this, ensure that "
694
+ f"`{param.name}` is registered before resolving, "
695
+ f"or register it with `from_context=True` if it should be "
696
+ f"provided via scoped context."
697
+ ) from None
698
+
699
+ # If the dependency is a from_context provider, mark it appropriately
700
+ if dep_provider.from_context:
701
+ resolved_params.append(
702
+ ProviderParameter(
703
+ name=param.name,
704
+ dependency_type=dependency_type,
705
+ default=param.default,
706
+ has_default=param.has_default,
707
+ provider=dep_provider,
708
+ shared_scope=True,
709
+ )
710
+ )
711
+ continue
712
+
713
+ # Ensure dependency is also resolved
714
+ dep_provider = self._ensure_provider_resolved(dep_provider, resolving)
715
+
716
+ # Validate scope compatibility
717
+ scope_hierarchy = (
718
+ self._scopes.get(provider.scope, ())
719
+ if provider.scope != "transient"
720
+ else ()
721
+ )
722
+ if scope_hierarchy and dep_provider.scope not in scope_hierarchy:
723
+ raise ValueError(
724
+ f"The provider `{provider}` with a `{provider.scope}` scope "
725
+ f"cannot depend on `{dep_provider}` with a "
726
+ f"`{dep_provider.scope}` scope. Please ensure all providers are "
727
+ f"registered with matching scopes."
728
+ )
729
+
730
+ # Calculate shared_scope
731
+ shared_scope = (
732
+ dep_provider.scope == provider.scope and provider.scope != "transient"
733
+ )
734
+
735
+ # Create resolved parameter (use unwrapped annotation)
736
+ resolved_params.append(
737
+ ProviderParameter(
738
+ name=param.name,
739
+ dependency_type=dependency_type,
740
+ default=param.default,
741
+ has_default=param.has_default,
742
+ provider=dep_provider,
743
+ shared_scope=shared_scope,
744
+ )
745
+ )
746
+
747
+ # Replace provider with resolved version
748
+ resolved_provider = Provider(
749
+ factory=provider.factory,
750
+ scope=provider.scope,
751
+ dependency_type=provider.dependency_type,
752
+ parameters=tuple(resolved_params),
753
+ is_class=provider.is_class,
754
+ is_coroutine=provider.is_coroutine,
755
+ is_generator=provider.is_generator,
756
+ is_async_generator=provider.is_async_generator,
757
+ is_async=provider.is_async,
758
+ is_resource=provider.is_resource,
759
+ from_context=provider.from_context,
760
+ )
761
+ self._providers[provider.dependency_type] = resolved_provider
762
+
763
+ return resolved_provider
600
764
 
601
765
  def _set_provider(self, provider: Provider) -> None:
602
- """Set a provider by interface."""
603
- self._providers[provider.interface] = provider
766
+ """Set a provider by dependency type."""
767
+ self._providers[provider.dependency_type] = provider
604
768
  if provider.is_resource:
605
- self._resources[provider.scope].append(provider.interface)
769
+ self._resources[provider.scope].append(provider.dependency_type)
606
770
 
607
771
  def _delete_provider(self, provider: Provider) -> None:
608
772
  """Delete a provider."""
609
- if provider.interface in self._providers:
610
- del self._providers[provider.interface]
773
+ if provider.dependency_type in self._providers:
774
+ del self._providers[provider.dependency_type]
611
775
  if provider.is_resource:
612
- self._resources[provider.scope].remove(provider.interface)
776
+ self._resources[provider.scope].remove(provider.dependency_type)
613
777
 
614
778
  # == Instance Resolution ==
615
779
 
616
780
  @overload
617
- def resolve(self, interface: type[T]) -> T: ...
781
+ def resolve(self, dependency_type: type[T], /) -> T: ...
618
782
 
619
783
  @overload
620
- def resolve(self, interface: T) -> T: ... # type: ignore
784
+ def resolve(self, dependency_type: T, /) -> T: ... # type: ignore
621
785
 
622
- def resolve(self, interface: type[T]) -> T:
623
- """Resolve an instance by interface using compiled sync resolver."""
624
- cached = self._resolver.get_cached(interface, is_async=False)
786
+ def resolve(self, dependency_type: type[T], /) -> T:
787
+ """Resolve an instance by dependency type using compiled sync resolver."""
788
+ cached = self._resolver.get_cached(dependency_type, is_async=False)
625
789
  if cached is not None:
626
790
  return cached.resolve(self)
627
791
 
628
- provider = self._get_or_register_provider(interface)
792
+ provider = self._get_or_register_provider(dependency_type)
629
793
  compiled = self._resolver.compile(provider, is_async=False)
630
794
  return compiled.resolve(self)
631
795
 
632
796
  @overload
633
- async def aresolve(self, interface: type[T]) -> T: ...
797
+ async def aresolve(self, dependency_type: type[T], /) -> T: ...
634
798
 
635
799
  @overload
636
- async def aresolve(self, interface: T) -> T: ...
800
+ async def aresolve(self, dependency_type: T, /) -> T: ...
637
801
 
638
- async def aresolve(self, interface: type[T]) -> T:
639
- """Resolve an instance by interface asynchronously."""
640
- cached = self._resolver.get_cached(interface, is_async=True)
802
+ async def aresolve(self, dependency_type: type[T], /) -> T:
803
+ """Resolve an instance by dependency type asynchronously."""
804
+ cached = self._resolver.get_cached(dependency_type, is_async=True)
641
805
  if cached is not None:
642
806
  return await cached.resolve(self)
643
807
 
644
- provider = self._get_or_register_provider(interface)
808
+ provider = self._get_or_register_provider(dependency_type)
645
809
  compiled = self._resolver.compile(provider, is_async=True)
646
810
  return await compiled.resolve(self)
647
811
 
648
- def create(self, interface: type[T], /, **defaults: Any) -> T:
649
- """Create an instance by interface."""
812
+ def create(self, dependency_type: type[T], /, **defaults: Any) -> T:
813
+ """Create an instance by dependency type."""
650
814
  if not defaults:
651
- cached = self._resolver.get_cached(interface, is_async=False)
815
+ cached = self._resolver.get_cached(dependency_type, is_async=False)
652
816
  if cached is not None:
653
817
  return cached.create(self, None)
654
818
 
655
- provider = self._get_or_register_provider(interface, defaults)
819
+ provider = self._get_or_register_provider(dependency_type, defaults)
656
820
  compiled = self._resolver.compile(provider, is_async=False)
657
821
  return compiled.create(self, defaults or None)
658
822
 
659
- async def acreate(self, interface: type[T], /, **defaults: Any) -> T:
660
- """Create an instance by interface asynchronously."""
823
+ async def acreate(self, dependency_type: type[T], /, **defaults: Any) -> T:
824
+ """Create an instance by dependency type asynchronously."""
661
825
  if not defaults:
662
- cached = self._resolver.get_cached(interface, is_async=True)
826
+ cached = self._resolver.get_cached(dependency_type, is_async=True)
663
827
  if cached is not None:
664
828
  return await cached.create(self, None)
665
829
 
666
- provider = self._get_or_register_provider(interface, defaults)
830
+ provider = self._get_or_register_provider(dependency_type, defaults)
667
831
  compiled = self._resolver.compile(provider, is_async=True)
668
832
  return await compiled.create(self, defaults or None)
669
833
 
670
- def is_resolved(self, interface: Any) -> bool:
671
- """Check if an instance by interface exists."""
834
+ def is_resolved(self, dependency_type: Any, /) -> bool:
835
+ """Check if an instance for the dependency type exists."""
672
836
  try:
673
- provider = self._get_provider(interface)
837
+ provider = self._get_provider(dependency_type)
674
838
  except LookupError:
675
839
  return False
676
840
  if provider.scope == "transient":
677
841
  return False
678
842
  context = self._get_instance_context(provider.scope)
679
- return interface in context
843
+ return dependency_type in context
680
844
 
681
- def release(self, interface: Any) -> None:
682
- """Release an instance by interface."""
683
- provider = self._get_provider(interface)
845
+ def release(self, dependency_type: Any, /) -> None:
846
+ """Release an instance by dependency type."""
847
+ provider = self._get_provider(dependency_type)
684
848
  if provider.scope == "transient":
685
849
  return None
686
850
  context = self._get_instance_context(provider.scope)
687
- del context[interface]
851
+ del context[dependency_type]
688
852
 
689
853
  def reset(self) -> None:
690
854
  """Reset resolved instances."""
691
- for interface, provider in self._providers.items():
855
+ for dependency_type, provider in self._providers.items():
692
856
  if provider.scope == "transient":
693
857
  continue
694
858
  try:
695
859
  context = self._get_instance_context(provider.scope)
696
860
  except LookupError:
697
861
  continue
698
- del context[interface]
862
+ del context[dependency_type]
699
863
 
700
864
  # == Injection Utilities ==
701
865
 
@@ -736,24 +900,294 @@ class Container:
736
900
  # == Package Scanning ==
737
901
 
738
902
  def scan(
739
- self, /, packages: PackageOrIterable, *, tags: Iterable[str] | None = None
903
+ self,
904
+ /,
905
+ packages: PackageOrIterable,
906
+ *,
907
+ tags: Iterable[str] | None = None,
908
+ ignore: PackageOrIterable | None = None,
740
909
  ) -> None:
741
- self._scanner.scan(packages=packages, tags=tags)
910
+ self._scanner.scan(packages=packages, tags=tags, ignore=ignore)
911
+
912
+ # == Build ==
913
+
914
+ def build(self) -> None:
915
+ """Build the container by validating the complete dependency graph."""
916
+ if self.ready:
917
+ raise RuntimeError("Container has already been built")
918
+
919
+ self._resolve_provider_dependencies()
920
+ self._detect_circular_dependencies()
921
+ self._validate_scope_compatibility()
922
+
923
+ self._ready = True
924
+
925
+ def rebuild(self) -> None:
926
+ """Rebuild the container by re-validating the complete dependency graph."""
927
+ if self._ready:
928
+ self._ready = False
929
+ self._resolver.clear_caches()
930
+ self.build()
931
+
932
+ def graph(
933
+ self,
934
+ output_format: Literal["tree", "mermaid", "dot", "json"] = "tree",
935
+ *,
936
+ full_path: bool = False,
937
+ **kwargs: Any,
938
+ ) -> str:
939
+ """Draw the dependency graph."""
940
+ if not self.ready:
941
+ self.build()
942
+ return self._graph.draw(
943
+ output_format=output_format,
944
+ full_path=full_path,
945
+ **kwargs,
946
+ )
947
+
948
+ def _resolve_provider_dependencies(self) -> None:
949
+ """Resolve all provider dependencies by filling in provider references."""
950
+ for dependency_type, provider in list(self._providers.items()):
951
+ resolved_params: list[ProviderParameter] = []
952
+
953
+ for param in provider.parameters:
954
+ if param.provider is not None:
955
+ # Already resolved
956
+ resolved_params.append(param)
957
+ continue
958
+
959
+ param_dependency_type = param.dependency_type
960
+
961
+ # Try to resolve the dependency
962
+ try:
963
+ dep_provider = self._get_provider(param_dependency_type)
964
+ except LookupError:
965
+ # Check if it's a @provided class
966
+ if inspect.isclass(param_dependency_type) and is_provided(
967
+ param_dependency_type
968
+ ):
969
+ provided_scope = param_dependency_type.__provided__["scope"]
970
+
971
+ # Auto-register @provided class
972
+ dep_provider = self._register_provider(
973
+ param_dependency_type,
974
+ param_dependency_type,
975
+ provided_scope,
976
+ False,
977
+ False,
978
+ None,
979
+ )
980
+ elif param.has_default:
981
+ # Has default, can be missing
982
+ resolved_params.append(param)
983
+ continue
984
+ else:
985
+ # Required dependency is missing
986
+ raise LookupError(
987
+ f"The provider `{provider}` depends on "
988
+ f"`{param.name}` of type "
989
+ f"`{type_repr(param_dependency_type)}`, which has not been "
990
+ f"registered or set. To resolve this, ensure that "
991
+ f"`{param.name}` is registered before calling build(), "
992
+ f"or register it with `from_context=True` if it should be "
993
+ f"provided via scoped context."
994
+ ) from None
995
+
996
+ # If the dependency is a from_context provider, mark it appropriately
997
+ if dep_provider.from_context:
998
+ resolved_params.append(
999
+ ProviderParameter(
1000
+ name=param.name,
1001
+ dependency_type=param_dependency_type,
1002
+ default=param.default,
1003
+ has_default=param.has_default,
1004
+ provider=dep_provider,
1005
+ shared_scope=True,
1006
+ )
1007
+ )
1008
+ continue
1009
+
1010
+ # Calculate shared_scope
1011
+ shared_scope = (
1012
+ dep_provider.scope == provider.scope
1013
+ and provider.scope != "transient"
1014
+ )
1015
+
1016
+ # Create resolved parameter
1017
+ resolved_params.append(
1018
+ ProviderParameter(
1019
+ name=param.name,
1020
+ dependency_type=param_dependency_type,
1021
+ default=param.default,
1022
+ has_default=param.has_default,
1023
+ provider=dep_provider,
1024
+ shared_scope=shared_scope,
1025
+ )
1026
+ )
1027
+
1028
+ # Replace provider with resolved version
1029
+ resolved_provider = Provider(
1030
+ factory=provider.factory,
1031
+ scope=provider.scope,
1032
+ dependency_type=provider.dependency_type,
1033
+ parameters=tuple(resolved_params),
1034
+ is_class=provider.is_class,
1035
+ is_coroutine=provider.is_coroutine,
1036
+ is_generator=provider.is_generator,
1037
+ is_async_generator=provider.is_async_generator,
1038
+ is_async=provider.is_async,
1039
+ is_resource=provider.is_resource,
1040
+ from_context=provider.from_context,
1041
+ )
1042
+ self._providers[dependency_type] = resolved_provider
1043
+
1044
+ def _detect_circular_dependencies(self) -> None:
1045
+ """Detect circular dependencies in the provider graph."""
1046
+
1047
+ def visit(
1048
+ dependency_type: Any,
1049
+ provider: Provider,
1050
+ path: list[str],
1051
+ visited: set[Any],
1052
+ in_path: set[Any],
1053
+ ) -> None:
1054
+ """DFS traversal to detect cycles."""
1055
+ if dependency_type in in_path:
1056
+ # Found a cycle!
1057
+ cycle_start = next(
1058
+ i for i, name in enumerate(path) if name == str(provider)
1059
+ )
1060
+ cycle_path = " -> ".join(path[cycle_start:] + [str(provider)])
1061
+ raise ValueError(
1062
+ f"Circular dependency detected: {cycle_path}. "
1063
+ f"Please restructure your dependencies to break the cycle."
1064
+ )
1065
+
1066
+ if dependency_type in visited:
1067
+ return
1068
+
1069
+ visited.add(dependency_type)
1070
+ in_path.add(dependency_type)
1071
+ path.append(str(provider))
1072
+
1073
+ # Visit dependencies
1074
+ for param in provider.parameters:
1075
+ # Look up the dependency provider from self._providers instead of
1076
+ # using param.provider, which might be stale/unresolved
1077
+ if param.dependency_type in self._providers:
1078
+ dep_provider = self._providers[param.dependency_type]
1079
+ visit(
1080
+ param.dependency_type,
1081
+ dep_provider,
1082
+ path,
1083
+ visited,
1084
+ in_path,
1085
+ )
1086
+
1087
+ path.pop()
1088
+ in_path.remove(dependency_type)
1089
+
1090
+ visited: set[Any] = set()
1091
+
1092
+ for dependency_type, provider in self._providers.items():
1093
+ if dependency_type not in visited:
1094
+ visit(dependency_type, provider, [], visited, set())
1095
+
1096
+ def _validate_scope_compatibility(self) -> None:
1097
+ """Validate that all dependencies have compatible scopes."""
1098
+ for provider in self._providers.values():
1099
+ scope = provider.scope
1100
+
1101
+ # Skip validation for transient (can depend on anything)
1102
+ if scope == "transient":
1103
+ continue
1104
+
1105
+ # Get scope hierarchy for this provider
1106
+ scope_hierarchy = (
1107
+ self._scopes.get(scope, ()) if scope != "transient" else ()
1108
+ )
1109
+
1110
+ # Check each dependency
1111
+ for param in provider.parameters:
1112
+ if param.provider is None:
1113
+ # Unresolved (allowed for scoped providers)
1114
+ continue
1115
+
1116
+ dep_scope = param.provider.scope
1117
+
1118
+ # Validate scope compatibility
1119
+ if scope_hierarchy and dep_scope not in scope_hierarchy:
1120
+ raise ValueError(
1121
+ f"The provider `{provider}` with a `{scope}` scope "
1122
+ f"cannot depend on `{param.provider}` with a "
1123
+ f"`{dep_scope}` scope. Please ensure all providers are "
1124
+ f"registered with matching scopes."
1125
+ )
742
1126
 
743
1127
  # == Testing / Override Support ==
744
1128
 
1129
+ def enable_test_mode(self) -> None:
1130
+ """Enable test mode for override support on all resolutions."""
1131
+ self._test_mode = True
1132
+
1133
+ def disable_test_mode(self) -> None:
1134
+ """Disable test mode for override support on all resolutions."""
1135
+ self._test_mode = False
1136
+
1137
+ @contextlib.contextmanager
1138
+ def test_mode(self) -> Iterator[None]:
1139
+ if self._test_mode:
1140
+ yield
1141
+ return
1142
+
1143
+ self._test_mode = True
1144
+ try:
1145
+ yield
1146
+ finally:
1147
+ self._test_mode = False
1148
+
745
1149
  @contextlib.contextmanager
746
- def override(self, interface: Any, instance: Any) -> Iterator[None]:
1150
+ def override(
1151
+ self,
1152
+ dependency_type: Any = NOT_SET,
1153
+ /,
1154
+ instance: Any = NOT_SET,
1155
+ *,
1156
+ interface: Any = NOT_SET,
1157
+ ) -> Iterator[None]:
747
1158
  """Override a dependency with a specific instance for testing."""
748
- if not self.has_provider_for(interface):
1159
+ if not self._test_mode:
1160
+ warnings.warn(
1161
+ "Using override() without enabling test mode. "
1162
+ "Consider using `with container.test_mode():` for consistent behavior.",
1163
+ UserWarning,
1164
+ stacklevel=2,
1165
+ )
1166
+
1167
+ if interface is not NOT_SET:
1168
+ warnings.warn(
1169
+ "The `interface` is deprecated. Use `dependency_type` instead.",
1170
+ DeprecationWarning,
1171
+ stacklevel=2,
1172
+ )
1173
+ if dependency_type is NOT_SET:
1174
+ dependency_type = interface
1175
+
1176
+ if dependency_type is NOT_SET:
1177
+ raise TypeError("override() missing required argument: 'dependency_type'")
1178
+
1179
+ if instance is NOT_SET:
1180
+ raise TypeError("override() missing required argument: 'instance'")
1181
+
1182
+ if not self.has_provider_for(dependency_type):
749
1183
  raise LookupError(
750
- f"The provider interface `{type_repr(interface)}` not registered."
1184
+ f"The provider `{type_repr(dependency_type)}` is not registered."
751
1185
  )
752
- self._resolver.add_override(interface, instance)
1186
+ self._resolver.add_override(dependency_type, instance)
753
1187
  try:
754
1188
  yield
755
1189
  finally:
756
- self._resolver.remove_override(interface)
1190
+ self._resolver.remove_override(dependency_type)
757
1191
 
758
1192
 
759
1193
  def import_container(container_path: str) -> Container: