Source code for linkd.ext.starlette

# -*- coding: utf-8 -*-
# Copyright (c) 2025-present tandemdude
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included in all
# copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
# SOFTWARE.
"""
Extension module adding support for using linkd-based dependency injection with `Starlette <https://www.starlette.io/>`_.

See the examples directory for a full working application using this module.

----
"""

from __future__ import annotations

__all__ = ["Contexts", "DiContextMiddleware", "RequestContainer", "RootContainer", "inject"]

import functools
import typing as t

from linkd import context as _context
from linkd import solver as _solver
from linkd.context import RootContainer
from linkd.ext import _common
from linkd.ext._common import RequestContainer

if t.TYPE_CHECKING:
    from starlette.requests import Request
    from starlette.responses import Response
    from starlette.types import ASGIApp


[docs] @t.final class Contexts: """Collection of the dependency injection context values linkd.ext.starlette uses.""" __slots__ = () ROOT = _context.Contexts.ROOT """The root DI context - ALL other contexts are built with this as the parent.""" REQUEST = _common.REQUEST_CONTEXT """DI context used during HTTP request handling."""
try: from starlette.middleware.base import BaseHTTPMiddleware from starlette.requests import Request class DiContextMiddleware(BaseHTTPMiddleware): """ Middleware class which handles setting up a DI context for each HTTP request. Args: app: The app this middleware will be applied to. manager: The dependency injection manager to use when entering the DI context. Example: .. code-block:: python from starlette.applications import Starlette from starlette.middleware import Middleware import linkd manager = linkd.DependencyInjectionManager() middleware = [ Middleware(linkd.ext.starlette.DiContextMiddleware, manager=manager), ] app = Starlette(routes=..., lifetime=..., middleware=middleware) """ __slots__ = ("app", "manager") def __init__(self, app: ASGIApp, manager: _solver.DependencyInjectionManager) -> None: super().__init__(app) self.manager: _solver.DependencyInjectionManager = manager async def dispatch(self, request: Request, call_next: t.Callable[[Request], t.Awaitable[Response]]) -> Response: async with self.manager.enter_context(Contexts.ROOT), self.manager.enter_context(Contexts.REQUEST) as rc: rc.add_value(Request, request) return await call_next(request) except ImportError: if not t.TYPE_CHECKING:
[docs] class DiContextMiddleware: __slots__ = () def __init__(self, *args: t.Any) -> None: raise RuntimeError("starlette is not installed - starlette middleware is not available")
[docs] def inject(func: _common.InjectedCallableT) -> _common.InjectedCallableT: """ Specialised decorator enabling linkd dependency injection for starlette route handlers. This decorator must be used instead of the standard :meth:`~linkd.solver.inject` decorator so that starlette does not treat injection-enabled functions as ASGI apps instead of request/response functions. If you are enabling DI for any function other than a route handler then the standard decorator will still work. Args: func: The function to enable DI for. Returns: The function with dependency injection enabled. Note: ASGI route handlers are not supported when using this decorator, you should use the standard :meth:`~linkd.solver.inject` decorator instead. Example: .. code-block:: python from starlette.applications import Starlette from starlette.middleware import Middleware from starlette.requests import Request from starlette.responses import Response from starlette.routing import Route import linkd @linkd.ext.starlette.inject async def some_handler(request: Request, dependency: SomeDependency) -> Response: ... routes = [ Route("/foo", some_handler), ] manager = linkd.DependencyInjectionManager() middleware = [ Middleware(linkd.ext.starlette.DiContextMiddleware, manager=manager), ] app = Starlette(routes=routes, lifetime=..., middleware=middleware) """ injection_enabled = _solver.inject(func) @functools.wraps(func) async def wrapper(*args: t.Any, **kwargs: t.Any) -> t.Any: return await injection_enabled(*args, **kwargs) # sorry pyright :/ return wrapper # type: ignore[reportReturnType]