Skip to content

Commit 4d63d1b

Browse files
committed
Dropped Starlette BaseHTTPMiddleware and added a FunctionBasedMiddleware
1 parent baf4b13 commit 4d63d1b

File tree

9 files changed

+133
-28
lines changed

9 files changed

+133
-28
lines changed

docs/overview/modules.md

Lines changed: 45 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -51,7 +51,7 @@ from ellar.core import ModuleBase
5151
commands=[],
5252
base_directory=None,
5353
static_folder='static',
54-
template_folder='template'
54+
template_folder='templates'
5555
)
5656
class BookModule(ModuleBase):
5757
pass
@@ -134,12 +134,12 @@ from ellar.core.context import IExecutionContext
134134
@Module()
135135
class ModuleExceptionSample(ModuleBase):
136136
@exception_handler(404)
137-
def exception_404_handler(cls, ctx: IExecutionContext, exc: Exception) -> Response:
137+
def exception_404_handler(cls, context: IExecutionContext, exc: Exception) -> Response:
138138
return JSONResponse(dict(detail="Resource not found."))
139139
```
140140
`exception_404_handler` will be register to the application at runtime during `ModuleExceptionSample` computation.
141141

142-
### **`Module Templating Filters`**
142+
### **Module Templating Filters**
143143
We can also define `Jinja2` templating filters in project Modules or any `@Module()` module.
144144
The defined filters are be passed down to `Jinja2` **environment** instance alongside the `template_folder`
145145
value when creating **TemplateLoader**.
@@ -193,6 +193,48 @@ class DogsModule(ModuleBase):
193193
pass
194194
```
195195

196+
## **Module Middleware**
197+
198+
Middlewares functions can be defined at Module level with `@middleware()` function decorator.
199+
200+
For example:
201+
202+
```python
203+
from ellar.common import Module, middleware
204+
from ellar.core import ModuleBase
205+
from ellar.core.context import IExecutionContext
206+
from starlette.responses import PlainTextResponse
207+
208+
209+
@Module()
210+
class ModuleMiddlewareSample(ModuleBase):
211+
@middleware()
212+
async def my_middleware_function_1(cls, context: IExecutionContext, call_next):
213+
request = context.switch_to_request() # for http response only
214+
request.state.my_middleware_function_1 = True
215+
await call_next()
216+
217+
@middleware()
218+
async def my_middleware_function_2(cls, context: IExecutionContext, call_next):
219+
connection = context.switch_to_http_connection() # for websocket response only
220+
if connection.scope['type'] == 'websocket':
221+
websocket = context.switch_to_websocket()
222+
websocket.state.my_middleware_function_2 = True
223+
await call_next()
224+
225+
@middleware()
226+
async def my_middleware_function_3(cls, context: IExecutionContext, call_next):
227+
connection = context.switch_to_http_connection() # for http response only
228+
if connection.headers['somekey']:
229+
# response = context.get_response() -> use the `response` to add extra definitions to things you want to see on
230+
return PlainTextResponse('Header is not allowed.')
231+
await call_next()
232+
```
233+
Things to note:
234+
235+
- middleware functions must be `async`.
236+
- middleware functions can return a `response` or modify a `response` returned
237+
- middleware functions must call `call_next` and `await` its actions as shown above.
196238

197239
## **Injector Module**
198240
`EllarInjector` is based on a python library [injector](https://injector.readthedocs.io/en/latest/index.html). Both share similar `Module` features with few distinct features.

ellar/common/decorators/middleware.py

Lines changed: 4 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,11 @@
11
import typing as t
22

3-
from starlette.middleware.base import BaseHTTPMiddleware
4-
53
from ellar.constants import MIDDLEWARE_HANDLERS_KEY
4+
from ellar.core.middleware import FunctionBasedMiddleware
65
from ellar.core.middleware.schema import MiddlewareSchema
76

87

9-
def add_middleware(
8+
def _add_middleware(
109
middleware_class: type, dispatch: t.Callable, **options: t.Any
1110
) -> None:
1211
setattr(
@@ -18,18 +17,16 @@ def add_middleware(
1817
)
1918

2019

21-
def middleware(middleware_type: str) -> t.Callable:
20+
def middleware() -> t.Callable:
2221
"""
2322
========= MODULE DECORATOR ==============
2423
2524
Defines middle functions at module level
26-
:param middleware_type: Middleware type
2725
:return: Function
2826
"""
29-
assert middleware_type == "http", 'Currently only middleware("http") is supported.'
3027

3128
def decorator(func: t.Callable) -> t.Callable:
32-
add_middleware(BaseHTTPMiddleware, dispatch=func)
29+
_add_middleware(FunctionBasedMiddleware, dispatch=func)
3330
return func
3431

3532
return decorator

ellar/core/middleware/__init__.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
11
from starlette.middleware import Middleware as Middleware
22
from starlette.middleware.authentication import AuthenticationMiddleware
3-
from starlette.middleware.base import BaseHTTPMiddleware
43
from starlette.middleware.cors import CORSMiddleware as CORSMiddleware
54
from starlette.middleware.errors import ServerErrorMiddleware
65
from starlette.middleware.gzip import GZipMiddleware as GZipMiddleware
@@ -14,12 +13,13 @@
1413

1514
from .di import RequestServiceProviderMiddleware
1615
from .exceptions import ExceptionMiddleware
16+
from .function import FunctionBasedMiddleware
1717
from .versioning import RequestVersioningMiddleware
1818

1919
__all__ = [
2020
"Middleware",
2121
"AuthenticationMiddleware",
22-
"BaseHTTPMiddleware",
22+
"FunctionBasedMiddleware",
2323
"CORSMiddleware",
2424
"ServerErrorMiddleware",
2525
"ExceptionMiddleware",

ellar/core/middleware/exceptions.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ def __init__(
1717
debug: bool = False,
1818
) -> None:
1919
self.app = app
20-
self.debug = debug # TODO: We ought to handle 404 cases if debug is set.
20+
self.debug = debug
2121
self._exception_middleware_service = exception_middleware_service
2222

2323
async def __call__(self, scope: TScope, receive: TReceive, send: TSend) -> None:

ellar/core/middleware/function.py

Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
import typing as t
2+
3+
from starlette.responses import Response
4+
5+
from ellar.core.connection import HTTPConnection
6+
from ellar.core.context import IExecutionContext
7+
from ellar.types import ASGIApp, TReceive, TScope, TSend
8+
9+
AwaitableCallable = t.Callable[..., t.Awaitable]
10+
DispatchFunction = t.Callable[
11+
[IExecutionContext, AwaitableCallable], t.Awaitable[t.Optional[Response]]
12+
]
13+
T = t.TypeVar("T")
14+
15+
16+
class FunctionBasedMiddleware:
17+
"""
18+
Convert ASGI Middleware to a Node-like Middleware.
19+
20+
Usage: Example 1
21+
@middleware()
22+
def my_middleware(context: IExecution, call_next):
23+
print("Called my_middleware")
24+
request = context.switch_to_request()
25+
request.state.my_middleware = True
26+
await call_next()
27+
28+
Usage: Example 2
29+
@middleware()
30+
def my_middleware(context: IExecution, call_next):
31+
print("Called my_middleware")
32+
response = context.get_response()
33+
response.content = "Some Content"
34+
response.status_code = 200
35+
return response
36+
"""
37+
38+
def __init__(
39+
self, app: ASGIApp, dispatch: t.Optional[DispatchFunction] = None
40+
) -> None:
41+
self.app = app
42+
self.dispatch_function = dispatch or self.dispatch
43+
44+
async def dispatch(
45+
self, context: IExecutionContext, call_next: AwaitableCallable
46+
) -> Response:
47+
raise NotImplementedError() # pragma: no cover
48+
49+
async def __call__(self, scope: TScope, receive: TReceive, send: TSend) -> None:
50+
if scope["type"] not in ("http", "websocket"):
51+
await self.app(scope, receive, send)
52+
return
53+
54+
connection = HTTPConnection(scope, receive)
55+
56+
if not connection.service_provider: # pragma: no cover
57+
raise Exception("Service Provider is required")
58+
59+
context = connection.service_provider.get(IExecutionContext) # type: ignore
60+
61+
async def call_next() -> None:
62+
await self.app(scope, receive, send)
63+
64+
response = await self.dispatch_function(context, call_next)
65+
66+
if response and isinstance(response, Response):
67+
await response(scope, receive, send)

ellar/core/middleware/schema.py

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,14 @@
11
import typing as t
22

33
from starlette.middleware import Middleware
4-
from starlette.middleware.base import BaseHTTPMiddleware
54

65
from ellar.core.schema import Schema
76

7+
from .function import FunctionBasedMiddleware
8+
89

910
class MiddlewareSchema(Schema):
10-
middleware_class: t.Type[BaseHTTPMiddleware]
11+
middleware_class: t.Type[FunctionBasedMiddleware]
1112
dispatch: t.Callable[[t.Any, t.Callable], t.Any]
1213
options: t.Dict
1314

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,10 @@
1-
from starlette.middleware.base import BaseHTTPMiddleware
2-
31
from ellar.common import middleware
42
from ellar.constants import MIDDLEWARE_HANDLERS_KEY
3+
from ellar.core.middleware import FunctionBasedMiddleware
54
from ellar.core.middleware.schema import MiddlewareSchema
65

76

8-
@middleware("http")
7+
@middleware()
98
def middleware_function_test():
109
pass
1110

@@ -15,5 +14,5 @@ def test_middleware_decorator_works():
1514
middleware_detail: MiddlewareSchema = getattr(
1615
middleware_function_test, MIDDLEWARE_HANDLERS_KEY
1716
)
18-
assert middleware_detail.middleware_class is BaseHTTPMiddleware
17+
assert middleware_detail.middleware_class is FunctionBasedMiddleware
1918
assert middleware_detail.dispatch is middleware_function_test

tests/test_modules/test_module_ref.py

Lines changed: 6 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -195,10 +195,9 @@ async def exception_404(cls, ctx, exc):
195195
def test_module_template_ref_scan_middle_ware():
196196
@Module()
197197
class ModuleMiddlewareExample(ModuleBase):
198-
@middleware("http")
199-
async def middleware(cls, request, call_next):
200-
response = await call_next(request)
201-
return response
198+
@middleware()
199+
async def middleware_func(cls, context, call_next):
200+
await call_next()
202201

203202
config = Config(**{MIDDLEWARE_HANDLERS_KEY: ()})
204203
container = EllarInjector(auto_bind=False).container
@@ -207,10 +206,10 @@ async def middleware(cls, request, call_next):
207206
create_module_ref_factor(
208207
ModuleMiddlewareExample, config=config, container=container
209208
)
210-
middleware = config[MIDDLEWARE_HANDLERS_KEY]
209+
config_middleware = config[MIDDLEWARE_HANDLERS_KEY]
211210

212-
assert isinstance(middleware, list)
213-
assert "middleware" == get_name(middleware[0].options["dispatch"])
211+
assert isinstance(config_middleware, list)
212+
assert "middleware_func" == get_name(config_middleware[0].options["dispatch"])
214213

215214

216215
def test_module_template_ref_get_all_routers():

tests/test_response/test_response_file.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -83,9 +83,9 @@ def test_file_response_for_module_router_and_controller(path):
8383
response.headers["content-disposition"]
8484
== 'attachment; filename="file-test-css.css"'
8585
)
86-
assert response.headers["content-length"] == "22"
86+
assert response.headers["content-length"] == "23"
8787
assert response.headers["etag"]
88-
assert response.text == ".div {background: red}"
88+
assert response.text == ".div {background: red}\n"
8989

9090

9191
@pytest.mark.parametrize(

0 commit comments

Comments
 (0)