anydi 0.67.1__py3-none-any.whl → 0.68.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,29 +8,24 @@ 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
23
25
  from ._provider import Provider, ProviderDef, ProviderKind, ProviderParameter
24
26
  from ._resolver import Resolver
25
27
  from ._scanner import PackageOrIterable, Scanner
26
- from ._types import (
27
- NOT_SET,
28
- Event,
29
- Scope,
30
- is_event_type,
31
- is_iterator_type,
32
- is_none_type,
33
- )
28
+ from ._types import NOT_SET, Event, Scope, is_event_type, is_iterator_type, is_none_type
34
29
 
35
30
  T = TypeVar("T", bound=Any)
36
31
  P = ParamSpec("P")
@@ -62,24 +57,30 @@ class Container:
62
57
  self._injector = Injector(self)
63
58
  self._modules = ModuleRegistrar(self)
64
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
65
67
 
66
68
  # Register default scopes
67
69
  self.register_scope("request")
68
70
 
69
71
  # Register self as provider
70
- self._register_provider(
71
- lambda: self,
72
- "singleton",
73
- Container,
74
- )
72
+ self.register(Container, lambda: self, scope="singleton")
75
73
 
76
74
  # Register providers
77
75
  providers = providers or []
78
76
  for provider in providers:
79
77
  self._register_provider(
80
- provider.call,
78
+ provider.dependency_type,
79
+ provider.factory,
81
80
  provider.scope,
82
- provider.interface,
81
+ provider.from_context,
82
+ False,
83
+ None,
83
84
  )
84
85
 
85
86
  # Register modules
@@ -94,6 +95,11 @@ class Container:
94
95
  """Get the registered providers."""
95
96
  return self._providers
96
97
 
98
+ @property
99
+ def ready(self) -> bool:
100
+ """Check if the container is ready."""
101
+ return self._ready
102
+
97
103
  @property
98
104
  def logger(self) -> logging.Logger:
99
105
  """Get the logger instance."""
@@ -118,8 +124,8 @@ class Container:
118
124
  def start(self) -> None:
119
125
  """Start the singleton context."""
120
126
  # Resolve all singleton resources
121
- for interface in self._resources.get("singleton", []):
122
- self.resolve(interface)
127
+ for dependency_type in self._resources.get("singleton", []):
128
+ self.resolve(dependency_type)
123
129
 
124
130
  def close(self) -> None:
125
131
  """Close the singleton context."""
@@ -141,8 +147,8 @@ class Container:
141
147
 
142
148
  async def astart(self) -> None:
143
149
  """Start the singleton context asynchronously."""
144
- for interface in self._resources.get("singleton", []):
145
- await self.aresolve(interface)
150
+ for dependency_type in self._resources.get("singleton", []):
151
+ await self.aresolve(dependency_type)
146
152
 
147
153
  async def aclose(self) -> None:
148
154
  """Close the singleton context asynchronously."""
@@ -165,10 +171,10 @@ class Container:
165
171
  token = context_var.set(context)
166
172
 
167
173
  # Resolve all request resources
168
- for interface in self._resources.get(scope, []):
169
- if not is_event_type(interface):
174
+ for dependency_type in self._resources.get(scope, []):
175
+ if not is_event_type(dependency_type):
170
176
  continue
171
- self.resolve(interface)
177
+ self.resolve(dependency_type)
172
178
 
173
179
  with context:
174
180
  yield context
@@ -191,10 +197,10 @@ class Container:
191
197
  token = context_var.set(context)
192
198
 
193
199
  # Resolve all request resources
194
- for interface in self._resources.get(scope, []):
195
- if not is_event_type(interface):
200
+ for dependency_type in self._resources.get(scope, []):
201
+ if not is_event_type(dependency_type):
196
202
  continue
197
- await self.aresolve(interface)
203
+ await self.aresolve(dependency_type)
198
204
 
199
205
  async with context:
200
206
  yield context
@@ -327,33 +333,59 @@ class Container:
327
333
 
328
334
  def register(
329
335
  self,
330
- interface: Any,
331
- call: Callable[..., Any] = NOT_SET,
336
+ dependency_type: Any = NOT_SET,
337
+ factory: Callable[..., Any] = NOT_SET,
332
338
  *,
333
339
  scope: Scope = "singleton",
340
+ from_context: bool = False,
334
341
  override: bool = False,
342
+ interface: Any = NOT_SET,
343
+ call: Callable[..., Any] = NOT_SET,
335
344
  ) -> Provider:
336
- """Register a provider for the specified interface."""
337
- if call is NOT_SET:
338
- call = interface
339
- return self._register_provider(call, scope, interface, override)
340
-
341
- def is_registered(self, interface: Any) -> bool:
342
- """Check if a provider is registered for the specified interface."""
343
- return interface in self._providers
344
-
345
- def has_provider_for(self, interface: Any) -> bool:
346
- """Check if a provider exists for the specified interface."""
347
- return self.is_registered(interface) or is_provided(interface)
348
-
349
- def unregister(self, interface: Any) -> None:
350
- """Unregister a provider by interface."""
351
- 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):
352
384
  raise LookupError(
353
- f"The provider interface `{type_repr(interface)}` not registered."
385
+ f"The provider `{type_repr(dependency_type)}` is not registered."
354
386
  )
355
387
 
356
- provider = self._get_provider(interface)
388
+ provider = self._get_provider(dependency_type)
357
389
 
358
390
  # Cleanup instance context
359
391
  if provider.scope != "transient":
@@ -362,7 +394,7 @@ class Container:
362
394
  except LookupError:
363
395
  pass
364
396
  else:
365
- del context[interface]
397
+ del context[dependency_type]
366
398
 
367
399
  # Cleanup provider references
368
400
  self._delete_provider(provider)
@@ -373,334 +405,461 @@ class Container:
373
405
  """Decorator to register a provider function with the specified scope."""
374
406
 
375
407
  def decorator(call: Callable[P, T]) -> Callable[P, T]:
376
- self._register_provider(call, scope, NOT_SET, override)
408
+ self._register_provider(NOT_SET, call, scope, False, override, None)
377
409
  return call
378
410
 
379
411
  return decorator
380
412
 
381
413
  def _register_provider( # noqa: C901
382
414
  self,
383
- call: Callable[..., Any],
415
+ dependency_type: Any,
416
+ factory: Callable[..., Any],
384
417
  scope: Scope,
385
- interface: Any = NOT_SET,
386
- override: bool = False,
387
- defaults: dict[str, Any] | None = None,
418
+ from_context: bool,
419
+ override: bool,
420
+ defaults: dict[str, Any] | None,
388
421
  ) -> Provider:
389
422
  """Register a provider with the specified scope."""
390
- name = type_repr(call)
391
- kind = ProviderKind.from_call(call)
392
- is_class = kind == ProviderKind.CLASS
393
- is_coroutine = kind == ProviderKind.COROUTINE
394
- is_generator = kind == ProviderKind.GENERATOR
395
- is_async_generator = kind == ProviderKind.ASYNC_GENERATOR
396
- is_resource = is_generator or is_async_generator
397
-
398
- # Validate scope
399
- self._validate_provider_scope(scope, name, is_resource)
400
-
401
- # Get signature and detect interface
402
- signature = inspect.signature(call, eval_str=True)
403
-
404
- if interface is NOT_SET:
405
- interface = call if is_class else signature.return_annotation
406
- if interface is inspect.Signature.empty:
407
- interface = None
408
-
409
- # Handle iterator types for resources
410
- interface_origin = get_origin(interface)
411
- if is_iterator_type(interface) or is_iterator_type(interface_origin):
412
- if args := get_args(interface):
413
- interface = args[0]
414
- if is_none_type(interface):
415
- interface = type(f"Event_{uuid.uuid4().hex}", (Event,), {})
416
- else:
417
- raise TypeError(
418
- f"Cannot use `{name}` resource type annotation "
419
- "without actual type argument."
420
- )
421
-
422
- # Validate interface
423
- if is_none_type(interface):
424
- raise TypeError(f"Missing `{name}` provider return annotation.")
425
-
426
- if interface in self._providers and not override:
427
- raise LookupError(
428
- f"The provider interface `{type_repr(interface)}` already registered."
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()."
429
428
  )
430
429
 
431
- # Process parameters
432
- parameters: list[ProviderParameter] = []
433
- scope_provider: dict[Scope, Provider] = {}
434
- unresolved_parameter = None
435
- unresolved_exc: LookupError | None = None
436
- is_scoped = scope not in ("singleton", "transient")
437
- scope_hierarchy = self._scopes.get(scope, ()) if scope != "transient" else ()
430
+ # Default factory to dependency_type if not set
431
+ if not from_context and factory is NOT_SET:
432
+ factory = dependency_type
438
433
 
439
- for parameter in signature.parameters.values():
440
- if parameter.annotation is inspect.Parameter.empty:
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:
441
442
  raise TypeError(
442
- f"Missing provider `{name}` "
443
- f"dependency `{parameter.name}` annotation."
443
+ "The `dependency_type` parameter is required when using "
444
+ "`from_context=True`."
444
445
  )
445
- if parameter.kind == inspect.Parameter.POSITIONAL_ONLY:
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."
450
+ )
451
+
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,
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:
446
476
  raise TypeError(
447
- "Positional-only parameters "
448
- f"are not allowed in the provider `{name}`."
477
+ f"The resource provider `{name}` is attempting to register "
478
+ "with a transient scope, which is not allowed."
449
479
  )
450
480
 
451
- has_default = parameter.default is not inspect.Parameter.empty
452
- default = parameter.default if has_default else NOT_SET
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,), {})
453
501
 
454
- try:
455
- sub_provider = self._get_or_register_provider(parameter.annotation)
456
- except LookupError as exc:
457
- if (defaults and parameter.name in defaults) or has_default:
458
- continue
459
- # For scoped dependencies, allow unresolved parameters via context.set()
460
- if is_scoped:
461
- self._resolver.add_unresolved(parameter.annotation)
462
- parameters.append(
463
- ProviderParameter(
464
- name=parameter.name,
465
- annotation=parameter.annotation,
466
- default=default,
467
- has_default=has_default,
468
- provider=None,
469
- shared_scope=True,
470
- )
502
+ if is_none_type(dependency_type):
503
+ raise TypeError(f"Missing `{name}` provider return annotation.")
504
+
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."
509
+ )
510
+
511
+ # Process parameters (lazy - store without resolving dependencies)
512
+ parameters: list[ProviderParameter] = []
513
+
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."
471
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}`."
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:
472
534
  continue
473
- unresolved_parameter = parameter
474
- unresolved_exc = exc
475
- continue
476
535
 
477
- # Track scope providers for validation
478
- scope_provider.setdefault(sub_provider.scope, sub_provider)
479
-
480
- # For scoped dependencies with same scope having unresolved params,
481
- # defer to context.set() instead
482
- if (
483
- is_scoped
484
- and sub_provider.scope == scope
485
- and any(p.provider is None for p in sub_provider.parameters)
486
- ):
487
- self._resolver.add_unresolved(parameter.annotation)
536
+ # Lazy registration: Store parameter without resolving dependencies
488
537
  parameters.append(
489
538
  ProviderParameter(
490
- name=parameter.name,
491
- annotation=parameter.annotation,
539
+ dependency_type=param.annotation,
540
+ name=param.name,
492
541
  default=default,
493
542
  has_default=has_default,
494
- provider=None,
495
- shared_scope=True,
543
+ provider=None, # Lazy - will be resolved in build()
544
+ shared_scope=False, # Lazy - will be computed in build()
496
545
  )
497
546
  )
498
- continue
499
-
500
- # Validate scope compatibility inline
501
- if scope_hierarchy and sub_provider.scope not in scope_hierarchy:
502
- raise ValueError(
503
- f"The provider `{name}` with a `{scope}` scope "
504
- f"cannot depend on `{sub_provider}` with a "
505
- f"`{sub_provider.scope}` scope. Please ensure all "
506
- "providers are registered with matching scopes."
507
- )
508
547
 
509
- parameters.append(
510
- ProviderParameter(
511
- name=parameter.name,
512
- annotation=parameter.annotation,
513
- default=default,
514
- has_default=has_default,
515
- provider=sub_provider,
516
- shared_scope=sub_provider.scope == scope and scope != "transient",
517
- )
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,
518
560
  )
519
561
 
520
- # Handle unresolved parameters
521
- if unresolved_parameter:
522
- if is_scoped: # pragma: no cover
523
- # Note: This branch is currently unreachable because
524
- # unresolved_parameter is only set when is_scoped=False
525
- self._resolver.add_unresolved(interface)
526
- else:
527
- raise LookupError(
528
- f"The provider `{name}` depends on `{unresolved_parameter.name}` "
529
- f"of type `{type_repr(unresolved_parameter.annotation)}`, "
530
- "which has not been registered or set. To resolve this, ensure "
531
- f"that `{unresolved_parameter.name}` is registered before "
532
- f"attempting to use it."
533
- ) from unresolved_exc
534
-
535
- # Create and register provider
536
- provider = Provider(
537
- call=call,
538
- scope=scope,
539
- interface=interface,
540
- name=name,
541
- parameters=tuple(parameters),
542
- is_class=is_class,
543
- is_coroutine=is_coroutine,
544
- is_generator=is_generator,
545
- is_async_generator=is_async_generator,
546
- is_async=is_coroutine or is_async_generator,
547
- is_resource=is_resource,
548
- )
549
-
550
562
  self._set_provider(provider)
551
-
552
563
  if override:
553
564
  self._resolver.clear_caches()
554
565
 
555
- return provider
566
+ # Resolve dependencies for providers registered after build()
567
+ if self.ready:
568
+ provider = self._ensure_provider_resolved(provider, set())
556
569
 
557
- def _validate_provider_scope(
558
- self, scope: Scope, name: str, is_resource: bool
559
- ) -> None:
560
- """Validate the provider scope."""
561
- if scope not in self._scopes:
562
- raise ValueError(
563
- f"The provider `{name}` scope is invalid. Only the following "
564
- f"scopes are supported: {', '.join(self._scopes.keys())}. "
565
- "Please use one of the supported scopes when registering a provider."
566
- )
567
- if scope == "transient" and is_resource:
568
- raise TypeError(
569
- f"The resource provider `{name}` is attempting to register "
570
- "with a transient scope, which is not allowed."
571
- )
570
+ return provider
572
571
 
573
- def _get_provider(self, interface: Any) -> Provider:
574
- """Get provider by interface."""
572
+ def _get_provider(self, dependency_type: Any) -> Provider:
573
+ """Get provider by dependency type."""
575
574
  try:
576
- return self._providers[interface]
577
- except KeyError as exc:
575
+ return self._providers[dependency_type]
576
+ except KeyError:
578
577
  raise LookupError(
579
- f"The provider interface for `{type_repr(interface)}` has "
580
- "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 "
581
580
  "properly registered before attempting to use it."
582
- ) from exc
581
+ ) from None
583
582
 
584
583
  def _get_or_register_provider(
585
- self, interface: Any, defaults: dict[str, Any] | None = None
584
+ self, dependency_type: Any, defaults: dict[str, Any] | None = None
586
585
  ) -> Provider:
587
- """Get or register a provider by interface."""
586
+ """Get or register a provider by dependency type."""
587
+ registered = False
588
588
  try:
589
- return self._providers[interface]
590
- except KeyError:
591
- if inspect.isclass(interface) and is_provided(interface):
592
- return self._register_provider(
593
- interface,
594
- interface.__provided__["scope"],
595
- NOT_SET,
589
+ provider = self._get_provider(dependency_type)
590
+ except LookupError:
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),
596
597
  False,
597
598
  defaults,
598
599
  )
599
- raise LookupError(
600
- f"The provider interface `{type_repr(interface)}` is either not "
601
- "registered, not provided, or not set in the scoped context. "
602
- "Please ensure that the provider interface is properly registered and "
603
- "that the class is decorated with a scope before attempting to use it."
604
- ) 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
605
764
 
606
765
  def _set_provider(self, provider: Provider) -> None:
607
- """Set a provider by interface."""
608
- self._providers[provider.interface] = provider
766
+ """Set a provider by dependency type."""
767
+ self._providers[provider.dependency_type] = provider
609
768
  if provider.is_resource:
610
- self._resources[provider.scope].append(provider.interface)
769
+ self._resources[provider.scope].append(provider.dependency_type)
611
770
 
612
771
  def _delete_provider(self, provider: Provider) -> None:
613
772
  """Delete a provider."""
614
- if provider.interface in self._providers:
615
- del self._providers[provider.interface]
773
+ if provider.dependency_type in self._providers:
774
+ del self._providers[provider.dependency_type]
616
775
  if provider.is_resource:
617
- self._resources[provider.scope].remove(provider.interface)
776
+ self._resources[provider.scope].remove(provider.dependency_type)
618
777
 
619
778
  # == Instance Resolution ==
620
779
 
621
780
  @overload
622
- def resolve(self, interface: type[T]) -> T: ...
781
+ def resolve(self, dependency_type: type[T], /) -> T: ...
623
782
 
624
783
  @overload
625
- def resolve(self, interface: T) -> T: ... # type: ignore
784
+ def resolve(self, dependency_type: T, /) -> T: ... # type: ignore
626
785
 
627
- def resolve(self, interface: type[T]) -> T:
628
- """Resolve an instance by interface using compiled sync resolver."""
629
- 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)
630
789
  if cached is not None:
631
790
  return cached.resolve(self)
632
791
 
633
- provider = self._get_or_register_provider(interface)
792
+ provider = self._get_or_register_provider(dependency_type)
634
793
  compiled = self._resolver.compile(provider, is_async=False)
635
794
  return compiled.resolve(self)
636
795
 
637
796
  @overload
638
- async def aresolve(self, interface: type[T]) -> T: ...
797
+ async def aresolve(self, dependency_type: type[T], /) -> T: ...
639
798
 
640
799
  @overload
641
- async def aresolve(self, interface: T) -> T: ...
800
+ async def aresolve(self, dependency_type: T, /) -> T: ...
642
801
 
643
- async def aresolve(self, interface: type[T]) -> T:
644
- """Resolve an instance by interface asynchronously."""
645
- 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)
646
805
  if cached is not None:
647
806
  return await cached.resolve(self)
648
807
 
649
- provider = self._get_or_register_provider(interface)
808
+ provider = self._get_or_register_provider(dependency_type)
650
809
  compiled = self._resolver.compile(provider, is_async=True)
651
810
  return await compiled.resolve(self)
652
811
 
653
- def create(self, interface: type[T], /, **defaults: Any) -> T:
654
- """Create an instance by interface."""
812
+ def create(self, dependency_type: type[T], /, **defaults: Any) -> T:
813
+ """Create an instance by dependency type."""
655
814
  if not defaults:
656
- cached = self._resolver.get_cached(interface, is_async=False)
815
+ cached = self._resolver.get_cached(dependency_type, is_async=False)
657
816
  if cached is not None:
658
817
  return cached.create(self, None)
659
818
 
660
- provider = self._get_or_register_provider(interface, defaults)
819
+ provider = self._get_or_register_provider(dependency_type, defaults)
661
820
  compiled = self._resolver.compile(provider, is_async=False)
662
821
  return compiled.create(self, defaults or None)
663
822
 
664
- async def acreate(self, interface: type[T], /, **defaults: Any) -> T:
665
- """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."""
666
825
  if not defaults:
667
- cached = self._resolver.get_cached(interface, is_async=True)
826
+ cached = self._resolver.get_cached(dependency_type, is_async=True)
668
827
  if cached is not None:
669
828
  return await cached.create(self, None)
670
829
 
671
- provider = self._get_or_register_provider(interface, defaults)
830
+ provider = self._get_or_register_provider(dependency_type, defaults)
672
831
  compiled = self._resolver.compile(provider, is_async=True)
673
832
  return await compiled.create(self, defaults or None)
674
833
 
675
- def is_resolved(self, interface: Any) -> bool:
676
- """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."""
677
836
  try:
678
- provider = self._get_provider(interface)
837
+ provider = self._get_provider(dependency_type)
679
838
  except LookupError:
680
839
  return False
681
840
  if provider.scope == "transient":
682
841
  return False
683
842
  context = self._get_instance_context(provider.scope)
684
- return interface in context
843
+ return dependency_type in context
685
844
 
686
- def release(self, interface: Any) -> None:
687
- """Release an instance by interface."""
688
- 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)
689
848
  if provider.scope == "transient":
690
849
  return None
691
850
  context = self._get_instance_context(provider.scope)
692
- del context[interface]
851
+ del context[dependency_type]
693
852
 
694
853
  def reset(self) -> None:
695
854
  """Reset resolved instances."""
696
- for interface, provider in self._providers.items():
855
+ for dependency_type, provider in self._providers.items():
697
856
  if provider.scope == "transient":
698
857
  continue
699
858
  try:
700
859
  context = self._get_instance_context(provider.scope)
701
860
  except LookupError:
702
861
  continue
703
- del context[interface]
862
+ del context[dependency_type]
704
863
 
705
864
  # == Injection Utilities ==
706
865
 
@@ -741,24 +900,294 @@ class Container:
741
900
  # == Package Scanning ==
742
901
 
743
902
  def scan(
744
- 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,
745
909
  ) -> None:
746
- 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
+ )
747
1126
 
748
1127
  # == Testing / Override Support ==
749
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
+
750
1149
  @contextlib.contextmanager
751
- 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]:
752
1158
  """Override a dependency with a specific instance for testing."""
753
- 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):
754
1183
  raise LookupError(
755
- f"The provider interface `{type_repr(interface)}` not registered."
1184
+ f"The provider `{type_repr(dependency_type)}` is not registered."
756
1185
  )
757
- self._resolver.add_override(interface, instance)
1186
+ self._resolver.add_override(dependency_type, instance)
758
1187
  try:
759
1188
  yield
760
1189
  finally:
761
- self._resolver.remove_override(interface)
1190
+ self._resolver.remove_override(dependency_type)
762
1191
 
763
1192
 
764
1193
  def import_container(container_path: str) -> Container: