Skip to content

Commit e75db27

Browse files
committed
LazyModule loading: Added docs on lazy loading modules and refactored AppFactory to accept LazyModule when creating an application
1 parent f3e3902 commit e75db27

File tree

18 files changed

+161
-73
lines changed

18 files changed

+161
-73
lines changed

docs/basics/dynamic-modules.md

Lines changed: 100 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
# **Dynamic Modules**
2-
We have seen in many example given on how to statically configure a [module](../overview/modules.md){_target='blank'}.
3-
In this section we are going to look at different ways to dynamically set up a module.
2+
We have seen in many examples given on how to statically configure a [module](../overview/modules.md){_target='blank'}.
3+
In this section, we are going to look at different ways to dynamically set up a module.
44

55
Why is this important? Consider a scenario where a general-purpose module needs to behave differently in different use cases,
66
it may be useful to use a configuration-based approach to allow customization. This is similar to the concept of a "plugin" in many systems,
@@ -36,7 +36,7 @@ while the `ModuleSetup` instance is used when the module does not require any ad
3636
`DynamicModule` is a dataclass type that is used to **override** `Module` decorated attributes without having to modify the module code directly.
3737
In other words, it gives you the flexibility to reconfigure module.
3838

39-
For example: Lets look at the code below:
39+
For example, Let's look at the code below:
4040
```python
4141
from ellar.common import Module
4242
from ellar.core import DynamicModule
@@ -79,11 +79,12 @@ It allows you to define the module **dependencies** and allow a **callback facto
7979
Let's look this `ModuleSetup` example code below with our focus on how we eventually configured `DynamicService` type,
8080
how we used `my_module_configuration_factory` to dynamically build `MyModule` module.
8181

82-
```python
82+
```python linenums="1"
8383
import typing as t
8484
from ellar.common import Module, IModuleSetup
8585
from ellar.di import ProviderConfig
86-
from ellar.core import DynamicModule, ModuleBase, Config, ModuleSetup, AppFactory
86+
from ellar.core import DynamicModule, ModuleBase, Config, ModuleSetup
87+
from ellar.app import AppFactory
8788

8889

8990
class Foo:
@@ -123,6 +124,7 @@ app = AppFactory.create_from_app_module(ApplicationModule, config_module=dict(
123124
))
124125

125126
dynamic_service = app.injector.get(DynamicService)
127+
126128
assert dynamic_service.param1 == "param1"
127129
assert dynamic_service.param2 == "param2"
128130
assert dynamic_service.foo == "foo"
@@ -142,3 +144,96 @@ all the required **parameters** and returned a `DynamicModule` of `MyModule`.
142144

143145
For more example, checkout [Ellar Throttle Module](https://github.com/eadwinCode/ellar-throttler/blob/master/ellar_throttler/module.py){target="_blank"}
144146
or [Ellar Cache Module](../techniques/caching.md){target="_blank"}
147+
148+
149+
## **Lazy Loading Modules**
150+
Ellar supports loading module decorated classes through a string reference using `LazyModuleImport`.
151+
For a better application context availability usage in module like, `current_config`,
152+
`current_app` and `current_injector`, it's advised to go with lazy module import.
153+
154+
For example,
155+
we can lazy load `CarModule` from our example [here](../overview/modules.md#feature-modules){target="_blank"} into
156+
`ApplicationModule`
157+
158+
```python title="project_name/root_module.py" linenums="1"
159+
160+
from ellar.common import IExecutionContext, Module, exception_handler
161+
from ellar.common.responses import JSONResponse, Response
162+
from ellar.core import ModuleBase, LazyModuleImport as lazyLoad
163+
from ellar.samples.modules import HomeModule
164+
165+
166+
@Module(modules=[HomeModule, lazyLoad('apps.car.module:CarModule')])
167+
class ApplicationModule(ModuleBase):
168+
@exception_handler(404)
169+
def exception_404_handler(cls, ctx: IExecutionContext, exc: Exception) -> Response:
170+
return JSONResponse({"detail": "Resource not found."}, status_code=404)
171+
```
172+
173+
In the above illustration, we provided a string reference to `CarModule` into `LazyModuleImport` instance.
174+
And during `AppFactory` Module bootstrap, `CarModule` will be resolved, validated and registered into the application
175+
176+
### **Properties**
177+
`LazyModuleImport` attributes,
178+
179+
- `module`: String reference for Module import
180+
- `setup`: Module setup function name for modules that requires specific function as in case of `DynamicModule` and `ModuleSetup`.
181+
- `setup_options`: Module setup function parameters
182+
183+
### **Lazy Loading DynamicModules**
184+
Having the understanding of `DynamicModule` and its registration pattern,
185+
to lazy load DynamicModule follows the same pattern.
186+
187+
For example, lets lazy load `MyModule` as a `DynamicModule`.
188+
For that to happen, we need to call `MyModule.setup` with some parameters and in turn returns a `DynamicModule`
189+
190+
```python title="project_name/root_module.py" linenums="1"
191+
from ellar.common import Module, exception_handler, JSONResponse, IExecutionContext, Response
192+
from ellar.core import ModuleBase
193+
from .custom_module import MyModule, Foo
194+
195+
196+
@Module(modules=[MyModule.setup(12, 23, Foo())])
197+
class ApplicationModule(ModuleBase):
198+
@exception_handler(404)
199+
def exception_404_handler(cls, ctx: IExecutionContext, exc: Exception) -> Response:
200+
return JSONResponse({"detail": "Resource not found."}, status_code=404)
201+
```
202+
203+
Let's rewrite this using `LazyModuleImport`.
204+
205+
```python title="project_name/root_module.py" linenums="1"
206+
from ellar.common import Module, exception_handler, JSONResponse, IExecutionContext, Response
207+
from ellar.core import ModuleBase, LazyModuleImport as lazyLoad
208+
209+
210+
@Module(modules=[
211+
lazyLoad('project_name.custom_module:MyModule', 'setup', param1=12, param2=23, foo=Foo()),
212+
])
213+
class ApplicationModule(ModuleBase):
214+
@exception_handler(404)
215+
def exception_404_handler(cls, ctx: IExecutionContext, exc: Exception) -> Response:
216+
return JSONResponse({"detail": "Resource not found."}, status_code=404)
217+
218+
```
219+
220+
### **Lazy Loading ModuleSetup**
221+
Just as in `DynamicModule`, `ModuleSetup` can be lazy loaded the same way.
222+
Let's take [CacheModule](https://github.com/python-ellar/ellar/blob/main/ellar/cache/module.py) for example.
223+
224+
```python title="project_name/root_module.py" linenums="1"
225+
from ellar.common import Module, exception_handler, JSONResponse, IExecutionContext, Response
226+
from ellar.core import ModuleBase, LazyModuleImport as lazyLoad
227+
228+
229+
@Module(modules=[
230+
lazyLoad('ellar.cache.module:CacheModule', 'register_setup'),
231+
])
232+
class ApplicationModule(ModuleBase):
233+
@exception_handler(404)
234+
def exception_404_handler(cls, ctx: IExecutionContext, exc: Exception) -> Response:
235+
return JSONResponse({"detail": "Resource not found."}, status_code=404)
236+
237+
```
238+
In the above illustration, we have registered `CacheModule` through `register_setup` function
239+
which returns a `ModuleSetup` that configures the `CacheModule` to read its configurations from application config.

docs/overview/modules.md

Lines changed: 12 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -16,8 +16,8 @@ Thus, the architecture resulting from most applications will include multiple mo
1616
Building an application as a group of feature modules bundled together helps to manage complexity, have a maintainable, extendable, and testable code base, and encourage development using SOLID principles.
1717

1818
A typical example of a feature module is the **car** project. The `CarModule` wraps all the services and controller that manages the `car` resource which makes it easy to maintain, extend, and testable.
19-
```python
20-
# project_name/apps/car/module.py
19+
```python title='project_name/apps/car/module.py' linenums="1"
20+
2121

2222
from ellar.common import Module
2323
from ellar.core import ModuleBase
@@ -78,26 +78,24 @@ class BookModule(ModuleBase):
7878
### **Module Events**
7979
Every registered Module receives two event calls during its instantiation and when the application is ready.
8080

81-
```python
81+
```python linenums="1"
8282
from ellar.common import Module
83-
from ellar.core import ModuleBase, Config, App
83+
from ellar.core import ModuleBase, Config
8484

8585
@Module()
8686
class ModuleEventSample(ModuleBase):
8787
@classmethod
8888
def before_init(cls, config: Config) -> None:
8989
"""Called before creating Module object"""
90-
91-
def application_ready(self, app: App) -> None:
92-
"""Called when application is ready - this is similar to @on_startup event"""
9390

9491
```
95-
`before_init` receives current app `Config` as a parameter and `application_ready` function receives `App` instance as parameter.
92+
`before_init` receives current app `Config` as a parameter for further configurations before `ModuleEventSample` is initiated.
93+
It's important to note that returned values from `before_init` will be passed to the constructor of `ModuleEventSample` during instantiation.
9694

9795
### **Module Exceptions**
9896
Custom exception handlers can be registered through modules.
9997

100-
```python
98+
```python linenums="1"
10199
from ellar.common import Module, exception_handler, JSONResponse, Response, IHostContext
102100
from ellar.core import ModuleBase
103101

@@ -114,7 +112,7 @@ We can also define `Jinja2` templating filters in project Modules or any `@Modul
114112
The defined filters are be passed down to `Jinja2` **environment** instance alongside the `template_folder`
115113
value when creating **TemplateLoader**.
116114

117-
```python
115+
```python linenums="1"
118116

119117
from ellar.common import Module, template_global, template_filter
120118
from ellar.core import ModuleBase
@@ -137,9 +135,9 @@ class ModuleTemplateFilterSample(ModuleBase):
137135
## **Dependency Injection**
138136
A module class can inject providers as well (e.g., for configuration purposes):
139137

140-
For example, from our sample project, the can inject `Config` to the `CarModule`
138+
For example, from our sample project, we can inject `Config` to the `CarModule`
141139

142-
```python
140+
```python linenums="1"
143141
# project_name/apps/car/module.py
144142

145143
from ellar.common import Module
@@ -169,7 +167,7 @@ Middlewares functions can be defined at Module level with `@middleware()` functi
169167

170168
For example:
171169

172-
```python
170+
```python linenums="1"
173171
from ellar.common import Module, middleware, IHostContext, PlainTextResponse
174172
from ellar.core import ModuleBase
175173

@@ -211,7 +209,7 @@ As an added support, you can create or reuse modules from `injector` Modules.
211209
!!! info
212210
This type of module is used to configure `injector` **bindings** and **providers** for dependency injection purposes.
213211

214-
```python
212+
```python linenums="1"
215213
from ellar.core import ModuleBase
216214
from ellar.di import Container
217215
from injector import provider

docs/overview/providers.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
# **Providers**
22
A provider is any class or object that is **injectable** as a dependency to another class when creating an instance of that class.
33

4-
Providers are like services, repositories services, factories, etc., classes that manage complex tasks. These providers can be made available to a controller, a route handler, or to another provider as a dependency.
4+
Providers are like services, repository services, factories, etc., classes that manage complex tasks. These providers can be made available to a controller, a route handler, or to another provider as a dependency.
55
This concept is commonly known as [Dependency Injection](https://en.wikipedia.org/wiki/Dependency_injection){target="_blank"}
66

77
In Ellar, you can easily create a `provider/injectable` class by decorating that class with the `@injectable()` mark and stating the scope.

docs/security/authentication.md

Lines changed: 11 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -166,17 +166,16 @@ Let us configure the JWTModule inside AuthModule. 
166166
from datetime import timedelta
167167

168168
from ellar.common import Module
169-
from ellar.core import ModuleBase
169+
from ellar.core import ModuleBase, LazyModuleImport as lazyLoad
170170
from ellar_jwt import JWTModule
171171

172172
from .controllers import AuthController
173-
from ..user.module import UserModule
174173
from .services import AuthService
175174

176175

177176
@Module(
178177
modules=[
179-
UserModule,
178+
lazyLoad('project_name.users.module:UserModule'),
180179
JWTModule.setup(
181180
signing_secret_key="my_poor_secret_key_lol", lifetime=timedelta(minutes=5)
182181
),
@@ -231,14 +230,12 @@ To do that, we need to register `AuthModule` to the `ApplicationModule`.
231230
```python title="project_name.root_module.py" linenums="1"
232231
from ellar.common import Module, exception_handler
233232
from ellar.common import IExecutionContext, JSONResponse, Response
234-
from ellar.core import ModuleBase
233+
from ellar.core import ModuleBase, LazyModuleImport as lazyLoad
235234
from ellar.samples.modules import HomeModule
236-
from .apps.car.module import CarModule
237-
from .apps.auth.module import AuthModule
238235

239236

240237
@Module(
241-
modules=[HomeModule, CarModule, AuthModule],
238+
modules=[HomeModule, lazyLoad('project_name.auth.module:AuthModule'),],
242239
)
243240
class ApplicationModule(ModuleBase):
244241
@exception_handler(404)
@@ -451,18 +448,17 @@ First, let us register `AuthGuard` a global guard in `AuthModule`.
451448
from datetime import timedelta
452449

453450
from ellar.common import GlobalGuard, Module
454-
from ellar.core import ModuleBase
451+
from ellar.core import ModuleBase, LazyModuleImport as lazyLoad
455452
from ellar.di import ProviderConfig
456453
from ellar_jwt import JWTModule
457454

458455
from .controllers import AuthController
459-
from ..user.module import UserModule
460456
from .services import AuthService
461457
from .guards import AuthGuard
462458

463459
@Module(
464460
modules=[
465-
UserModule,
461+
lazyLoad('project_name.users.module:UserModule'),
466462
JWTModule.setup(
467463
signing_secret_key="my_poor_secret_key_lol", lifetime=timedelta(minutes=5)
468464
),
@@ -658,12 +654,12 @@ Let us make `JWTAuthentication` Handler available for ellar to use as shown belo
658654
```python title='project_name.server.py' linenums='1'
659655
import os
660656
from ellar.common.constants import ELLAR_CONFIG_MODULE
661-
from ellar.core.factory import AppFactory
662-
from .root_module import ApplicationModule
657+
from ellar.app import AppFactory
658+
from ellar.core import LazyModuleImport as lazyLoad
663659
from .auth_scheme import JWTAuthentication
664660

665661
application = AppFactory.create_from_app_module(
666-
ApplicationModule,
662+
lazyLoad('project_name.root_module:ApplicationModule'),
667663
config_module=os.environ.get(
668664
ELLAR_CONFIG_MODULE, "project_name.config:DevelopmentConfig"
669665
),
@@ -738,18 +734,17 @@ just like we did in [applying guard globally](#apply-authguard-globally)
738734
from datetime import timedelta
739735
from ellar.auth.guard import AuthenticatedRequiredGuard
740736
from ellar.common import GlobalGuard, Module
741-
from ellar.core import ModuleBase
737+
from ellar.core import ModuleBase, LazyModuleImport as lazyLoad
742738
from ellar.di import ProviderConfig
743739
from ellar_jwt import JWTModule
744740

745-
from ..users.module import UsersModule
746741
from .controllers import AuthController
747742
from .services import AuthService
748743

749744

750745
@Module(
751746
modules=[
752-
UsersModule,
747+
lazyLoad('project_name.users.module:UserModule'),
753748
JWTModule.setup(
754749
signing_secret_key="my_poor_secret_key_lol", lifetime=timedelta(minutes=5)
755750
),

ellar/app/factory.py

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -76,6 +76,9 @@ def _build_modules(
7676
:param injector: App Injector instance
7777
:return: `None`
7878
"""
79+
if isinstance(app_module, LazyModuleImport):
80+
app_module = app_module.get_module("AppFactory")
81+
7982
assert reflect.get_metadata(
8083
MODULE_WATERMARK, app_module
8184
), "Only Module is allowed"
@@ -129,8 +132,6 @@ def _get_config_kwargs() -> t.Dict:
129132
return {"config_module": config_module}
130133
return dict(config_module)
131134

132-
assert reflect.get_metadata(MODULE_WATERMARK, module), "Only Module is allowed"
133-
134135
config = Config(app_configured=True, **_get_config_kwargs())
135136

136137
injector = EllarInjector(auto_bind=config.INJECTOR_AUTO_BIND)

examples/01-carapp/carapp/server.py

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,17 +2,16 @@
22

33
from ellar.app import AppFactory
44
from ellar.common.constants import ELLAR_CONFIG_MODULE
5+
from ellar.core import LazyModuleImport as lazyLoad
56
from ellar.openapi import (
67
OpenAPIDocumentBuilder,
78
OpenAPIDocumentModule,
89
ReDocsUI,
910
SwaggerUI,
1011
)
1112

12-
from .root_module import ApplicationModule
13-
1413
application = AppFactory.create_from_app_module(
15-
ApplicationModule,
14+
lazyLoad("carapp.root_module:ApplicationModule"),
1615
config_module=os.environ.get(
1716
ELLAR_CONFIG_MODULE, "carapp.config:DevelopmentConfig"
1817
),

examples/02-socketio-app/socketio_app/apps/events/gateways.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ async def my_broadcast_event(self, message: MessageData = WsBody()):
2222

2323
@subscribe_message("join")
2424
async def join(self, message: MessageRoom = WsBody()):
25-
await self.context.server.enter_room(self.context.sid, message.room)
25+
self.context.server.enter_room(self.context.sid, message.room)
2626
await self.context.server.emit(
2727
"my_response",
2828
{"data": "Entered room: " + message.room},
@@ -31,7 +31,7 @@ async def join(self, message: MessageRoom = WsBody()):
3131

3232
@subscribe_message("leave")
3333
async def leave(self, message: MessageRoom = WsBody()):
34-
await self.context.server.leave_room(self.context.sid, message.room)
34+
self.context.server.leave_room(self.context.sid, message.room)
3535
await self.context.server.emit(
3636
"my_response", {"data": "Left room: " + message.room}, room=self.context.sid
3737
)
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
"""
2+
Define endpoints routes in python function fashion
3+
example:
4+
5+
my_router = ModuleRouter("/cats", tag="Cats", description="Cats Resource description")
6+
7+
@my_router.get('/')
8+
def index(request: Request):
9+
return {'detail': 'Welcome to Cats Resource'}
10+
"""

0 commit comments

Comments
 (0)