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/_cli.py +80 -0
- anydi/_container.py +697 -263
- 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 +125 -380
- anydi/ext/typer.py +4 -4
- {anydi-0.67.2.dist-info → anydi-0.69.0.dist-info}/METADATA +1 -1
- anydi-0.69.0.dist-info/RECORD +29 -0
- {anydi-0.67.2.dist-info → anydi-0.69.0.dist-info}/entry_points.txt +3 -0
- anydi-0.67.2.dist-info/RECORD +0 -27
- {anydi-0.67.2.dist-info → anydi-0.69.0.dist-info}/WHEEL +0 -0
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.
|
|
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.
|
|
78
|
+
provider.dependency_type,
|
|
79
|
+
provider.factory,
|
|
74
80
|
provider.scope,
|
|
75
|
-
provider.
|
|
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
|
|
115
|
-
self.resolve(
|
|
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
|
|
138
|
-
await self.aresolve(
|
|
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
|
|
162
|
-
if not is_event_type(
|
|
174
|
+
for dependency_type in self._resources.get(scope, []):
|
|
175
|
+
if not is_event_type(dependency_type):
|
|
163
176
|
continue
|
|
164
|
-
self.resolve(
|
|
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
|
|
188
|
-
if not is_event_type(
|
|
200
|
+
for dependency_type in self._resources.get(scope, []):
|
|
201
|
+
if not is_event_type(dependency_type):
|
|
189
202
|
continue
|
|
190
|
-
await self.aresolve(
|
|
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
|
-
|
|
324
|
-
|
|
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
|
|
330
|
-
if
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
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
|
|
385
|
+
f"The provider `{type_repr(dependency_type)}` is not registered."
|
|
347
386
|
)
|
|
348
387
|
|
|
349
|
-
provider = self._get_provider(
|
|
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[
|
|
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,
|
|
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
|
-
|
|
415
|
+
dependency_type: Any,
|
|
416
|
+
factory: Callable[..., Any],
|
|
377
417
|
scope: Scope,
|
|
378
|
-
|
|
379
|
-
override: bool
|
|
380
|
-
defaults: dict[str, Any] | 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
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
#
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
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
|
-
|
|
412
|
-
"
|
|
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
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
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
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
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
|
-
|
|
433
|
-
|
|
502
|
+
if is_none_type(dependency_type):
|
|
503
|
+
raise TypeError(f"Missing `{name}` provider return annotation.")
|
|
434
504
|
|
|
435
|
-
if
|
|
436
|
-
raise
|
|
437
|
-
f"
|
|
438
|
-
|
|
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
|
-
|
|
447
|
-
|
|
511
|
+
# Process parameters (lazy - store without resolving dependencies)
|
|
512
|
+
parameters: list[ProviderParameter] = []
|
|
448
513
|
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
if
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
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
|
-
|
|
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
|
-
|
|
486
|
-
|
|
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=
|
|
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
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
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
|
-
|
|
566
|
+
# Resolve dependencies for providers registered after build()
|
|
567
|
+
if self.ready:
|
|
568
|
+
provider = self._ensure_provider_resolved(provider, set())
|
|
551
569
|
|
|
552
|
-
|
|
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,
|
|
569
|
-
"""Get provider by
|
|
572
|
+
def _get_provider(self, dependency_type: Any) -> Provider:
|
|
573
|
+
"""Get provider by dependency type."""
|
|
570
574
|
try:
|
|
571
|
-
return self._providers[
|
|
575
|
+
return self._providers[dependency_type]
|
|
572
576
|
except KeyError:
|
|
573
577
|
raise LookupError(
|
|
574
|
-
f"The provider
|
|
575
|
-
"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 "
|
|
576
580
|
"properly registered before attempting to use it."
|
|
577
581
|
) from None
|
|
578
582
|
|
|
579
583
|
def _get_or_register_provider(
|
|
580
|
-
self,
|
|
584
|
+
self, dependency_type: Any, defaults: dict[str, Any] | None = None
|
|
581
585
|
) -> Provider:
|
|
582
|
-
"""Get or register a provider by
|
|
586
|
+
"""Get or register a provider by dependency type."""
|
|
587
|
+
registered = False
|
|
583
588
|
try:
|
|
584
|
-
|
|
589
|
+
provider = self._get_provider(dependency_type)
|
|
585
590
|
except LookupError:
|
|
586
|
-
if inspect.isclass(
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
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
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
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
|
|
603
|
-
self._providers[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.
|
|
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.
|
|
610
|
-
del self._providers[provider.
|
|
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.
|
|
776
|
+
self._resources[provider.scope].remove(provider.dependency_type)
|
|
613
777
|
|
|
614
778
|
# == Instance Resolution ==
|
|
615
779
|
|
|
616
780
|
@overload
|
|
617
|
-
def resolve(self,
|
|
781
|
+
def resolve(self, dependency_type: type[T], /) -> T: ...
|
|
618
782
|
|
|
619
783
|
@overload
|
|
620
|
-
def resolve(self,
|
|
784
|
+
def resolve(self, dependency_type: T, /) -> T: ... # type: ignore
|
|
621
785
|
|
|
622
|
-
def resolve(self,
|
|
623
|
-
"""Resolve an instance by
|
|
624
|
-
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)
|
|
625
789
|
if cached is not None:
|
|
626
790
|
return cached.resolve(self)
|
|
627
791
|
|
|
628
|
-
provider = self._get_or_register_provider(
|
|
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,
|
|
797
|
+
async def aresolve(self, dependency_type: type[T], /) -> T: ...
|
|
634
798
|
|
|
635
799
|
@overload
|
|
636
|
-
async def aresolve(self,
|
|
800
|
+
async def aresolve(self, dependency_type: T, /) -> T: ...
|
|
637
801
|
|
|
638
|
-
async def aresolve(self,
|
|
639
|
-
"""Resolve an instance by
|
|
640
|
-
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)
|
|
641
805
|
if cached is not None:
|
|
642
806
|
return await cached.resolve(self)
|
|
643
807
|
|
|
644
|
-
provider = self._get_or_register_provider(
|
|
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,
|
|
649
|
-
"""Create an instance by
|
|
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(
|
|
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(
|
|
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,
|
|
660
|
-
"""Create an instance by
|
|
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(
|
|
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(
|
|
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,
|
|
671
|
-
"""Check if an instance
|
|
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(
|
|
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
|
|
843
|
+
return dependency_type in context
|
|
680
844
|
|
|
681
|
-
def release(self,
|
|
682
|
-
"""Release an instance by
|
|
683
|
-
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)
|
|
684
848
|
if provider.scope == "transient":
|
|
685
849
|
return None
|
|
686
850
|
context = self._get_instance_context(provider.scope)
|
|
687
|
-
del context[
|
|
851
|
+
del context[dependency_type]
|
|
688
852
|
|
|
689
853
|
def reset(self) -> None:
|
|
690
854
|
"""Reset resolved instances."""
|
|
691
|
-
for
|
|
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[
|
|
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,
|
|
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(
|
|
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.
|
|
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
|
|
1184
|
+
f"The provider `{type_repr(dependency_type)}` is not registered."
|
|
751
1185
|
)
|
|
752
|
-
self._resolver.add_override(
|
|
1186
|
+
self._resolver.add_override(dependency_type, instance)
|
|
753
1187
|
try:
|
|
754
1188
|
yield
|
|
755
1189
|
finally:
|
|
756
|
-
self._resolver.remove_override(
|
|
1190
|
+
self._resolver.remove_override(dependency_type)
|
|
757
1191
|
|
|
758
1192
|
|
|
759
1193
|
def import_container(container_path: str) -> Container:
|