Coverage for src/lexigram/web/middleware/base.py: 36%
61 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"""Middleware base class and registry.
3Canonical Middleware Pattern
4----------------------------
6There is **one** canonical way to write middleware in Lexigram:
8.. code-block:: python
10 class MyMiddleware:
11 def __init__(self, app: ASGIApp) -> None:
12 self.app = app
14 async def __call__(self, scope: Scope, receive: Receive, send: Send) -> None:
15 # before
16 await self.app(scope, receive, send)
17 # after (e.g. modify headers via a send wrapper)
19Register via ``WebProvider`` ``middleware`` kwarg or
20``provider.middleware_manager.add()``.
22For route-specific concerns, use guards and interceptors instead of middleware.
23"""
25from __future__ import annotations
27from collections.abc import Callable
28from enum import IntEnum
29from typing import Any
31from starlette.types import ASGIApp
33from lexigram.logging import get_logger
35logger = get_logger(__name__)
38class MiddlewarePriority(IntEnum):
39 """Simple ordering for middleware registration.
41 Lower value => higher precedence (executed earlier / outermost).
42 """
44 EARLY = 0
45 NORMAL = 1
46 LATE = 2
49class MiddlewareChain:
50 """Chain of middleware for sequential execution"""
52 def __init__(self, middlewares: list[Any]):
53 self.middlewares = middlewares
55 async def process_request(self, request: Any) -> Any | None:
56 """Process request through middleware chain"""
57 for middleware in self.middlewares:
58 response = await middleware.process_request(request)
59 if response is not None:
60 # Short-circuit the chain
61 return response
62 return None
64 async def process_response(self, response: Any) -> Any:
65 """Process response through middleware chain (in reverse order)"""
66 for middleware in reversed(self.middlewares):
67 response = await middleware.process_response(response)
68 return response
71class MiddlewareRegistry:
72 """Registry to track and prevent duplicate middleware.
74 Ensures each middleware type is only registered once,
75 preventing double-execution bugs.
76 """
78 def __init__(self) -> None:
79 """Initialize middleware registry."""
80 self._registered_types: set[type[Any]] = set()
81 self._registered_names: set[str] = set()
82 self._middleware_order: list[str] = []
83 # Store tuples: (priority: int, middleware_class: Type, options: dict)
84 self._middleware_stack: list[tuple[int, type[Any], dict]] = []
85 # Cache for sorted middleware
86 self._sorted_cache: list[tuple[int, type[Any], dict]] | None = None
88 def register_middleware(
89 self,
90 middleware_class: type[Any],
91 *,
92 priority: MiddlewarePriority = MiddlewarePriority.NORMAL,
93 **options: Any,
94 ) -> None:
95 """Register ASGI middleware class with duplicate checking and priority.
97 Args:
98 middleware_class: ASGI Middleware class to register
99 priority: MiddlewarePriority (EARLY, NORMAL, LATE)
100 options: Additional middleware options
102 Raises:
103 ValueError: If middleware already registered
104 """
105 # Check if already registered
106 if middleware_class in self._registered_types:
107 logger.warning(
108 "Middleware %s already registered, skipping",
109 middleware_class.__name__,
110 )
111 return
113 # Invalidate cache when new middleware is added
114 self._sorted_cache = None
116 # Track registration
117 self._registered_types.add(middleware_class)
118 self._middleware_order.append(middleware_class.__name__)
120 # Store middleware entry with its priority for later composition
121 self._middleware_stack.append((int(priority), middleware_class, options))
123 logger.info(
124 "Registered middleware: %s (order: %d, priority: %s)",
125 middleware_class.__name__,
126 len(self._middleware_order),
127 priority.name,
128 )
130 def compose_app(self, app: ASGIApp) -> ASGIApp:
131 """Compose the middleware stack around the app.
133 Uses cached sorted middleware for performance.
135 Args:
136 app: The base ASGI app
138 Returns:
139 App wrapped with all middleware
140 """
141 # Use cached sorted middleware if available
142 if self._sorted_cache is None:
143 # Order entries by priority (EARLY -> NORMAL -> LATE) so that when we
144 # apply them in reverse we get outermost EARLY -> ... -> innermost LATE.
145 self._sorted_cache = sorted(self._middleware_stack, key=lambda t: t[0])
147 # Apply middleware in reverse order (innermost first)
148 for _, middleware_class, options in reversed(self._sorted_cache):
149 app = middleware_class(app, **options)
151 return app
153 def register_function_middleware(
154 self,
155 app: Any,
156 func: Callable[..., Any],
157 name: str | None = None,
158 ) -> None:
159 """Register function-based middleware with duplicate checking.
161 Args:
162 app: Starlette application
163 func: Middleware function
164 name: Unique name for this middleware (defaults to function name)
166 Raises:
167 ValueError: If middleware with same name already registered
168 """
169 middleware_name = name or func.__name__
171 # Check if already registered
172 if middleware_name in self._registered_names:
173 logger.warning(
174 "Function middleware '%s' already registered, skipping",
175 middleware_name,
176 )
177 return
179 # Register and track
180 app.middleware("http")(func)
181 self._registered_names.add(middleware_name)
182 self._middleware_order.append(middleware_name)
184 logger.info(
185 "Registered function middleware: %s (order: %d)",
186 middleware_name,
187 len(self._middleware_order),
188 )
190 def get_middleware_order(self) -> list[str]:
191 """Get list of registered middleware in execution order.
193 Returns:
194 List of middleware names in order they execute
195 """
196 return self._middleware_order.copy()
198 def is_registered(
199 self,
200 middleware: type[Any] | str,
201 ) -> bool:
202 """Check if middleware is registered.
204 Args:
205 middleware: Middleware class or name
207 Returns:
208 True if middleware is registered
209 """
210 if isinstance(middleware, str):
211 return middleware in self._registered_names
212 return middleware in self._registered_types