Metadata-Version: 2.4
Name: providify
Version: 1.0.0a0
Summary: A Python dependency injection container inspired by Jakarta CDI and Spring. Supports sync and async resolution, multiple scopes, lifecycle hooks, and configuration modules.
License: Apache-2.0
License-File: LICENSE
Keywords: dependency injection,di,ioc,inversion of control,container,async,jakarta,spring
Author: edoardo.scarpaci
Author-email: edoardo.scarpaci@gmail.com
Requires-Python: >=3.12
Classifier: Development Status :: 3 - Alpha
Classifier: Intended Audience :: Developers
Classifier: License :: OSI Approved :: Apache Software License
Classifier: Operating System :: OS Independent
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3.12
Classifier: Programming Language :: Python :: 3.13
Classifier: Programming Language :: Python :: 3.14
Classifier: Topic :: Software Development :: Libraries :: Application Frameworks
Classifier: Topic :: Software Development :: Libraries :: Python Modules
Classifier: Typing :: Typed
Project-URL: Repository, https://github.com/edoardoscarpaci/providify
Description-Content-Type: text/markdown

# providify

A Python dependency injection container inspired by Jakarta CDI and Spring.
Supports sync and async resolution, multiple scopes, lifecycle hooks, and configuration modules.

---

## Installation

```bash
poetry install
```

Requires Python 3.12+.

---

## Quick start

```python
from providify import DIContainer, Component, Singleton

class Notifier:
    def send(self, msg: str) -> None: ...

@Component
class EmailNotifier(Notifier):
    def send(self, msg: str) -> None:
        print(f"email: {msg}")

@Singleton
class AlertService:
    def __init__(self, notifier: Notifier) -> None:
        self._notifier = notifier   # injected automatically

    def alert(self, msg: str) -> None:
        self._notifier.send(msg)

container = DIContainer()
container.bind(Notifier, EmailNotifier)
container.register(AlertService)

svc = container.get(AlertService)
svc.alert("hello")   # -> email: hello
```

---

## Core concepts

The container operates in two phases:

1. **Registration** — declare bindings via `bind()`, `register()`, `provide()`, `scan()`, or `install()`
2. **Resolution** — the first `get()` / `aget()` call validates all bindings, then resolves them

Dependencies can be declared in two places:

- **Constructor parameters** — `def __init__(self, svc: Service) -> None` — resolved automatically
- **Class-level annotations** — `svc: Inject[Service]` on the class body — resolved after the constructor runs

Both forms support `Inject[T]`, `Live[T]`, and `Lazy[T]`.

---

## Scope decorators

Mark a class so the container knows how to manage its lifetime.

```python
from providify import Component, Singleton, RequestScoped, SessionScoped

@Component        # new instance on every resolution (default)
class EmailSender: ...

@Singleton        # one instance for the lifetime of the container
class Database: ...

@RequestScoped    # one instance per active request context
class RequestLogger: ...

@SessionScoped    # one instance per active session context
class UserSession: ...
```

All scope decorators accept optional keyword arguments:

```python
@Singleton(qualifier="primary", priority=1, inherited=True)
class PrimaryDB(Database): ...
```

| Argument | Type | Meaning |
|----------|------|---------|
| `qualifier` | `str \| type \| None` | Named or typed qualifier — used to distinguish multiple bindings of the same type |
| `priority` | `int` | Higher number wins when multiple candidates match (default `0`) |
| `inherited` | `bool` | Subclasses inherit this metadata via MRO walk (default `False`) |
| `track` | `bool` | Track DEPENDENT instances for manual teardown via `flush_dependents()` (default `False`) |

`ApplicationScoped` is an alias for `@Singleton` — provided for Jakarta CDI parity:

```python
from providify import ApplicationScoped

@ApplicationScoped
class UserCache: ...   # exactly equivalent to @Singleton
```

---

## Typed qualifiers — @Qualifier and @Default

### @Qualifier

Define a **type** as a qualifier marker. The type itself (not a string) is used to filter bindings, giving you IDE completion, refactoring support, and no risk of typos.

```python
from providify import Component, Qualifier

@Qualifier
class Cloud: ...

@Qualifier
class OnPrem: ...

@Component(qualifier=Cloud)
class AwsNotifier(Notifier): ...

@Component(qualifier=OnPrem)
class SmtpNotifier(Notifier): ...

# Resolve with the type, not a string
cloud_svc = container.get(Notifier, qualifier=Cloud)
onprem_svc = container.get(Notifier, qualifier=OnPrem)
```

Any class decorated with `@Qualifier` can be used in `qualifier=` on any scope decorator, `@Provider`, and `container.get()` / `container.aget()`.

### @Default

Mark a binding as the explicit default. `@Default`-qualified beans are returned on **unqualified** lookups — the same as no qualifier at all. This mirrors Jakarta CDI semantics and is useful for documentation clarity:

```python
from providify import Component, Default

@Component(qualifier=Default)
class EmailSender(Sender): ...   # returned by container.get(Sender) with no qualifier

@Component(qualifier=Cloud)
class AwsSender(Sender): ...     # only returned by container.get(Sender, qualifier=Cloud)
```

> **Semantics note**: In providify `qualifier=None` in `_filter()` means "no filter — return all qualifiers". `@Default` is normalised to `qualifier=None` before the filter runs, so `@Default`-marked beans are visible in unqualified lookups just like undecorated ones.

---

## @Alternative — deployment-time bean replacement

`@Alternative` marks a bean that is **excluded from resolution by default**. Activate it explicitly per-container to replace the regular implementation — useful for mocks, staging overrides, and feature flags.

```python
from providify import Component, Alternative

@Component
class RealPaymentGateway(PaymentGateway): ...

@Alternative
@Component(priority=10)     # higher priority ensures it wins when enabled
class MockPaymentGateway(PaymentGateway): ...
```

```python
# Production container — MockPaymentGateway is invisible
container.bind(PaymentGateway, RealPaymentGateway)

# Test container — activate the mock
container.bind(PaymentGateway, RealPaymentGateway)
container.register(MockPaymentGateway)
container.enable_alternative(MockPaymentGateway)

gw = container.get(PaymentGateway)   # MockPaymentGateway ✅

# Revert
container.disable_alternative(MockPaymentGateway)
gw = container.get(PaymentGateway)   # RealPaymentGateway ✅
```

> **Priority rule**: an `@Alternative` bean must have a **higher `priority`** than the non-alternative beans of the same type to win selection. At equal priority (both `0`) the first-registered binding wins. Set `priority > 0` on alternatives to guarantee they override.

---

## @Stereotype — reusable composed annotations

`@Stereotype` bundles scope, qualifier, priority, and inherited into a single reusable decorator — the Python equivalent of Jakarta CDI `@Stereotype`.

```python
from providify import Stereotype, Singleton, Component
from providify.metadata import Scope

# Define the stereotype — reusable across the codebase
DomainRepository = Stereotype(scope=Scope.SINGLETON, priority=1)
ApplicationService = Stereotype(scope=Scope.SINGLETON, qualifier="app")

# Apply it — identical to @Singleton(priority=1)
@DomainRepository
class UserRepository:
    def find(self, id: int) -> User: ...

@DomainRepository
class OrderRepository:
    def find(self, id: int) -> Order: ...

# Explicit annotations always win over the stereotype defaults
@Component(qualifier="test")    # scope and priority taken from stereotype; qualifier overridden
@DomainRepository
class TestRepository: ...
```

---

## @Provider

Register a factory function instead of a class. The return type determines the resolved interface.

```python
from providify import Provider

@Provider
def make_sender() -> EmailSender:
    return EmailSender(host="smtp.example.com")

# singleton=True caches the result — provider called only once
@Provider(singleton=True)
def make_db() -> Database:
    return Database(url=os.environ["DB_URL"])

# async providers are supported — resolve with aget()
@Provider(singleton=True)
async def make_pool() -> ConnectionPool:
    pool = ConnectionPool()
    await pool.connect()
    return pool
```

Providers also accept `qualifier=` and `priority=`.

---

## Container API

```python
from providify import DIContainer

container = DIContainer()

# ── Registration ──────────────────────────────────────────────────
container.bind(Interface, ConcreteClass)   # bind interface -> implementation
container.register(ConcreteClass)          # self-bind: interface == implementation
container.provide(factory_fn)              # register a @Provider function
container.scan("myapp.services")           # auto-discover decorated classes in a module
container.install(MyModule)                # install a @Configuration module (see below)

# ── Mutation ──────────────────────────────────────────────────────
container.override(Interface, MockImpl)    # replace all bindings for Interface in-place (for tests)
container.reset_binding(Interface)         # remove all bindings for Interface; returns count removed
container.enable_alternative(MockImpl)     # activate an @Alternative bean for this container
container.disable_alternative(MockImpl)    # deactivate an @Alternative bean
container.add_interceptor(LoggingInterceptor)  # register an @Interceptor class

# ── Sync resolution ───────────────────────────────────────────────
svc  = container.get(Service)
svc  = container.get(Service, qualifier="primary")    # string qualifier
svc  = container.get(Service, qualifier=Cloud)        # type qualifier (@Qualifier class)
svc  = container.get(Service, priority=1)
svcs = container.get_all(Service)          # all matching bindings, sorted by priority ascending

# ── Async resolution ──────────────────────────────────────────────
svc  = await container.aget(Service)
svc  = await container.aget(Service, qualifier="primary")
svcs = await container.aget_all(Service)

# ── Introspection (no instantiation) ──────────────────────────────
binding  = container.get_binding(Service)             # best-match AnyBinding (raises LookupError if absent)
bindings = container.get_all_bindings(Service)        # all AnyBinding objects; [] if none registered
ok       = container.is_resolvable(Service)           # True if at least one binding matches

# ── Global singleton ──────────────────────────────────────────────
container = DIContainer.current()          # sync — thread-safe
container = await DIContainer.acurrent()   # async — never blocks the event loop
DIContainer.reset()                        # wipe global (useful in tests)

# ── Scoped global — swap in a fresh container for one block ───────
with DIContainer.scoped() as c:
    c.bind(...)
    c.get(Service)
# original global is restored on exit, even if an exception is raised

async with DIContainer.scoped() as c:
    await c.aget(Service)

# ── Scope utilities ───────────────────────────────────────────────
container.run_in_request(fn, *args, **kwargs)           # run fn inside a new request scope
await container.arun_in_request(fn, *args, **kwargs)    # async version
container.run_in_session("user-abc", fn, *args)         # run fn inside a named session scope
await container.arun_in_session("user-abc", fn, *args)  # async version

# ── DEPENDENT tracking ────────────────────────────────────────────
container.flush_dependents()        # call @PreDestroy on all track=True DEPENDENT instances
await container.aflush_dependents() # async version (context manager calls this automatically)

# ── Instance lifecycle ────────────────────────────────────────────
with container:                   # calls flush_dependents() + shutdown() on __exit__
    ...

async with container:             # calls aflush_dependents() + ashutdown() on __aexit__
    ...
```

---

## Injection types

### Plain type annotation

The simplest case — annotate the parameter with the type to inject.
Pylance / mypy see the real type directly; no special import needed.

```python
@Component
class OrderService:
    def __init__(self, db: Database) -> None:
        self.db = db
```

### Optional[T] and T | None — nullable injection

Use Python's standard `Optional[T]` or the pipe-union syntax `T | None` when
a dependency may not be registered. If no binding is found, the parameter
receives `None` instead of raising `LookupError`.

```python
from typing import Optional

@Component
class Notifier:
    def __init__(
        self,
        sms:   Optional[SmsService],    # None if SmsService is not registered
        push:  PushService | None,      # equivalent pipe-syntax form
    ) -> None:
        self._sms  = sms   # may be None at runtime
        self._push = push
```

No import from `providify` is needed — plain `Optional[T]` / `T | None` is
enough. The container detects the union at resolution time and handles the
missing-binding case automatically.

> **`Optional[T]` vs `Annotated[T, InjectMeta(optional=True)]`**: both inject
> `None` when the binding is absent, but the `Optional[T]` form is more
> idiomatic Python and works without importing `InjectMeta`.

### Union[T1, T2] — first-match injection

A `Union` with multiple non-`None` types is resolved by trying each candidate
in declaration order. The first type that has a registered binding is used;
`LookupError` is raised only if **no** candidate resolves.

```python
from typing import Union

@Component
class StorageService:
    def __init__(
        self,
        # Prefers S3Storage if registered; falls back to LocalStorage otherwise
        backend: Union[S3Storage, LocalStorage],
    ) -> None:
        self.backend = backend
```

Combining with `None` makes the whole injection optional:

```python
@Component
class AnalyticsCollector:
    def __init__(
        self,
        # Uses SegmentAnalytics if available, Mixpanel as fallback, skipped if neither
        tracker: Union[SegmentAnalytics, MixpanelAnalytics, None] = None,
    ) -> None:
        self.tracker = tracker
```

**Resolution rules:**

| Annotation | First candidate bound? | Second candidate bound? | Result |
|---|---|---|---|
| `Optional[T]` / `T \| None` | yes | — | T instance |
| `Optional[T]` / `T \| None` | no | — | `None` |
| `Union[T1, T2]` | yes | — | T1 instance |
| `Union[T1, T2]` | no | yes | T2 instance |
| `Union[T1, T2]` | no | no | raises `LookupError` |
| `Union[T1, T2, None]` | no | no | `None` |

### Inject[T] — subscript form (recommended)

Use `Inject[T]` when you want to be explicit that this parameter is managed
by the DI container. Linters and type checkers resolve `Inject[Database]`
directly to `Database`, so hover, completion, and type errors work normally.

```python
from providify import Inject

@Component
class OrderService:
    def __init__(self, db: Inject[Database]) -> None:
        self.db = db   # linter sees: db: Database ✅
```

### Annotated[T, InjectMeta(...)] — for qualifier / priority / optional (recommended)

When you need injection **options** (qualifier, priority, optional), use
`Annotated` with `InjectMeta` directly. This is the underlying form that
`Inject[T]` expands to at runtime, and it is fully valid Python — no
`# type: ignore` comment needed. Pylance hover shows the bare type `T`.

```python
from typing import Annotated
from providify import Inject, InjectMeta

@Component
class ReportService:
    def __init__(
        self,
        db:      Inject[Database],                                    # simple — no options needed
        cache:   Annotated[Cache,   InjectMeta(qualifier="redis")],   # named qualifier ✅
        metrics: Annotated[Metrics, InjectMeta(optional=True)],       # None if not bound ✅
        audit:   Annotated[AuditLog, InjectMeta(priority=1)],         # exact priority ✅
    ) -> None: ...
```

> **Why not `Inject(T, qualifier=...)`?**
> The call form `Inject(Cache, qualifier="redis")` works at runtime but is **not
> recommended** — type checkers (Pylance, mypy, pyright) flag it as invalid in
> annotation position and cannot infer the return type, so hover and completion
> show `Unknown` instead of `Cache`. Use `Annotated[T, InjectMeta(...)]` instead.
> It resolves identically and keeps the full type-checker experience intact.

### InjectInstances[T] — all bindings as a list

Inject every registered implementation of an interface, sorted by priority.
Pylance resolves `InjectInstances[Sender]` to `list[Sender]`.

```python
from providify import InjectInstances

@Component
class NotificationFanout:
    def __init__(self, senders: InjectInstances[Sender]) -> None:
        self.senders = senders   # linter sees: senders: list[Sender] ✅

    def notify(self, msg: str) -> None:
        for sender in self.senders:
            sender.send(msg)
```

For qualifier filtering on `InjectInstances`, use `Annotated` with `InjectMeta(all=True)`:

```python
from typing import Annotated
from providify import InjectMeta

@Component
class CloudFanout:
    def __init__(
        self,
        senders: Annotated[list[Sender], InjectMeta(all=True, qualifier="cloud")],
    ) -> None:
        self.senders = senders
```

### Class-level attributes

Injection annotations can be placed directly on class-level attributes instead of (or alongside) constructor parameters. They are resolved and set on the instance **after** the constructor runs, and **before** `@PostConstruct` fires — so lifecycle hooks can access them.

```python
from providify import Inject, Live, Lazy

@Singleton
class ReportService:
    # Class-level — resolved after __init__ returns
    storage: Inject[StorageBackend]
    logger:  Live[RequestLogger]    # re-resolves per request (see Live[T] below)

    # Constructor parameters still work normally alongside class-level annotations
    def __init__(self, db: Database) -> None:
        self.db = db
```

All injection forms (`Inject[T]`, `Live[T]`, `Lazy[T]`, `Instance[T]`) work as class-level annotations.
For options (`qualifier=`, `priority=`, `optional=`), use `Annotated` + the corresponding meta type:

```python
from typing import Annotated
from providify import InjectMeta, LiveMeta, LazyMeta

@Singleton
class ReportService:
    storage:  Annotated[StorageBackend, InjectMeta(qualifier="primary")]
    logger:   Annotated[RequestLogger,  LiveMeta(qualifier="request")]
    slow_svc: Annotated[HeavyService,   LazyMeta(qualifier="heavy")]
```

#### `ClassVar[...]` form

All four injection types also accept the `ClassVar[...]` wrapper, which is useful when
a type checker or style guide requires class-level attributes to be explicitly typed as
class variables:

```python
from typing import ClassVar
from providify import Instance, Live, Lazy, Inject

@Singleton
class AlertService:
    # ClassVar form — treated identically to the plain form by the container
    emailer:  ClassVar[Instance[Emailer]]
    logger:   ClassVar[Live[RequestLogger]]
    config:   ClassVar[Lazy[AppConfig]]
    storage:  ClassVar[Inject[StorageBackend]]
```

The container unwraps `ClassVar[X]` to `X` before dispatching, so resolution,
scope-violation detection, and dependency-graph construction all work identically
to the plain annotation form.

> **Constructor takes priority** — if the same name appears both as a class-level annotation and as an `__init__` parameter, the constructor value is used and the class-level annotation is skipped.

### Inheritance and MRO

The two injection paths intentionally have **different MRO behaviour**:

| Injection path | MRO walk? | Why |
|---|---|---|
| `__init__` parameters | ❌ No | The declared signature is an explicit contract. If a child overrides `__init__`, it is asserting its own construction semantics. |
| Class-level annotations | ✅ Yes | `get_type_hints(cls)` walks the full MRO — annotations declared on a parent class are inherited and injected automatically. |

**When `__init__` is *not* overridden** the parent's `__init__` is already picked up via Python's own MRO — no special handling is needed. The asymmetry only matters when the child *does* override `__init__`.

**Recommended patterns for inheritance:**

```python
# Option A — re-declare the parent dep in the child signature (explicit, zero magic)
class Base:
    def __init__(self, svc_a: Inject[ServiceA]) -> None:
        self.svc_a = svc_a

class Child(Base):
    def __init__(self, svc_a: Inject[ServiceA], svc_b: Inject[ServiceB]) -> None:
        super().__init__(svc_a)   # explicit hand-off — no surprise injections
        self.svc_b = svc_b

# Option B — use class-level annotations for inherited deps (MRO is walked)
class Base:
    svc_a: Inject[ServiceA]   # injected after construction; inherited by all subclasses

class Child(Base):
    def __init__(self, svc_b: Inject[ServiceB]) -> None:
        self.svc_b = svc_b
    # svc_a is still set on self via the class-var injection path ✓
```

> ⚠️ **Avoid injecting parent `__init__` params via MRO manually.** If the container were to merge parent and child `__init__` signatures automatically, it would conflict with any `super().__init__(arg)` call inside the child — the same instance could be resolved twice, producing two separate objects for what should be a single dep.

### Lazy[T] — deferred injection

Wraps the dependency in a `LazyProxy`. The real instance is **not resolved until `.get()` (or `.aget()`) is called for the first time**, after which the result is cached.

The primary use case is **breaking circular dependencies** — `A` can hold `Lazy[B]` while `B` holds `A` directly:

```python
from providify import Lazy

@Singleton
class ReportService:
    def __init__(self, repo: Lazy[ReportRepository]) -> None:
        self._repo = repo   # proxy — ReportRepository not resolved yet

    def run(self) -> Report:
        return self._repo.get().fetch_all()   # resolved here on first call

# Async resolution
async def run_async(self) -> Report:
    repo = await self._repo.aget()
    return await repo.fetch_all_async()
```

`Lazy` also accepts `qualifier=`, `priority=`, and `optional=` via `Annotated` + `LazyMeta`:

```python
from typing import Annotated
from providify import LazyMeta

repo: Annotated[Cache, LazyMeta(qualifier="redis", priority=1)]

# optional=True — proxy.get() returns None if T is not bound (instead of raising)
svc: Annotated[OptionalService, LazyMeta(optional=True)]
```

#### Optional lazy injection — `Lazy[T | None]`

Use the pipe-union form as shorthand for `optional=True`:

```python
from providify import Lazy

# Both are equivalent — proxy.get() returns None when T is not bound
svc: Lazy[OptionalService | None]                                   # pipe-union form
svc: Annotated[OptionalService, LazyMeta(optional=True)]            # explicit form
```

> ⚠️ **`Lazy[T]` is not scope-safe for `@RequestScoped` or `@SessionScoped` deps.**
> After the first `.get()` call the proxy caches the resolved instance — subsequent calls return the same (stale) object regardless of which request is active.
> Use **`Live[T]`** instead when a longer-lived component needs a scoped dep.

### Live[T] — always-fresh injection

Returns a `LiveProxy` that calls `container.get(T)` on **every `.get()` or `.aget()` invocation** — it never caches. The correct choice when a longer-lived component (`@Singleton`, `@SessionScoped`) holds a `@RequestScoped` or `@SessionScoped` dependency.

```python
from providify import Live

@Singleton
class AuthService:
    def __init__(self, token: Live[JsonWebToken]) -> None:
        self._token = token   # LiveProxy — not the token itself

    def get_user_id(self) -> str:
        # Re-resolves from the active request scope on every call
        return self._token.get().subject

    async def get_user_id_async(self) -> str:
        token = await self._token.aget()
        return token.subject
```

Works as a class-level annotation too:

```python
@Singleton
class AuthService:
    token: Live[JsonWebToken]   # set after construction, re-resolves per request
```

`Live` also accepts `qualifier=`, `priority=`, and `optional=` via `Annotated` + `LiveMeta`:

```python
from typing import Annotated
from providify import LiveMeta

token: Annotated[JsonWebToken, LiveMeta(qualifier="bearer")]

# optional=True — proxy.get() returns None if T is not bound
ctx: Annotated[OptionalContext, LiveMeta(optional=True)]
```

#### Optional live injection — `Live[T | None]`

Use the pipe-union form as shorthand for `optional=True`. Every `.get()` / `.aget()`
call returns `None` when T is not bound, instead of raising `LookupError`:

```python
from providify import Live

# Both are equivalent
ctx: Live[OptionalContext | None]                                    # pipe-union form
ctx: Annotated[OptionalContext, LiveMeta(optional=True)]             # explicit form
```

**`Lazy[T]` vs `Live[T]` at a glance:**

| | `Lazy[T]` | `Live[T]` |
|---|---|---|
| First `.get()` | Resolves and **caches** | Resolves (no cache) |
| Subsequent `.get()` | Returns **cached** instance | Re-resolves every time |
| Circular deps | ✅ Breaks A→B→A cycles | ❌ Does not help |
| Scoped deps in singletons | ❌ Stale after first access | ✅ Always fresh |

---

## Scope contexts

`@RequestScoped` and `@SessionScoped` bindings require an active scope context.

```python
# Sync request scope
with container.request():
    svc = container.get(RequestLogger)   # same instance within this block

# Async request scope
async with container.arequest():
    svc = await container.aget(RequestLogger)

# Session scope — provide a stable ID to share state across multiple requests
with container.session("user-abc"):
    profile = container.get(UserProfile)

# Resume the same session later
with container.session("user-abc"):
    profile = container.get(UserProfile)   # same cached instance

# Destroy a session on logout
container.invalidate_session("user-abc")

# scope_context property — still available for advanced use or direct cache access
with container.scope_context.request():   # equivalent to container.request()
    ...
```

> Resolving a `@RequestScoped` or `@SessionScoped` binding outside an active context
> raises `RuntimeError` immediately.

---

## Scope safety

The container detects scope leaks at `validate_bindings()` time (triggered by the first `get()` call) and raises before any instance is created.

A **scope leak** occurs when a longer-lived component holds a direct reference to a shorter-lived one, causing it to silently serve a stale instance across scope boundaries.

### LiveInjectionRequiredError

Raised when a `@Singleton` (or `@SessionScoped`) injects a `@RequestScoped` or `@SessionScoped` dep via `Inject[T]`, `Lazy[T]`, or a bare type annotation — all of which capture one instance at construction time:

```python
@Singleton
class Bad:
    def __init__(self, ctx: RequestContext) -> None:  # ❌ captured once, stale forever
        self.ctx = ctx
```

Fix: wrap with `Live[T]` so the dep is re-resolved on every access:

```python
@Singleton
class Good:
    def __init__(self, ctx: Live[RequestContext]) -> None:  # ✅ re-resolves per request
        self._ctx = ctx
```

Scope safety is checked for **both** constructor parameters and class-level annotations:

```python
@Singleton
class AlsoDetected:
    ctx: Inject[RequestContext]   # ❌ also caught — same rule applies to class-level attrs
```

### ScopeViolationDetectedError

Raised for other scope leaks — e.g. a `@Singleton` holding a `@Component` (DEPENDENT) dep directly. This is less critical but still signals a design issue: the singleton pins one `@Component` instance for its entire lifetime instead of getting a fresh one.

---

## Lifecycle hooks

### @PostConstruct

Called by the container immediately after the instance is constructed and all
dependencies are injected. Both sync and async forms are supported.

```python
from providify import PostConstruct

@Singleton
class SearchIndex:
    @PostConstruct
    def build(self) -> None:
        self._load_from_disk()

    # Async — must resolve with aget()
    @PostConstruct
    async def async_build(self) -> None:
        await self._fetch_from_s3()
```

### @PreDestroy

Called when a cached instance is about to be discarded. Fires in two situations:

- **`shutdown()` / `ashutdown()`** — for every cached `@Singleton` instance.
- **Scope exit** — when a `request()` / `session()` block exits, `@PreDestroy` is called on every `@RequestScoped` / `@SessionScoped` instance cached in that scope.

`DEPENDENT` instances are not owned by the container and are never destroyed automatically.

```python
from providify import PreDestroy

@Singleton
class ConnectionPool:
    @PreDestroy
    def close(self) -> None:
        self._pool.close()

    # Async — use ashutdown() / arequest() / asession() to invoke
    @PreDestroy
    async def async_close(self) -> None:
        await self._pool.aclose()

@RequestScoped
class RequestLogger:
    @PreDestroy
    def flush(self) -> None:
        self._buffer.flush()   # called automatically when request() block exits
```

> **Sync scope exit + async `@PreDestroy`**: if an async `@PreDestroy` hook is registered
> on a scoped instance and the scope exits via the sync `request()` / `session()` context
> manager, the async hook is **skipped** (cannot be awaited in a sync context). Use
> `arequest()` / `asession()` when your scoped components have async teardown.

### Shutdown

```python
container.shutdown()         # calls @PreDestroy on all cached singletons, clears caches
await container.ashutdown()  # async — awaits async @PreDestroy hooks
```

Calling `shutdown()` when any cached singleton has an `async @PreDestroy` raises `RuntimeError` —
use `ashutdown()` in that case.

### @Disposes — provider teardown

Define a teardown method for a `@Provider`-produced object inside a `@Configuration` module.
`@Disposes` is the CDI equivalent of combining `@PreDestroy` with a factory-produced bean.

```python
from providify import Disposes, Provider, Configuration
from providify.metadata import Scope

class Connection:
    def close(self) -> None: ...

@Configuration
class InfraModule:
    @Provider(scope=Scope.SINGLETON)
    def make_conn(self) -> Connection:
        return Connection()

    @Disposes(Connection)
    def close_conn(self, conn: Connection) -> None:
        conn.close()   # called by shutdown() — receives the cached instance

container.install(InfraModule)
conn = container.get(Connection)
container.shutdown()   # close_conn(conn) is called here ✅
```

`@Disposes` is only triggered for **SINGLETON-scoped providers** that have a cached instance. DEPENDENT providers are not tracked and their disposers are never called.

### DEPENDENT scope tracking — `track=True`

DEPENDENT beans (`@Component`) are not owned by the container and normally receive no teardown. Opt in with `track=True` to have the container collect them and call their `@PreDestroy` hooks on demand:

```python
from providify import Component, PreDestroy

@Component(track=True)
class TempFile:
    def __init__(self, path: str) -> None:
        self.path = path

    @PreDestroy
    def cleanup(self) -> None:
        os.remove(self.path)

with container:
    container.register(TempFile)
    f = container.get(TempFile)
    # __exit__ calls flush_dependents() automatically — cleanup() fires here ✅
```

Call manually when not using the context manager:

```python
container.flush_dependents()        # sync
await container.aflush_dependents() # async
```

---

## @Configuration modules

Group related `@Provider` methods in a single class.
**Spring-style**: the module's own `__init__` parameters are injected by the container at `install()` time,
so providers can share config or other injected collaborators via `self`.

```python
from providify import Configuration
from providify.decorator.scope import Provider, Singleton

@Singleton
class AppConfig:
    db_url  = "postgresql://localhost/mydb"
    pool_size = 10

@Configuration
class DatabaseModule:
    def __init__(self, config: AppConfig) -> None:
        self._config = config   # injected at install() time

    @Provider(singleton=True)
    def connection_pool(self) -> ConnectionPool:
        return ConnectionPool(self._config.db_url, size=self._config.pool_size)

    @Provider
    def user_repo(self) -> UserRepository:
        return UserRepository(self._connection_pool())

container.register(AppConfig)
container.install(DatabaseModule)         # sync
await container.ainstall(DatabaseModule)  # async — use when module deps need aget()
```

All `@Provider` options (`qualifier=`, `priority=`, `singleton=`) work normally inside modules.

### Field-level `@Provider` — `@property` pattern

Combine `@property` with `@Provider` to declare a provider as a property on the module class. The return type annotation determines the registered interface — identical to a regular `@Provider` method but allows `self.config` syntax for accessing the produced value within the module:

```python
from providify import Provider, Configuration
from providify.metadata import Scope as _Scope

@Configuration
class AppModule:
    @property
    @Provider
    def config(self) -> AppConfig:
        return AppConfig(dsn=os.environ["DATABASE_URL"])

    @property
    @Provider(scope=_Scope.SINGLETON)
    def cache(self) -> Cache:
        return Cache(url=os.environ["REDIS_URL"])
```

Both `@Provider` and `@Provider(scope=...)` work on properties. The inner `@Provider` must be the innermost decorator (closest to the function definition) and `@property` wraps it on the outside.

---

## Autodiscovery — `scan()`

`scan()` inspects a module (or an entire package tree) and automatically registers every
class and function that carries a scope decorator or `@Provider` — no manual `bind()` /
`register()` / `provide()` call needed.

```python
container = DIContainer()

# Scan a single module by dotted name
container.scan("myapp.services")

# Scan a whole package and every sub-package inside it
container.scan("myapp", recursive=True)

# Pass an already-imported module object instead of a string
import myapp.repositories
container.scan(myapp.repositories)
```

### Auto-scan at construction time

Pass `scan=` to the `DIContainer` constructor to scan one or more modules
**immediately when the container is created** — before any `bind()` / `get()` call.
This keeps the bootstrap code declarative and ensures every decorated class is
registered before any other code interacts with the container.

```python
from providify import DIContainer

# Single package — all sub-packages walked recursively (default)
container = DIContainer(scan="myapp")

# Explicit list — both packages scanned, left-to-right
container = DIContainer(scan=["myapp.services", "myapp.repositories"])

# Opt out of recursive walking
container = DIContainer(scan="myapp.services", recursive=False)
```

| Parameter | Type | Default | Meaning |
|-----------|------|---------|---------|
| `scan` | `str \| list[str] \| None` | `None` | Module(s) to scan at construction. `None` = no auto-scan (backward-compatible). |
| `recursive` | `bool` | `True` | Walk sub-packages recursively. Forwarded to each `scan()` call. |

> **`ModuleNotFoundError`** is raised at construction time if a module name
> cannot be imported — errors surface at the point of misconfiguration rather
> than at the first `get()` call.

The constructor `scan=` is purely additive — you can still call `container.scan()`
manually afterward to register additional modules.

### What gets discovered

| Decorator on the member | What the scanner registers |
|-------------------------|---------------------------|
| `@Component` / `@Singleton` / `@RequestScoped` / `@SessionScoped` | The class, bound to every abstract base class it implements; self-bound if it has none |
| `@Provider` function | The function, equivalent to calling `container.provide(fn)` |
| `@Configuration` class | **Not** picked up by `scan()` — use `container.install()` instead |

### Abstract base class auto-binding

When a scanned class implements one or more abstract base classes (ABCs), the scanner
automatically binds each ABC to the concrete class. You can then resolve by the interface
without writing any `bind()` call yourself.

```python
from abc import ABC, abstractmethod
from providify import Component

class IRepository(ABC):
    @abstractmethod
    def find_all(self) -> list: ...

@Component
class SqlRepository(IRepository):
    def find_all(self) -> list:
        return []

container.scan("myapp.repositories")
# Equivalent to: container.bind(IRepository, SqlRepository)

repo = container.get(IRepository)   # SqlRepository is resolved
```

### What the scanner skips

- **Private members** — anything whose name starts with `_`
- **Re-exports** — symbols imported *into* the scanned module from somewhere else;
  only members *defined* in that module are registered (prevents duplicate bindings)
- **Plain classes** — classes without a scope decorator are silently ignored

### Idempotency

Calling `scan()` multiple times on the same module is safe — the scanner checks for
existing bindings before registering and skips any class or provider that is already
registered.

```python
container.scan("myapp.services")
container.scan("myapp.services")   # no-op — bindings already present
```

### Recursive scanning

Pass `recursive=True` to discover every sub-package automatically. Sub-modules that
fail to import are logged as warnings and skipped rather than halting the entire scan.

```python
# Registers decorated members from myapp, myapp.services,
# myapp.repositories, myapp.utils, and so on
container.scan("myapp", recursive=True)
```

---

## Named qualifiers and priority

### @Named and @Priority decorators

Qualifiers and priorities can be applied inline via the scope decorator or as
separate `@Named` / `@Priority` modifiers on top of any scope decorator.

```python
from providify import Named, Priority

# Inline form — shorter, good for simple cases
@Singleton(qualifier="primary", priority=1)
class PrimaryDB(Database): ...

# Modifier form — useful when the qualifier or priority is a separate concern
@Singleton
@Named(name="replica")
@Priority(priority=2)
class ReplicaDB(Database): ...
```

`@Named` requires keyword argument `name=`. Both `@Named` (bare, no parens) and `@Named("smtp")` (positional string) raise `TypeError` with a message pointing to the correct form: `@Named(name="smtp")`.

Both modifiers work on `@Provider` functions too:

```python
@Provider(singleton=True)
@Named(name="readonly")
@Priority(priority=5)
def make_replica() -> Database:
    return ReplicaDB(url=os.environ["REPLICA_URL"])
```

### Resolving by qualifier and priority

```python
@Singleton(qualifier="primary")
class PrimaryDB(Database): ...

@Singleton(qualifier="replica", priority=1)
class ReplicaDB(Database): ...

# Resolve by name
db = container.get(Database, qualifier="primary")

# Resolve all, sorted by priority ascending (lowest value first, highest-priority binding last)
all_dbs = container.get_all(Database)
```

---

## Generic types

The container resolves parameterised generic types — bind and get `Repository[User]`
as a distinct interface from `Repository[Post]`.

```python
from typing import Generic, TypeVar
from abc import ABC, abstractmethod
from providify import Component

T = TypeVar("T")

class Repository(ABC, Generic[T]):
    @abstractmethod
    def find(self, id: int) -> T: ...

@Component
class UserRepository(Repository[User]):
    def find(self, id: int) -> User: ...

@Component
class PostRepository(Repository[Post]):
    def find(self, id: int) -> Post: ...

container.bind(Repository[User], UserRepository)
container.bind(Repository[Post], PostRepository)

user_repo = container.get(Repository[User])   # UserRepository
post_repo = container.get(Repository[Post])   # PostRepository
```

---

## Warm-up — eager singleton instantiation

By default singletons are created lazily on the first `get()` call.
Call `warm_up()` to pre-create them at startup so the first real request
doesn't pay the construction cost.

```python
# Sync — raises RuntimeError if any singleton has an async provider
container.warm_up()
container.warm_up(qualifier="db")    # only bindings with qualifier="db"
container.warm_up(priority=0)        # only bindings with priority=0

# Async — handles both sync and async singleton providers
await container.awarm_up()
await container.awarm_up(qualifier="db")
```

`warm_up()` is all-or-nothing: if any matching singleton is backed by an async
provider it raises **before** touching the cache, so the cache is never left
partially warmed.  Use `awarm_up()` when you have async providers.

---

## Dependency visualization

`container.describe()` returns a `DIContainerDescriptor` — a snapshot of the
entire binding registry with recursive dependency trees, scope information, and
scope-leak flags.  Use it to audit your wiring, generate documentation, or
build health-check endpoints.

> **Note:** `describe()` only resolves dependencies declared with `Inject[T]`,
> `Live[T]`, or `Lazy[T]`.  Plain type annotations (`dep: MyClass`) are
> invisible to the dependency graph layer.

```python
from providify.type import Inject

class Notifier: pass

@Singleton
class AlertService:
    def __init__(self, notifier: Inject[Notifier]) -> None:
        self.notifier = notifier

@Singleton
class EmailNotifier(Notifier): pass

container.bind(Notifier, EmailNotifier)
container.register(AlertService)

descriptor = container.describe()
print(descriptor)
# [SINGLETON]
# ├── Notifier [SINGLETON] → EmailNotifier
# ├── EmailNotifier [SINGLETON] → EmailNotifier
# └── AlertService [SINGLETON] → AlertService
#     └── Notifier [SINGLETON] → EmailNotifier
```

### Structured access

```python
# Bindings grouped by scope
descriptor.singleton_bindings   # list[BindingDescriptor]
descriptor.request_bindings
descriptor.session_bindings
descriptor.dependent_bindings

# Scope-leak detection — True when a longer-lived component holds a
# direct reference to a shorter-lived one (e.g. SINGLETON → DEPENDENT)
for b in descriptor.singleton_bindings:
    for dep in b.dependencies:
        if dep.scope_leak:
            print(f"⚠ Scope leak: {b.interface} → {dep.interface}")
```

### JSON export

```python
import json

data = container.describe().to_dict()
print(json.dumps(data, indent=2))
# {
#   "validated": false,
#   "singleton_bindings": [
#     {
#       "interface": "AlertService",
#       "implementation": "AlertService",
#       "scope": "SINGLETON",
#       "qualifier": null,
#       "scope_leak": false,
#       "priority": null,
#       "dependencies": [
#         {
#           "interface": "Notifier",
#           "implementation": "EmailNotifier",
#           "scope": "SINGLETON",
#           "scope_leak": false,
#           ...
#         }
#       ]
#     }
#   ],
#   ...
# }
```

---

## Circular dependency detection

The container detects circular dependencies at resolution time and raises
`CircularDependencyError` with a readable chain:

```
CircularDependencyError: Circular dependency detected: OrderService -> PaymentService -> OrderService
```

To break a cycle intentionally, use `Lazy[T]`:

```python
@Component
class A:
    def __init__(self, b: Lazy[B]) -> None:
        self._b = b   # proxy — B is not resolved during A's construction

@Component
class B:
    def __init__(self, a: A) -> None:
        self.a = a    # A is fully constructed here — no cycle
```

---

## Interceptors — @Interceptor / @AroundInvoke

Interceptors add cross-cutting behaviour (logging, transactions, metrics) to any bean without modifying its source code.

### Define an interceptor binding

`@InterceptorBinding` creates an annotation class that binds an interceptor to its targets:

```python
from providify import InterceptorBinding, Interceptor, AroundInvoke, InvocationContext

@InterceptorBinding
class Logged: ...       # use @Logged to mark targets

@InterceptorBinding
class Transactional: ...
```

### Implement the interceptor

```python
@Transactional
@Interceptor
class TxInterceptor:
    @AroundInvoke
    def around(self, ctx: InvocationContext) -> object:
        print(f"BEGIN tx for {ctx.method.__name__}")
        result = ctx.proceed()   # call the next interceptor or the real method
        print("COMMIT tx")
        return result
```

### Attach and activate

```python
@Transactional          # binds TxInterceptor to this bean
@Singleton
class OrderService:
    def place_order(self, order: Order) -> None: ...

container.register(OrderService)
container.add_interceptor(TxInterceptor)

svc = container.get(OrderService)
svc.place_order(order)
# Output:
# BEGIN tx for place_order
# COMMIT tx
```

### InvocationContext

`ctx.proceed()` calls the next interceptor in the chain, or the real method if no more interceptors remain.

| Attribute | Type | Description |
|-----------|------|-------------|
| `ctx.target` | `object` | The bean instance |
| `ctx.method` | `Callable` | The method being intercepted |
| `ctx.parameters` | `tuple` | Positional arguments |
| `ctx.kwargs` | `dict` | Keyword arguments |
| `ctx.proceed()` | `Any` | Continue the invocation chain |

> **Note**: `isinstance(proxy, TargetClass)` returns `False` on the wrapped proxy. Use `type(proxy._target)` if you need the real class.

---

## @Decorator — interface delegation

`@Decorator` stacks additional behaviour on top of an existing bean without replacing it. The decorator receives the original bean via `Delegate[T]` injection — identical to the Jakarta CDI `@Decorator` pattern.

```python
from providify import Decorator, Delegate, Component
from typing import Annotated
from providify.type import DelegateMeta

class Notifier:
    def send(self, msg: str) -> None: ...

@Component
class EmailNotifier(Notifier):
    def send(self, msg: str) -> None:
        print(f"email: {msg}")

@Decorator
@Component(priority=10)   # higher priority ensures this wraps EmailNotifier
class LoggingNotifier(Notifier):
    def __init__(self, delegate: Annotated[Notifier, DelegateMeta()]) -> None:
        self._delegate = delegate   # receives EmailNotifier instance

    def send(self, msg: str) -> None:
        print(f"[LOG] sending: {msg}")
        self._delegate.send(msg)   # delegate to inner bean

container.bind(Notifier, EmailNotifier)
container.register(LoggingNotifier)

n = container.get(Notifier)
n.send("hello")
# [LOG] sending: hello
# email: hello
```

Multiple decorators can be stacked — they are applied in ascending priority order (lowest first, outermost last).

---

## Event bus — Event[T] / @Observes

Fire and observe typed events across beans without direct coupling.

### Fire an event

Inject `Event[T]` anywhere; call `.fire()` (or `await .afire()` for async):

```python
from providify import Component, Event
from typing import Annotated
from providify.type import EventMeta

class UserRegistered:
    def __init__(self, email: str) -> None:
        self.email = email

@Component
class RegistrationService:
    def __init__(self, user_event: Annotated[UserRegistered, EventMeta()]) -> None:
        self._event = user_event   # EventProxy[UserRegistered]

    def register(self, email: str) -> None:
        # ... create user ...
        self._event.fire(UserRegistered(email))     # sync
        # await self._event.afire(UserRegistered(email))  # async
```

### Observe an event

```python
from providify import Singleton, Observes

@Singleton
class AuditLog:
    @Observes(UserRegistered)
    def on_user_registered(self, event: UserRegistered) -> None:
        print(f"New user: {event.email}")

    @Observes(UserRegistered)
    async def async_on_registered(self, event: UserRegistered) -> None:
        await self._db.insert(event.email)
```

Observers are registered automatically when the bean is first instantiated. They are held via **weak references** — if the observer bean is garbage-collected, it is silently removed from the dispatch list.

> **Scope constraint**: observer beans should be SINGLETON, REQUEST, or SESSION scoped. DEPENDENT (`@Component`) observers may be GC'd before an event fires unless `track=True` is set.

### EventProxy API

| Method | Description |
|--------|-------------|
| `.fire(event)` | Dispatch synchronously to all registered observers |
| `await .afire(event)` | Dispatch — awaits async observers, calls sync ones directly |

Subtype events match supertype observers: if `AdminRegistered` extends `UserRegistered`, observers of `UserRegistered` will also receive `AdminRegistered` events.

---

## InjectionPoint — injection context metadata

Inject an `InjectionPoint` to receive metadata about _where_ a dependency is being injected — useful for dynamic configuration like per-class loggers.

```python
from providify import Singleton, InjectionPoint, Provider
from providify.decorator.module import Configuration
import logging

@Configuration
class LoggingModule:
    @Provider
    def logger(self, ip: InjectionPoint) -> logging.Logger:
        name = ip.declaring_class.__name__ if ip.declaring_class else "root"
        return logging.getLogger(name)

@Singleton
class OrderService:
    def __init__(self, log: logging.Logger) -> None:
        # log is getLogger("OrderService") — set by the InjectionPoint
        self._log = log
```

### InjectionPoint fields

| Field | Type | Description |
|-------|------|-------------|
| `declaring_class` | `type \| None` | The class being constructed at injection time (`None` for top-level calls) |
| `param_name` | `str` | The parameter name at the injection site |
| `qualifier` | `str \| type \| None` | The qualifier in effect at the injection site |
| `annotation` | `Any` | The full type annotation of the parameter |

---

## NamedMeta — name-based qualifier at the injection site

`NamedMeta` lets you specify a string qualifier directly in the type annotation — without having to pass it to `container.get()`. It is the injection-site dual of `@Named`:

```python
from typing import Annotated
from providify import Component, Singleton
from providify.type import NamedMeta

@Component(qualifier="primary")
class PrimaryDB(Database): ...

@Component(qualifier="replica")
class ReplicaDB(Database): ...

@Singleton
class ReportService:
    def __init__(
        self,
        primary: Annotated[Database, NamedMeta("primary")],
        replica: Annotated[Database, NamedMeta("replica")],
    ) -> None:
        self._primary = primary
        self._replica = replica
```

`NamedMeta("x")` at the injection site is equivalent to `container.get(Database, qualifier="x")`.

---

## Running tests

```bash
cd tests
poetry install
poetry run pytest
```

Tests are organised by feature — one file per subsystem:

| File | Covers |
|------|--------|
| `test_binding.py` | `ClassBinding`, `ProviderBinding` construction and errors |
| `test_container.py` | `bind`, `register`, `provide`, `get`, `get_all`, `current`, `scoped` |
| `test_scopes.py` | SINGLETON, DEPENDENT, REQUEST, SESSION, scope violation detection, class-level attr scope safety |
| `test_inject.py` | `Inject[T]`, `InjectInstances[T]`, `optional=True/False`, `Optional[T]` / `T \| None` / `Union[T1, T2]`, class-level attribute injection |
| `test_lazy.py` | `LazyProxy` unit tests, `Lazy[T]` injection, circular-via-lazy |
| `test_live.py` | `LiveProxy` unit tests, `Live[T]` injection, always-fresh resolution |
| `test_instance.py` | `InstanceProxy` unit tests, `Instance[T]` injection, `is_resolvable()`, scope-safety, async |
| `test_lifecycle.py` | `@PostConstruct`, `@PreDestroy`, `shutdown`, `ashutdown` |
| `test_async.py` | `aget`, `aget_all`, async providers, async context manager |
| `test_configuration.py` | `@Configuration`, `install()`, `ainstall()`, Spring-style injection |
| `test_circular.py` | `CircularDependencyError`, diamond dependency, `Lazy` cycle-break |
| `test_generics.py` | Generic[T] binding and resolution, parameterised interfaces |
| `test_scoped_providers.py` | `@Provider(scope=Scope.REQUEST/SESSION)` — factory result cached per scope |
| `test_warmup.py` | `warm_up()`, `awarm_up()`, all-or-nothing guard, qualifier/priority filter |
| `test_decorators.py` | `@Named`, `@Priority`, `@Inheritable`, stacking, error paths |
| `test_scanner.py` | `scan()`, recursive scan, ABC auto-binding, idempotency, `DIContainer(scan=...)` constructor |
| `test_describe.py` | `BindingDescriptor`, `ClassBinding.describe()`, ASCII tree output |
| `test_localns_cache.py` | `_build_localns()` caching and invalidation on `bind`/`register`/`provide` |
| `test_qualifier.py` | `@Qualifier` marker, type qualifier in binding and `get()`, string qualifier backward compat |
| `test_default.py` | `@Default` resolves same as no qualifier; `@Component(qualifier=Default)` visible on unqualified lookup |
| `test_alternative.py` | `@Alternative` excluded by default; activated by `enable_alternative()`; reverted by `disable_alternative()` |
| `test_stereotype.py` | `Stereotype(...)` applies scope/priority; explicit annotation wins over stereotype defaults |
| `test_interceptor.py` | `@Interceptor` wraps method; `ctx.proceed()` calls original; chain ordering by registration |
| `test_decorator_bean.py` | `@Decorator` wraps delegate; `DelegateMeta` receives inner implementation; multiple decorators stack |
| `test_events.py` | `fire()` reaches `@Observes` observer; `afire()` awaits async observer; subtype event matching; dead-ref cleanup |
| `test_injection_point.py` | `InjectionPoint` injected in constructor; `param_name` / `declaring_class` / `annotation` fields correct |
| `test_disposes.py` | `@Disposes` called on singleton shutdown; not called when instance never instantiated |
| `test_dependent_track.py` | `@PreDestroy` fires on `flush_dependents()`; context manager flushes on exit |
| `test_named_meta.py` | `NamedMeta("x")` resolves same binding as `qualifier="x"` |
| `test_field_provider.py` | `@property @Provider` registered at `install()` time; singleton property shares instance |
| `test_application_scoped.py` | `@ApplicationScoped` is identical to `@Singleton` |
| `test_run_in_scope.py` | `run_in_request` activates scope; `@RequestScoped` beans resolve; scope torn down after fn returns |

---

## Instance[T] — programmatic lookup handle

`Instance[T]` is the Jakarta CDI-inspired alternative when you need full
programmatic control over resolution at call time. Unlike `Inject[T]` (resolves
once, eager) or `InjectInstances[T]` (resolves all, eager), an `Instance[T]`
injects an `InstanceProxy` that defers every lookup to the call site — and
accepts qualifier / priority as **call-time arguments**, not annotation-time
metadata.

```python
from providify import Instance

@Singleton
class NotificationRouter:
    def __init__(self, senders: Instance[Sender]) -> None:
        self._senders = senders   # InstanceProxy — nothing resolved yet

    def route(self, msg: str, channel: str) -> None:
        # Qualifier chosen at runtime — same proxy, different filter each call
        sender = self._senders.get(qualifier=channel)
        sender.send(msg)

    def broadcast(self, msg: str) -> None:
        for sender in self._senders.get_all():
            sender.send(msg)

    def has_channel(self, channel: str) -> bool:
        # Side-effect-free check — never creates an instance
        return self._senders.resolvable(qualifier=channel)
```

### InstanceProxy methods

| Method | Description |
|--------|-------------|
| `.get(qualifier=None, priority=None)` | Resolve highest-priority match (sync) |
| `.get_all(qualifier=None)` | Resolve all matches sorted by priority (sync) |
| `.aget(qualifier=None, priority=None)` | Same as `.get()`, async |
| `.aget_all(qualifier=None)` | Same as `.get_all()`, async |
| `.resolvable(qualifier=None, priority=None)` | `True` if at least one binding matches — no instance created |

`get_all()` and `aget_all()` return `[]` (never raise) when no bindings match,
making them safe for the "zero or more" pattern.

### Scope safety

`Instance[T]` **always passes scope validation** — even `Instance[RequestScoped]`
inside a `@Singleton`. Because resolution is deferred to call time, the proxy
naturally fetches the current request's instance on each `.get()` call without
requiring an explicit `Live[T]` wrapper.

This exemption applies equally to the `ClassVar[Instance[T]]` form.

```python
@Singleton
class AuthGateway:
    # ✅ No LiveInjectionRequiredError — Instance[T] is inherently scope-safe
    def __init__(self, token: Instance[JwtToken]) -> None:
        self._token = token

    def verify(self) -> bool:
        return self._token.get().is_valid()   # re-resolves per request automatically
```

### `Lazy[T]` vs `Live[T]` vs `Instance[T]`

| | `Lazy[T]` | `Live[T]` | `Instance[T]` |
|---|---|---|---|
| Resolution time | First `.get()` call | Every `.get()` call | Every `.get()` call |
| Caches result | ✅ Yes | ❌ No | ❌ No |
| Qualifier at call time | ❌ Fixed at annotation | ❌ Fixed at annotation | ✅ Chosen per call |
| Breaks circular deps | ✅ Yes | ❌ No | ❌ No |
| Scope-safe in singletons | ❌ Stale after first access | ✅ Yes | ✅ Yes |
| `resolvable()` check | ❌ No | ❌ No | ✅ Yes |

---

## container.is_resolvable()

Check whether a type can be resolved **without creating any instances**:

```python
if container.is_resolvable(Notifier, qualifier="sms"):
    sms = container.get(Notifier, qualifier="sms")

# Reflects live binding state — re-evaluated on every call
container.bind(Notifier, SmsNotifier)
assert container.is_resolvable(Notifier) is True
```

---

## container.set_scoped()

Register a pre-built instance into the **currently active** scope cache.
Useful when an instance is created outside the container (e.g. deserialized
from a session cookie) and should be returned for subsequent `get()` calls
within the same scope block.

```python
with container.request():
    token = JwtToken.decode(raw_header)
    container.set_scoped(JwtToken, token)   # register into request cache

    # All code inside this block that resolves JwtToken gets this instance
    svc = container.get(AuthService)        # AuthService.token == token ✅
```

- Calling `set_scoped()` outside an active scope raises `RuntimeError` immediately.
- Calling it twice with the same type overwrites the cache entry.
- Works inside both `request()` and `session()` blocks.

---

## Scope reference

| Decorator | Lifetime |
|-----------|----------|
| `@Component` | New instance on every `get()` |
| `@Singleton` / `@ApplicationScoped` | One instance per container — shared for the container's lifetime |
| `@RequestScoped` | One instance per `container.request()` block |
| `@SessionScoped` | One instance per `container.session(id)` — survives across requests |

