|
| 1 | +# `@provides` |
| 2 | + |
| 3 | +`@injectable` covers 99% of dependency wiring. Use `@provides` when the concrete |
| 4 | +implementation of an interface must be chosen **at runtime** — typically based |
| 5 | +on a feature flag, environment, or config rule. |
| 6 | + |
| 7 | +## When to reach for `@provides` |
| 8 | + |
| 9 | +- A composite/strategy binding where the selected implementation depends on |
| 10 | + runtime state (feature flag, region, tenant, environment). |
| 11 | +- A third-party SDK client that needs non-trivial construction and can't be |
| 12 | + expressed as a single `@injectable` class. |
| 13 | + |
| 14 | +If you just need to register a class, use `@injectable`. If you need a manual |
| 15 | +binding for a value that never changes (e.g. a constant config object), prefer |
| 16 | +`extra_modules`. |
| 17 | + |
| 18 | +## Basic usage |
| 19 | + |
| 20 | +```python |
| 21 | +from abc import ABC, abstractmethod |
| 22 | +from django_autowired import injectable, provides |
| 23 | + |
| 24 | + |
| 25 | +class IPaymentGateway(ABC): |
| 26 | + @abstractmethod |
| 27 | + def charge(self, amount: int) -> str: ... |
| 28 | + |
| 29 | + |
| 30 | +class StripeV1Gateway(IPaymentGateway): |
| 31 | + def charge(self, amount: int) -> str: |
| 32 | + return f"stripe-v1:{amount}" |
| 33 | + |
| 34 | + |
| 35 | +class StripeV2Gateway(IPaymentGateway): |
| 36 | + def charge(self, amount: int) -> str: |
| 37 | + return f"stripe-v2:{amount}" |
| 38 | + |
| 39 | + |
| 40 | +@provides(bind_to=IPaymentGateway) |
| 41 | +def payment_gateway(flags: FeatureFlags) -> IPaymentGateway: |
| 42 | + if flags.is_enabled("use_stripe_v2"): |
| 43 | + return StripeV2Gateway() |
| 44 | + return StripeV1Gateway() |
| 45 | +``` |
| 46 | + |
| 47 | +`FeatureFlags` is resolved from the container just like any `@injectable` |
| 48 | +dependency — provider functions support full dependency injection. |
| 49 | + |
| 50 | +## Signature |
| 51 | + |
| 52 | +```python |
| 53 | +@provides(bind_to: type, scope: Scope = Scope.SINGLETON) |
| 54 | +``` |
| 55 | + |
| 56 | +- `bind_to` — **required**. The interface/type the factory's return value binds |
| 57 | + to. A provider without `bind_to` is rejected at decoration time. |
| 58 | +- `scope` — Lifecycle scope the container applies to factory invocation. |
| 59 | + |
| 60 | +## Scope semantics |
| 61 | + |
| 62 | +`@provides` is a factory — the framework owns the scope around it: |
| 63 | + |
| 64 | +| Scope | Factory invoked | |
| 65 | +| --- | --- | |
| 66 | +| `SINGLETON` (default) | Once per container lifetime. Result cached. | |
| 67 | +| `TRANSIENT` | On every `container.get()` call. | |
| 68 | +| `THREAD` | Once per thread. Result cached per-thread. | |
| 69 | + |
| 70 | +This matches Spring `@Bean` / Dagger `@Provides` semantics. Your factory does |
| 71 | +not manage caching — the framework does. |
| 72 | + |
| 73 | +## Factory injection |
| 74 | + |
| 75 | +Factory parameters are resolved from the container. All parameters **must be |
| 76 | +type-annotated**: |
| 77 | + |
| 78 | +```python |
| 79 | +@provides(bind_to=IPaymentGateway) |
| 80 | +def payment_gateway( |
| 81 | + flags: FeatureFlags, |
| 82 | + cfg: StripeConfig, |
| 83 | + logger: ILogger, |
| 84 | +) -> IPaymentGateway: |
| 85 | + if flags.is_enabled("use_stripe_v2"): |
| 86 | + return StripeV2Gateway(cfg, logger) |
| 87 | + return StripeV1Gateway(cfg, logger) |
| 88 | +``` |
| 89 | + |
| 90 | +A factory parameter without an annotation raises `TypeError` at build time. |
| 91 | + |
| 92 | +## One binding per target |
| 93 | + |
| 94 | +Whether you use `@injectable` or `@provides`, each interface can be bound |
| 95 | +**exactly once**. Any combination of: |
| 96 | + |
| 97 | +- two `@injectable` classes with the same `bind_to` |
| 98 | +- two `@provides` functions with the same `bind_to` |
| 99 | +- one `@injectable` + one `@provides` with the same `bind_to` |
| 100 | + |
| 101 | +…raises `DuplicateBindingError`. This is a feature: predictable, deterministic |
| 102 | +resolution is a core guarantee of `django-autowired`. |
| 103 | + |
| 104 | +If you need multiple candidates selected at runtime, express the conditional |
| 105 | +logic **inside a single** `@provides` function that returns the correct |
| 106 | +instance. |
| 107 | + |
| 108 | +## Eager evaluation |
| 109 | + |
| 110 | +Every `@provides` factory runs once during `container.initialize()` to catch |
| 111 | +errors at boot: |
| 112 | + |
| 113 | +- Factory raises → `ProviderResolutionError` wraps the exception. |
| 114 | +- Factory has unresolvable dependencies → `ProviderResolutionError`. |
| 115 | + |
| 116 | +For `SINGLETON` scope this eager invocation produces the cached instance. For |
| 117 | +`TRANSIENT` and `THREAD` scope the instance is discarded — the eager run is |
| 118 | +pure verification. This matches the library's "fail loudly at boot" principle. |
| 119 | + |
| 120 | +## What `@provides` cannot do |
| 121 | + |
| 122 | +- **Cannot decorate a class.** `@provides` only decorates functions. |
| 123 | +- **Cannot be called without `bind_to`.** Raises `TypeError` immediately. |
| 124 | +- **Cannot chain/fallback.** Multiple providers for the same target are an |
| 125 | + error, not an ordered list. |
| 126 | + |
| 127 | +## FAQ |
| 128 | + |
| 129 | +**Q: Should my factory return a class or an instance?** |
| 130 | +An instance. `@provides` is a factory. The framework handles scope/caching. |
| 131 | + |
| 132 | +**Q: What if the factory needs to construct something complex with many deps?** |
| 133 | +Declare those deps as factory parameters — they are resolved from the container |
| 134 | +just like `@injectable` constructor parameters. |
| 135 | + |
| 136 | +**Q: Can a `@provides` factory depend on another `@provides` factory?** |
| 137 | +Yes. Dependency resolution across factories is handled by the backend. |
0 commit comments