Coverage for src/lexigram/web/middleware/stack.py: 36%
22 statements
« prev ^ index » next coverage.py v7.15.4, created at 2026-08-25 04:37 +0800
« prev ^ index » next coverage.py v7.15.4, created at 2026-08-25 04:37 +0800
1"""Default middleware stack for Lexigram Web.
3Provides :class:`DefaultMiddlewareStack` — an explicit, named class that
4encapsulates the minimum set of Starlette-compatible middlewares applied by
5every Lexigram web application.
7Users can import this class to inspect, extend, or reproduce the default
8middleware stack without relying on opaque ``WebProvider`` internals.
9``WebProvider`` uses this class internally for DRY consistency.
11Example::
13 from lexigram.web.middleware import DefaultMiddlewareStack
15 stack = DefaultMiddlewareStack(container=container)
16 middlewares = stack.build()
17 # pass to Starlette(middleware=middlewares, ...)
18"""
20from __future__ import annotations
22from typing import TYPE_CHECKING, Any
24from lexigram.logging import get_logger
26if TYPE_CHECKING:
27 from starlette.middleware import Middleware as StarletteMiddleware
28 from starlette.types import ASGIApp
30 from lexigram.contracts.core.di import ContainerResolverProtocol
32logger = get_logger(__name__)
35class DefaultMiddlewareStack:
36 """Builds the default Lexigram middleware stack as an explicit, composable list.
38 Encapsulates the minimum set of middlewares that every Lexigram web
39 application applies so users can see and extend it without magic.
41 :class:`~lexigram.web.di.provider.WebProvider` delegates to this class
42 internally, ensuring that all code paths produce the same default stack.
44 Args:
45 container: The resolved DI container. Required for
46 :class:`~lexigram.web.middleware.di_scope.DIScopeMiddleware` which
47 provides request-scoped dependency resolution.
48 extra_middlewares: Additional user-supplied Lexigram middlewares to
49 include in the stack. They are adapted to Starlette
50 ``Middleware`` wrappers via
51 :class:`~lexigram.web.middleware.registry.MiddlewareAdapterRegistry`
52 and inserted before the built-in defaults.
54 Example::
56 from lexigram.web.middleware import DefaultMiddlewareStack
57 from lexigram.logging import get_logger
59 logger = get_logger(__name__)
61 # Inspect the default stack
62 stack = DefaultMiddlewareStack(container=container)
63 for mw in stack.build():
64 logger.debug("middleware", name=mw.cls.__name__)
66 # Extend with custom middleware
67 stack = DefaultMiddlewareStack(
68 container=container,
69 extra_middlewares=[MyTimingMiddleware()],
70 )
71 """
73 def __init__(
74 self,
75 container: ContainerResolverProtocol | None = None,
76 extra_middlewares: list[Any] | None = None,
77 ) -> None:
78 """Initialise the stack builder.
80 Args:
81 container: Resolved DI container for ``DIScopeMiddleware``.
82 extra_middlewares: Optional extra Lexigram middlewares to include.
83 """
84 self._container = container
85 self._extra_middlewares: list[Any] = list(extra_middlewares or [])
87 def build(self) -> list[StarletteMiddleware]:
88 """Build and return the ordered list of default Starlette middlewares.
90 The always-present entry is
91 :class:`~lexigram.web.middleware.di_scope.DIScopeMiddleware`.
92 Any ``extra_middlewares`` supplied at construction time are prepended
93 to that entry after adaptation.
95 Returns:
96 An ordered list of :class:`starlette.middleware.Middleware`
97 instances ready to pass to
98 :class:`starlette.applications.Starlette`.
99 """
100 from typing import cast
102 from lexigram.web.middleware.di_scope import DIScopeMiddleware
103 from lexigram.web.middleware.registry import MiddlewareAdapterRegistry
105 registry = MiddlewareAdapterRegistry()
107 # Start from user-supplied extras (order preserved)
108 middlewares: list[Any] = list(self._extra_middlewares)
110 # Always ensure DIScopeMiddleware is present for request-scoped DI
111 has_scope_mw = any(isinstance(mw, DIScopeMiddleware) for mw in middlewares)
112 if not has_scope_mw and self._container is not None:
113 middlewares.insert(
114 0,
115 DIScopeMiddleware(cast("ASGIApp", None), self._container), # type: ignore[arg-type]
116 )
117 logger.debug("default_middleware_stack.added_di_scope")
119 adapted = [registry.adapt(mw) for mw in middlewares]
120 logger.debug("default_middleware_stack.built", count=len(adapted))
121 return adapted
124__all__ = ["DefaultMiddlewareStack"]