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
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -74,7 +74,7 @@ The current `StreamMessage` is resolvable within DI via the pre-built `faststrea
## API

- `setup_di(app, container)` — stores the container in the app context and registers startup/shutdown lifecycle hooks: on startup it reopens the container and adds the DI middleware to every broker of the app (including one added via `app.add_broker` after `setup_di`), after shutdown it closes the container
- `FromDI(dependency, *, use_cache=True, cast=False)` — FastStream `Depends` that resolves a provider (or type) from the request container
- `FromDI(dependency, *, use_cache=True, cast=False)` — FastStream `Depends` that resolves a provider (or type) from the request container. Raises `RuntimeError` naming `setup_di` when a message reaches it without the middleware installed
- `fetch_di_container(app)` — returns the root container from the app context
- `faststream_message_provider` — `ContextProvider` for the current `faststream.StreamMessage`

Expand Down
10 changes: 9 additions & 1 deletion modern_di_faststream/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,12 @@
_ROOT_CONTAINER_KEY = "di_container"
_REQUEST_CONTAINER_KEY = "request_container"

_MISSING_REQUEST_CONTAINER = (
"No request container for this message, so the DI middleware did not run for it. "
"Call setup_di(app, container) on the app that owns this broker and start the app; "
"in tests, pair the test broker with TestApp(app) in the same `async with`."
)


class _DIMiddlewareFactory:
__slots__ = ("di_container",)
Expand Down Expand Up @@ -89,7 +95,9 @@ class Dependency(typing.Generic[T_co]):
marker: integrations.Marker[T_co]

async def __call__(self, context: faststream.ContextRepo) -> T_co:
request_container: Container = context.get(_REQUEST_CONTAINER_KEY)
request_container: Container | None = context.get(_REQUEST_CONTAINER_KEY)
if request_container is None:
raise RuntimeError(_MISSING_REQUEST_CONTAINER)
return self.marker.resolve(request_container)


Expand Down
32 changes: 32 additions & 0 deletions tests/test_faststream_di.py
Original file line number Diff line number Diff line change
Expand Up @@ -111,3 +111,35 @@ async def test_middleware_is_installed_once_per_broker_across_restarts() -> None
for broker in (first, second):
installed = [m for m in broker.config.broker_middlewares if isinstance(m, _DIMiddlewareFactory)]
assert len(installed) == 1


def _subscribe_with_from_di(broker: NatsBroker, subject: str) -> None:
@broker.subscriber(subject)
async def subscriber(instance: typing.Annotated[SimpleCreator, FromDI(Dependencies.app_factory)]) -> None:
"""Never reached: resolving ``instance`` fails first."""


async def test_from_di_without_setup_di_names_the_missing_step() -> None:
"""INVARIANT: ``FromDI`` on a message the DI middleware never saw fails by naming ``setup_di``.

Broken by handing ``ContextRepo.get``'s ``None`` straight to the marker, which surfaces as
modern-di's ``AttributeError: 'NoneType' object has no attribute 'resolve_dependency'`` and
points at nothing in this package.
"""
broker = NatsBroker()
app_ = faststream.FastStream(broker)
_subscribe_with_from_di(broker, TEST_SUBJECT)

async with TestNatsBroker(broker) as br, TestApp(app_):
with pytest.raises(RuntimeError, match="setup_di"):
await br.publish(None, TEST_SUBJECT)


async def test_from_di_on_an_app_never_started_names_the_missing_step(app: faststream.FastStream) -> None:
"""The middleware is installed on startup, so a test broker entered without ``TestApp`` has none."""
broker = typing.cast(NatsBroker, app.broker)
_subscribe_with_from_di(broker, TEST_SUBJECT)

async with TestNatsBroker(broker) as br:
with pytest.raises(RuntimeError, match="TestApp"):
await br.publish(None, TEST_SUBJECT)
Loading