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/_cli.py +80 -0
- anydi/_container.py +702 -273
- anydi/_context.py +14 -14
- anydi/_decorators.py +115 -8
- anydi/_graph.py +217 -0
- anydi/_injector.py +12 -10
- anydi/_marker.py +11 -13
- anydi/_provider.py +46 -8
- anydi/_resolver.py +205 -159
- anydi/_scanner.py +46 -7
- anydi/ext/fastapi.py +1 -1
- anydi/ext/faststream.py +1 -1
- anydi/ext/pydantic_settings.py +3 -3
- anydi/ext/pytest_plugin.py +6 -2
- anydi/ext/typer.py +7 -7
- {anydi-0.67.1.dist-info → anydi-0.68.0.dist-info}/METADATA +1 -1
- anydi-0.68.0.dist-info/RECORD +29 -0
- {anydi-0.67.1.dist-info → anydi-0.68.0.dist-info}/entry_points.txt +3 -0
- anydi-0.67.1.dist-info/RECORD +0 -27
- {anydi-0.67.1.dist-info → anydi-0.68.0.dist-info}/WHEEL +0 -0
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.
|
|
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.
|
|
78
|
+
provider.dependency_type,
|
|
79
|
+
provider.factory,
|
|
81
80
|
provider.scope,
|
|
82
|
-
provider.
|
|
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
|
|
122
|
-
self.resolve(
|
|
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
|
|
145
|
-
await self.aresolve(
|
|
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
|
|
169
|
-
if not is_event_type(
|
|
174
|
+
for dependency_type in self._resources.get(scope, []):
|
|
175
|
+
if not is_event_type(dependency_type):
|
|
170
176
|
continue
|
|
171
|
-
self.resolve(
|
|
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
|
|
195
|
-
if not is_event_type(
|
|
200
|
+
for dependency_type in self._resources.get(scope, []):
|
|
201
|
+
if not is_event_type(dependency_type):
|
|
196
202
|
continue
|
|
197
|
-
await self.aresolve(
|
|
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
|
-
|
|
331
|
-
|
|
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
|
|
337
|
-
if
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
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
|
|
385
|
+
f"The provider `{type_repr(dependency_type)}` is not registered."
|
|
354
386
|
)
|
|
355
387
|
|
|
356
|
-
provider = self._get_provider(
|
|
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[
|
|
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,
|
|
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
|
-
|
|
415
|
+
dependency_type: Any,
|
|
416
|
+
factory: Callable[..., Any],
|
|
384
417
|
scope: Scope,
|
|
385
|
-
|
|
386
|
-
override: bool
|
|
387
|
-
defaults: dict[str, Any] | 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
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
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
|
-
#
|
|
432
|
-
|
|
433
|
-
|
|
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
|
-
|
|
440
|
-
|
|
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
|
-
|
|
443
|
-
|
|
443
|
+
"The `dependency_type` parameter is required when using "
|
|
444
|
+
"`from_context=True`."
|
|
444
445
|
)
|
|
445
|
-
if
|
|
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
|
-
"
|
|
448
|
-
|
|
477
|
+
f"The resource provider `{name}` is attempting to register "
|
|
478
|
+
"with a transient scope, which is not allowed."
|
|
449
479
|
)
|
|
450
480
|
|
|
451
|
-
|
|
452
|
-
|
|
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
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
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
|
-
|
|
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
|
-
|
|
491
|
-
|
|
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=
|
|
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
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
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
|
-
|
|
566
|
+
# Resolve dependencies for providers registered after build()
|
|
567
|
+
if self.ready:
|
|
568
|
+
provider = self._ensure_provider_resolved(provider, set())
|
|
556
569
|
|
|
557
|
-
|
|
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,
|
|
574
|
-
"""Get provider by
|
|
572
|
+
def _get_provider(self, dependency_type: Any) -> Provider:
|
|
573
|
+
"""Get provider by dependency type."""
|
|
575
574
|
try:
|
|
576
|
-
return self._providers[
|
|
577
|
-
except KeyError
|
|
575
|
+
return self._providers[dependency_type]
|
|
576
|
+
except KeyError:
|
|
578
577
|
raise LookupError(
|
|
579
|
-
f"The provider
|
|
580
|
-
"not been registered. Please ensure that the provider
|
|
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
|
|
581
|
+
) from None
|
|
583
582
|
|
|
584
583
|
def _get_or_register_provider(
|
|
585
|
-
self,
|
|
584
|
+
self, dependency_type: Any, defaults: dict[str, Any] | None = None
|
|
586
585
|
) -> Provider:
|
|
587
|
-
"""Get or register a provider by
|
|
586
|
+
"""Get or register a provider by dependency type."""
|
|
587
|
+
registered = False
|
|
588
588
|
try:
|
|
589
|
-
|
|
590
|
-
except
|
|
591
|
-
if inspect.isclass(
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
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
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
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
|
|
608
|
-
self._providers[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.
|
|
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.
|
|
615
|
-
del self._providers[provider.
|
|
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.
|
|
776
|
+
self._resources[provider.scope].remove(provider.dependency_type)
|
|
618
777
|
|
|
619
778
|
# == Instance Resolution ==
|
|
620
779
|
|
|
621
780
|
@overload
|
|
622
|
-
def resolve(self,
|
|
781
|
+
def resolve(self, dependency_type: type[T], /) -> T: ...
|
|
623
782
|
|
|
624
783
|
@overload
|
|
625
|
-
def resolve(self,
|
|
784
|
+
def resolve(self, dependency_type: T, /) -> T: ... # type: ignore
|
|
626
785
|
|
|
627
|
-
def resolve(self,
|
|
628
|
-
"""Resolve an instance by
|
|
629
|
-
cached = self._resolver.get_cached(
|
|
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(
|
|
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,
|
|
797
|
+
async def aresolve(self, dependency_type: type[T], /) -> T: ...
|
|
639
798
|
|
|
640
799
|
@overload
|
|
641
|
-
async def aresolve(self,
|
|
800
|
+
async def aresolve(self, dependency_type: T, /) -> T: ...
|
|
642
801
|
|
|
643
|
-
async def aresolve(self,
|
|
644
|
-
"""Resolve an instance by
|
|
645
|
-
cached = self._resolver.get_cached(
|
|
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(
|
|
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,
|
|
654
|
-
"""Create an instance by
|
|
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(
|
|
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(
|
|
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,
|
|
665
|
-
"""Create an instance by
|
|
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(
|
|
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(
|
|
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,
|
|
676
|
-
"""Check if an instance
|
|
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(
|
|
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
|
|
843
|
+
return dependency_type in context
|
|
685
844
|
|
|
686
|
-
def release(self,
|
|
687
|
-
"""Release an instance by
|
|
688
|
-
provider = self._get_provider(
|
|
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[
|
|
851
|
+
del context[dependency_type]
|
|
693
852
|
|
|
694
853
|
def reset(self) -> None:
|
|
695
854
|
"""Reset resolved instances."""
|
|
696
|
-
for
|
|
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[
|
|
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,
|
|
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(
|
|
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.
|
|
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
|
|
1184
|
+
f"The provider `{type_repr(dependency_type)}` is not registered."
|
|
756
1185
|
)
|
|
757
|
-
self._resolver.add_override(
|
|
1186
|
+
self._resolver.add_override(dependency_type, instance)
|
|
758
1187
|
try:
|
|
759
1188
|
yield
|
|
760
1189
|
finally:
|
|
761
|
-
self._resolver.remove_override(
|
|
1190
|
+
self._resolver.remove_override(dependency_type)
|
|
762
1191
|
|
|
763
1192
|
|
|
764
1193
|
def import_container(container_path: str) -> Container:
|