Skip to content

Commit 61ff5c0

Browse files
committed
feat: Added middleware support to EllarMount and ApplicationContext available on App boostrapping
1 parent 7724b21 commit 61ff5c0

File tree

6 files changed

+102
-38
lines changed

6 files changed

+102
-38
lines changed

Makefile

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,9 @@ lint:fmt ## Run code linters
2323
ruff check ellar tests samples
2424
mypy ellar
2525

26+
ruff-fix: ## Run Ruff fixer
27+
ruff check ellar tests samples --fix --unsafe-fixes
28+
2629
fmt format:clean ## Run code formatters
2730
ruff format ellar tests samples
2831
ruff check --fix ellar tests samples

ellar/app/factory.py

Lines changed: 44 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -10,9 +10,11 @@
1010
from ellar.core.modules import ModuleRefBase
1111
from ellar.di import EllarInjector, ProviderConfig
1212
from ellar.reflect import reflect
13-
from ellar.utils import get_unique_type
13+
from ellar.threading.sync_worker import execute_async_context_manager
14+
from ellar.utils import get_name, get_unique_type
1415
from starlette.routing import BaseRoute, Host, Mount
1516

17+
from .context import ApplicationContext
1618
from .main import App
1719
from .services import EllarAppService
1820

@@ -118,6 +120,7 @@ def _build_modules(
118120
return routes
119121

120122
@classmethod
123+
@t.no_type_check
121124
def _create_app(
122125
cls,
123126
module: t.Type[t.Union[ModuleBase, t.Any]],
@@ -147,39 +150,48 @@ def _get_config_kwargs() -> t.Dict:
147150
service = EllarAppService(injector, config)
148151
service.register_core_services()
149152

150-
routes = cls._build_modules(app_module=module, injector=injector, config=config)
151-
152-
app = App(
153-
routes=routes,
154-
config=config,
155-
injector=injector,
156-
lifespan=config.DEFAULT_LIFESPAN_HANDLER,
157-
global_guards=global_guards,
158-
)
159-
160-
for module_config in reversed(list(injector.get_app_dependent_modules())):
161-
if injector.get_module(module_config.module): # pragma: no cover
162-
continue
153+
with execute_async_context_manager(ApplicationContext(injector)) as context:
154+
routes = cls._build_modules(
155+
app_module=module, injector=injector, config=config
156+
)
163157

164-
module_ref = module_config.configure_with_factory(
165-
config, injector.container
158+
app = App(
159+
routes=routes,
160+
config=config,
161+
injector=injector,
162+
lifespan=config.DEFAULT_LIFESPAN_HANDLER,
163+
global_guards=global_guards,
166164
)
165+
# tag application instance by ApplicationModule name
166+
context.injector.container.register_instance(app, App, tag=get_name(module))
167+
168+
for module_config in reversed(
169+
list(context.injector.get_app_dependent_modules())
170+
):
171+
if context.injector.get_module(
172+
module_config.module
173+
): # pragma: no cover
174+
continue
175+
176+
module_ref = module_config.configure_with_factory(
177+
config, context.injector.container
178+
)
167179

168-
assert isinstance(
169-
module_ref, ModuleRefBase
170-
), f"{module_config.module} is not properly configured."
180+
assert isinstance(
181+
module_ref, ModuleRefBase
182+
), f"{module_config.module} is not properly configured."
171183

172-
injector.add_module(module_ref)
173-
app.router.extend(module_ref.routes)
184+
context.injector.add_module(module_ref)
185+
app.router.extend(module_ref.routes)
174186

175-
# app.setup_jinja_environment
176-
app.setup_jinja_environment()
187+
# app.setup_jinja_environment
188+
app.setup_jinja_environment()
177189

178-
for module, module_ref in app.injector.get_modules().items():
179-
module_ref.run_module_register_services()
190+
for module, module_ref in context.injector.get_modules().items():
191+
module_ref.run_module_register_services()
180192

181-
if issubclass(module, IApplicationReady):
182-
app.injector.get(module).on_ready(app)
193+
if issubclass(module, IApplicationReady):
194+
context.injector.get(module).on_ready(app)
183195

184196
return app
185197

@@ -211,11 +223,12 @@ def create_app(
211223
)
212224
app_factory_module = get_unique_type()
213225
module(app_factory_module)
214-
return cls._create_app(
226+
app = cls._create_app(
215227
module=app_factory_module,
216228
config_module=config_module,
217229
global_guards=global_guards,
218230
)
231+
return t.cast(App, app)
219232

220233
@classmethod
221234
def create_from_app_module(
@@ -226,6 +239,8 @@ def create_from_app_module(
226239
] = None,
227240
config_module: t.Union[str, t.Dict, None] = None,
228241
) -> App:
229-
return cls._create_app(
242+
app = cls._create_app(
230243
module, config_module=config_module, global_guards=global_guards
231244
)
245+
246+
return t.cast(App, app)

ellar/app/main.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -201,8 +201,8 @@ def build_middleware_stack(self) -> ASGIApp:
201201
)
202202

203203
app = self.router
204-
for item in reversed(middleware):
205-
app = item(app=app, injector=self.injector)
204+
for cls, args, kwargs in reversed(middleware):
205+
app = cls(app, *args, **kwargs, ellar_injector=self.injector)
206206
return app
207207

208208
def application_context(self) -> ApplicationContext:

ellar/core/middleware/middleware.py

Lines changed: 14 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -16,11 +16,21 @@ def __init__(self, cls: t.Type[T], **options: t.Any) -> None:
1616
injectable()(self.cls)
1717
self.kwargs = build_init_kwargs(self.cls, self.kwargs)
1818

19+
def __iter__(self) -> t.Iterator[t.Any]:
20+
as_tuple = (self, self.args, self.kwargs)
21+
return iter(as_tuple)
22+
1923
@t.no_type_check
20-
def __call__(self, app: ASGIApp, injector: EllarInjector) -> T:
21-
self.kwargs.update(app=app)
24+
def __call__(self, app: ASGIApp, *args: t.Any, **kwargs: t.Any) -> T:
25+
from ellar.app.context import current_injector
26+
27+
kwargs.update(app=app)
28+
if "ellar_injector" in kwargs:
29+
injector: EllarInjector = kwargs.pop("ellar_injector")
30+
else:
31+
injector = current_injector
2232
try:
23-
return injector.create_object(self.cls, additional_kwargs=self.kwargs)
33+
return injector.create_object(self.cls, additional_kwargs=kwargs)
2434
except TypeError: # pragma: no cover
2535
# TODO: Fix future typing for lower python version.
26-
return self.cls(**self.kwargs)
36+
return self.cls(*args, **kwargs)

ellar/core/routing/mount.py

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -32,11 +32,18 @@ def __init__(
3232
middleware: t.Optional[t.Sequence[Middleware]] = None,
3333
) -> None:
3434
super(EllarMount, self).__init__(
35-
path=path, name=name, app=app, routes=[], middleware=middleware
35+
path=path, name=name, app=app, routes=[], middleware=[]
3636
)
3737
self.include_in_schema = include_in_schema
3838
self._current_found_route_key = f"{uuid.uuid4().hex:4}_EllarMountRoute"
3939
self._control_type = control_type
40+
self._middleware_stack = self.build_middleware_stack(middleware or [])
41+
42+
def build_middleware_stack(self, middleware: t.Sequence[Middleware]) -> ASGIApp:
43+
app = self._app_handler
44+
for cls, args, kwargs in reversed(middleware):
45+
app = cls(app, *args, **kwargs)
46+
return app
4047

4148
def get_control_type(self) -> t.Optional[t.Type[t.Any]]:
4249
return self._control_type
@@ -75,7 +82,7 @@ def matches(self, scope: TScope) -> t.Tuple[Match, TScope]:
7582

7683
return Match.NONE, {}
7784

78-
async def handle(self, scope: TScope, receive: TReceive, send: TSend) -> None:
85+
async def _app_handler(self, scope: TScope, receive: TReceive, send: TSend) -> None:
7986
request_logger.debug(
8087
f"Executing Matched URL Handler, path={scope['path']} - '{self.__class__.__name__}'"
8188
)
@@ -87,6 +94,9 @@ async def handle(self, scope: TScope, receive: TReceive, send: TSend) -> None:
8794
mount_router = t.cast(Router, self.app)
8895
await mount_router.default(scope, receive, send)
8996

97+
async def handle(self, scope: TScope, receive: TReceive, send: TSend) -> None:
98+
await self._middleware_stack(scope, receive, send)
99+
90100

91101
def router_default_decorator(func: ASGIApp) -> ASGIApp:
92102
@functools.wraps(func)

tests/test_controller/test_module_router.py

Lines changed: 27 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,21 @@
11
import pytest
2-
from ellar.common import ModuleRouter
2+
from ellar.common import IHostContext, ModuleRouter
3+
from ellar.core import Request
4+
from ellar.core.middleware import FunctionBasedMiddleware, Middleware
35
from ellar.core.router_builders import ModuleRouterBuilder
6+
from ellar.testing import Test
47

58
from .sample import router
69

710
another_router = ModuleRouter("/prefix/another", name="arouter")
811

912

13+
async def callable_middleware(ctx: IHostContext, call_next):
14+
scope, _, _ = ctx.get_args()
15+
scope["callable_middleware"] = "Functional Middleware executed successfully"
16+
await call_next()
17+
18+
1019
@another_router.get("/sample")
1120
def some_example():
1221
return {"message": "okay"}
@@ -68,3 +77,20 @@ def some_route():
6877
reversed_path = mount.url_path_for(f"{mount.name}:{mount.routes[0].name}")
6978
path = mount.path_format.replace("/{path}", mount.routes[0].path)
7079
assert reversed_path == path
80+
81+
82+
def test_module_router_callable_middleware():
83+
router_ = ModuleRouter(
84+
"",
85+
middleware=[Middleware(FunctionBasedMiddleware, dispatch=callable_middleware)],
86+
)
87+
88+
@router_.get
89+
async def some_route(req: Request):
90+
return {"message": req.scope["callable_middleware"]}
91+
92+
tm = Test.create_test_module(routers=[router_])
93+
res = tm.get_test_client().get("")
94+
95+
assert res.status_code == 200
96+
assert res.json() == {"message": "Functional Middleware executed successfully"}

0 commit comments

Comments
 (0)