Skip to content

src/genro_asgi/middleware/__init__.py

Source from this local checkout, regenerated when the reader rebuilds. Line links use #L<number>; a GitHub line range opens its first line.

1 # Copyright 2025 Softwell S.r.l.2 #3 # Licensed under the Apache License, Version 2.0 (the "License");4 # you may not use this file except in compliance with the License.5 # You may obtain a copy of the License at6 #7 #     https://www.apache.org/licenses/LICENSE-2.08 #9 # Unless required by applicable law or agreed to in writing, software10 # distributed under the License is distributed on an "AS IS" BASIS,11 # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.12 # See the License for the specific language governing permissions and13 # limitations under the License.14 15 """Middleware capability: the chain as a mixin over the base server (D16).16 17 The base server has NO middleware. This mixin adds the chain as a capability,18 composed BEFORE the server class (``class MyServer(MiddlewareMixin,19 BaseServer)``): its cooperative ``__init__`` peels ``middleware=`` (the20 ``{name: bool | dict}`` switches; ``None`` arms only the defaults) and21 ``middleware_registry=`` (extra ``{name: class}`` entries merged over22 ``default_registry()``), builds the chain ONCE around ``_base_call`` — the23 adapter delegating to the next ``__call__`` in the MRO, i.e. the base24 dispatch — and routes ONLY ``http`` scopes through it: ``lifespan`` and25 ``websocket`` go straight to ``super().__call__``. A composition WITHOUT the26 mixin simply lacks the attributes — a different type, not a ghost.27 28 ``default_registry()`` returns a FRESH dict per call ({"errors":29 ErrorMiddleware, "wellknown": WellKnownMiddleware, "logging":30 LoggingMiddleware, "cors": CORSMiddleware, "auth": AuthMiddleware, "session":31 SessionMiddleware} as of Phase 5) — deliberately a function so no module-level32 mutable registry exists.33 It lives in this module, not in ``base.py``, because ``base.py`` cannot import34 the concrete middleware modules (which subclass ``BaseMiddleware``) without a35 cycle.36 """37 38 from __future__ import annotations39 40 from typing import TYPE_CHECKING, Any41 42 from .authentication import AuthMiddleware43 from .base import BaseMiddleware, build_chain, headers_dict44 from .cors import CORSMiddleware45 from .errors import ErrorMiddleware46 from .logging import LoggingMiddleware47 from .session import SessionMiddleware48 from .wellknown import WellKnownMiddleware49 50 if TYPE_CHECKING:51     from ..types import ASGIApp, Receive, Scope, Send52 53 __all__ = [54     "AuthMiddleware",55     "BaseMiddleware",56     "CORSMiddleware",57     "ErrorMiddleware",58     "LoggingMiddleware",59     "MiddlewareMixin",60     "SessionMiddleware",61     "WellKnownMiddleware",62     "build_chain",63     "default_registry",64     "headers_dict",65 ]66 67 68 def default_registry() -> dict[str, type[BaseMiddleware]]:69     """A fresh ``{name: class}`` mapping of the middlewares shipped with the core."""70     return {71         "errors": ErrorMiddleware,72         "wellknown": WellKnownMiddleware,73         "logging": LoggingMiddleware,74         "cors": CORSMiddleware,75         "auth": AuthMiddleware,76         "session": SessionMiddleware,77     }78 79 80 class MiddlewareMixin:81     """Middleware capability mixin, composed BEFORE a server class.82 83     Constructor kwargs peeled here: ``middleware`` — the ``{name: bool | dict}``84     switches (a dict value enables the middleware and becomes its constructor85     options); ``middleware_registry`` — extra ``{name: class}`` entries merged86     over ``default_registry()``.87     """88 89     def __init__(self, **kwargs: Any) -> None:90         middleware: dict[str, bool | dict[str, Any]] | None = kwargs.pop("middleware", None)91         extra: dict[str, type[BaseMiddleware]] | None = kwargs.pop("middleware_registry", None)92         super().__init__(**kwargs)93         registry = default_registry()94         if extra:95             registry.update(extra)96         self._middleware_chain = build_chain(middleware or {}, self._base_call, self, registry)97 98     @property99     def middleware_chain(self) -> ASGIApp:100         """The assembled chain: outermost middleware first, base dispatch innermost."""101         return self._middleware_chain102 103     def get_middleware(self, middleware_class: type[BaseMiddleware]) -> BaseMiddleware | None:104         """The layer of that class in this chain, or ``None`` when it is off.105 106         Args:107             middleware_class: the class to look for; a subclass of it answers too.108 109         Returns:110             The assembled instance, or ``None`` — a middleware nobody enabled111             has none.112 113         The chain is walked from its head: every layer holds the next one, so114         the instances need not be kept a second time. What reads this is code115         that must do for a websocket what a middleware does for HTTP — the116         handshake asking ``SessionMiddleware`` for the session of a scope the117         chain never saw.118         """119         layer = self.middleware_chain120         while isinstance(layer, BaseMiddleware):121             if isinstance(layer, middleware_class):122                 return layer123             layer = layer.app124         return None125 126     async def _base_call(self, scope: Scope, receive: Receive, send: Send) -> None:127         """Innermost chain target: the next ``__call__`` after this mixin in the MRO."""128         await super().__call__(scope, receive, send)129 130     async def __call__(self, scope: Scope, receive: Receive, send: Send) -> None:131         """Route ``http`` scopes through the chain; every other scope passes straight through."""132         if scope["type"] != "http":133             await super().__call__(scope, receive, send)134             return135         await self.middleware_chain(scope, receive, send)