Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
39 changes: 28 additions & 11 deletions docs/integrations/grpc.md
Original file line number Diff line number Diff line change
Expand Up @@ -45,13 +45,12 @@ class Settings:


class RpcReport:
def __init__(self, settings: Settings, context: grpc.ServicerContext | None = None) -> None:
def __init__(self, settings: Settings, context: grpc.ServicerContext) -> None:
self._settings = settings # APP-scoped, injected by type
self._context = context # REQUEST context object, injected by type

def line(self) -> str:
peer = self._context.peer() if self._context is not None else "unknown"
return f"{self._settings.service_name} <- {peer}"
return f"{self._settings.service_name} <- {self._context.peer()}"


class AppGroup(Group):
Expand Down Expand Up @@ -140,20 +139,38 @@ import grpc
from modern_di import Group, Scope, providers


def make_caller(context: grpc.ServicerContext | None = None) -> str:
return context.peer() if context is not None else "unknown"
def make_caller(context: grpc.ServicerContext) -> str:
return context.peer()


class AppGroup(Group):
caller = providers.Factory(make_caller, scope=Scope.REQUEST)
```

`validate()` never constructs a provider, so the default isn't needed for
validation, and it does not make the context optional: outside an RPC, where no
context is set, resolving `caller` raises `ContextValueNotSetError`. For a factory
that must also resolve outside an RPC, pass your own
`ContextProvider(grpc.ServicerContext, scope=Scope.REQUEST, bound_type=None, default=None)`
through `kwargs`; see [Optional context](../providers/context.md#optional-context-default).
The interceptor's provider has no default, so the context is required. Outside an RPC no context
is set, and resolving `caller` raises `ContextValueNotSetError`. A parameter default such as
`context: grpc.ServicerContext | None = None` does not change that: once the interceptor has
registered its provider, the parameter is wired to it and the default is never used.

A factory that must also resolve outside an RPC needs its own optional provider. Declare a
`ContextProvider` for the same type with `default=None`, keep it out of type-based wiring with
`bound_type=None`, and pass it through `kwargs`. It reads the same context value as the
interceptor's provider. See [Optional context](../providers/context.md#optional-context-default).

```python
def make_caller(context: grpc.ServicerContext | None) -> str:
return context.peer() if context is not None else "unknown"


class AppGroup(Group):
optional_context = providers.ContextProvider(
grpc.ServicerContext, scope=Scope.REQUEST, bound_type=None, default=None
)
caller = providers.Factory(
make_caller, scope=Scope.REQUEST, kwargs={"context": optional_context}
)
```

The protobuf request `Message` is not exposed as a provider
(that would add a `protobuf` dependency); the request is already a servicer-method
argument.
Expand Down
16 changes: 9 additions & 7 deletions docs/integrations/writing-integrations.md
Original file line number Diff line number Diff line change
Expand Up @@ -436,13 +436,15 @@ Each official integration is its own repository and PyPI package, mirroring the
`ContextProvider` is registered by `setup_di`, so a service that requires it by
type would fail `validate()` if that call were placed *before* `setup_di`.
Demonstrate context injection in a dedicated "Framework context objects"
section instead. To validate a graph that references the connection object
*before* `setup_di` has registered its provider (a narrower, earlier check),
make the context parameter optional (`request: FrameworkType | None = None`)
so `validate()` skips it regardless of ordering, while the integration still
injects the real value at runtime. That is the pattern the [gRPC
page](grpc.md) uses and [Framework context
objects](../providers/context.md#framework-context-objects) documents. Follow
section instead, with a required parameter (`request: FrameworkType`).
Don't present `request: FrameworkType | None = None` as a way to make the
value optional. Once `setup_di` has registered the connection provider, the
parameter is wired to it and its default is never used, so resolving outside
a connection raises `ContextValueNotSetError`. For a factory that must also
resolve outside a connection, show an app-owned `ContextProvider` with
`bound_type=None, default=None` passed through `kwargs`, as the [gRPC
page](grpc.md#injecting-the-servicercontext) does and [Optional
context](../providers/context.md#optional-context-default) documents. Follow
the example with any framework-specific sections, a tailored `## See also`
block linking [Testing with overrides](../recipes/testing-overrides.md),
[Lifecycle](../providers/lifecycle.md), [Scopes](../providers/scopes.md), and
Expand Down
6 changes: 6 additions & 0 deletions docs/migration/to-4.x.md
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,10 @@ an ordinary dependency. With no value set it raises `ContextValueNotSetError`, w
resolved directly or as a `Factory` argument, and the parameter's default and annotation are
ignored. For a `Factory` argument, the message and `.parameter_name` name the parameter.

`ContextValueNotSetError` does not subclass `ArgumentResolutionError`. An
`except ArgumentResolutionError` clause that handled a missing context value in 3.x no longer
catches it; catch `ContextValueNotSetError` instead, or `ResolutionError`, which covers both.

Optional context is declared once, on the provider: `ContextProvider(T, default=X)` returns `X`
whenever no value is set. If you own the provider, add `default=` to it:

Expand Down Expand Up @@ -189,3 +193,5 @@ as before for every provider, and type hints that name `AbstractProvider` need n
`find_container(scope)` to reach an ancestor.
- `ContextValueNoneWarning` and `UnvalidatedContainerWarning` are removed. Neither has been emitted
since 3.0, so delete any `filterwarnings` entry or import that names them.
- The `modern_di.exceptions.warnings` module held only these three warnings and is deleted. An
import from that path raises `ModuleNotFoundError`, so delete it.
13 changes: 8 additions & 5 deletions docs/troubleshooting/argument-resolution-error.md
Original file line number Diff line number Diff line change
Expand Up @@ -33,11 +33,14 @@ class Dependencies(Group):
If the missing type is one a framework
integration provides at runtime (`fastapi.Request`, `taskiq.TaskiqMessage`, …), its
`ContextProvider` is registered by `setup_di()`, so a `container.validate()` call made
*before* `setup_di()` runs sees no provider for it yet and raises. Either call
`validate()` after `setup_di()` (the provider is registered by then), or make the
parameter optional (`request: fastapi.Request | None = None`) so validation skips it
regardless of ordering; the integration still injects the real value at runtime either
way. See [Framework context objects](../providers/context.md#framework-context-objects).
*before* `setup_di()` runs sees no provider for it yet and raises. Call `validate()` after
`setup_di()`, when the provider is registered. A parameter default such as
`request: fastapi.Request | None = None` does not make the value optional: once `setup_di()`
has registered the provider, the parameter is wired to it and the default is never used. For a
factory that must also resolve outside a request, declare your own
`ContextProvider(fastapi.Request, scope=Scope.REQUEST, bound_type=None, default=None)` and pass
it through `kwargs`. See [Framework context objects](../providers/context.md#framework-context-objects)
and [Optional context](../providers/context.md#optional-context-default).

Check `.suggestions` on the caught exception for a "did you mean" hint when a similarly-named type is
registered instead.
Expand Down
4 changes: 2 additions & 2 deletions docs/troubleshooting/child-container-registration-error.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,10 +18,10 @@ Call `add_providers()` on the root container instead:
app_container = Container(scope=Scope.APP, groups=[MyGroup])
request_container = app_container.build_child_container(scope=Scope.REQUEST)

# Wrong
# Broken
request_container.add_providers(late_provider) # raises ChildContainerRegistrationError

# Right
# Works
app_container.add_providers(late_provider)
```

Expand Down
4 changes: 2 additions & 2 deletions docs/troubleshooting/creator-call-error.md
Original file line number Diff line number Diff line change
Expand Up @@ -25,13 +25,13 @@ def create_service(host: str, port: int) -> Service: ...


class Dependencies(Group):
# Wrong: skip_creator_parsing=True but kwargs misses `port`
# Broken: skip_creator_parsing=True but kwargs misses `port`
service = providers.Factory(
create_service, scope=Scope.APP, skip_creator_parsing=True,
bound_type=Service, kwargs={"host": "localhost"},
)

# Right
# Works
service = providers.Factory(
create_service, scope=Scope.APP, skip_creator_parsing=True,
bound_type=Service, kwargs={"host": "localhost", "port": 5432},
Expand Down
4 changes: 2 additions & 2 deletions docs/troubleshooting/group-instantiation-error.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,10 +20,10 @@ class Dependencies(Group):
service = providers.Factory(Service, scope=Scope.APP)


# Wrong
# Broken
deps = Dependencies() # raises GroupInstantiationError

# Right
# Works
container = Container(groups=[Dependencies])
service = container.resolve_provider(Dependencies.service)
```
Expand Down
4 changes: 2 additions & 2 deletions docs/troubleshooting/invalid-child-scope-error.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,11 +20,11 @@ from modern_di import Scope

app_container = Container(scope=Scope.APP, groups=[MyGroup])

# Wrong: SESSION is not deeper than SESSION
# Broken: SESSION is not deeper than SESSION
mid = app_container.build_child_container(scope=Scope.SESSION)
bad = mid.build_child_container(scope=Scope.SESSION) # raises InvalidChildScopeError

# Right
# Works
good = mid.build_child_container(scope=Scope.REQUEST)
```

Expand Down
4 changes: 2 additions & 2 deletions docs/troubleshooting/invalid-scope-type-error.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,11 +20,11 @@ Use the built-in `Scope` enum, or your own `IntEnum` subclass:
```python
from modern_di import Container, Scope

# Wrong
# Broken
container = Container(scope=1) # raises InvalidScopeTypeError
container = Container(scope="APP") # raises InvalidScopeTypeError

# Right
# Works
container = Container(scope=Scope.APP)
```

Expand Down
8 changes: 4 additions & 4 deletions docs/troubleshooting/missing-provider.md
Original file line number Diff line number Diff line change
Expand Up @@ -42,12 +42,12 @@ startup catches this before the first request.

```python
# Broken: cannot resolve by type
def create_engine(...):
return sa_async.create_async_engine(...)
def create_engine(settings: Settings):
return sa_async.create_async_engine(settings.database_url)

# Works: return-typed
def create_engine(...) -> sa_async.AsyncEngine:
return sa_async.create_async_engine(...)
def create_engine(settings: Settings) -> sa_async.AsyncEngine:
return sa_async.create_async_engine(settings.database_url)
```

To fix it, add the return annotation, or set `bound_type=SomeType` on the provider explicitly.
Expand Down
4 changes: 2 additions & 2 deletions docs/troubleshooting/scope-not-initialized-error.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,10 +21,10 @@ Build the deeper-scoped container before resolving from it:
```python
app_container = Container(scope=Scope.APP, groups=[MyGroup])

# Wrong: no REQUEST container exists yet
# Broken: no REQUEST container exists yet
app_container.resolve(RequestScopedThing) # raises ScopeNotInitializedError

# Right
# Works
request_container = app_container.build_child_container(scope=Scope.REQUEST)
request_container.resolve(RequestScopedThing)
```
Expand Down
4 changes: 2 additions & 2 deletions docs/troubleshooting/scope-skipped-error.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,11 +21,11 @@ Build child containers through every intermediate scope your providers need:
```python
app_container = Container(scope=Scope.APP, groups=[MyGroup])

# Wrong: jumps straight past REQUEST
# Broken: jumps straight past REQUEST
action_container = app_container.build_child_container(scope=Scope.ACTION)
action_container.resolve(RequestScopedThing) # raises ScopeSkippedError

# Right: build through REQUEST first
# Works: build through REQUEST first
request_container = app_container.build_child_container(scope=Scope.REQUEST)
action_container = request_container.build_child_container(scope=Scope.ACTION)
action_container.resolve(RequestScopedThing)
Expand Down
4 changes: 2 additions & 2 deletions docs/troubleshooting/unknown-factory-kwarg-error.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,12 +21,12 @@ def create_service(connection_string: str) -> Service: ...


class Dependencies(Group):
# Wrong: typo — raises UnknownFactoryKwargError, suggests "connection_string"
# Broken: typo — raises UnknownFactoryKwargError, suggests "connection_string"
service = providers.Factory(
create_service, scope=Scope.APP, kwargs={"conection_string": "..."}
)

# Right
# Works
service = providers.Factory(
create_service, scope=Scope.APP, kwargs={"connection_string": "..."}
)
Expand Down
Loading