|
| 1 | +# **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. |
| 4 | + |
| 5 | +Why is this important? Consider a scenario where a general-purpose module needs to behave differently in different use cases, |
| 6 | +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, |
| 7 | +where a generic facility requires some configuration before it can be used by a consumer. |
| 8 | + |
| 9 | +## **Module Dynamic Setup** |
| 10 | + |
| 11 | +To dynamically configure a module, the module should inherit from `IModuleSetup` and provide a `setup` method or `setup_register` method |
| 12 | +that performs the necessary actions for setting up the module and then returns a `DynamicModule` or `ModuleSetup` instance. |
| 13 | + |
| 14 | +```python |
| 15 | +import typing as t |
| 16 | +from ellar.core.modules import DynamicModule, ModuleSetup |
| 17 | + |
| 18 | +class IModuleSetup: |
| 19 | + """Modules that must have a custom setup should inherit from IModuleSetup""" |
| 20 | + |
| 21 | + @classmethod |
| 22 | + def setup(cls, *args: t.Any, **kwargs: t.Any) -> DynamicModule: |
| 23 | + pass |
| 24 | + |
| 25 | + @classmethod |
| 26 | + def register_setup(cls, *args: t.Any, **kwargs: t.Any) -> ModuleSetup: |
| 27 | + pass |
| 28 | + |
| 29 | +``` |
| 30 | + |
| 31 | +Note that `setup` method returns a `DynamicModule` instance, while `register_setup` method returns a `ModuleSetup` instance. |
| 32 | +The `DynamicModule` instance is used when the module requires some configuration before it can be used by a consumer, |
| 33 | +while the `ModuleSetup` instance is used when the module does not require any additional configuration outside the ones provided in the application config. |
| 34 | + |
| 35 | +## **DynamicModule** |
| 36 | +`DynamicModule` is a dataclass type that is used **override** `Module` decorated attributes at easy without having to modify the module code directly. |
| 37 | +In other words, it gives you the flexibility to reconfigure module. |
| 38 | + |
| 39 | +For example: Lets look at the code below: |
| 40 | +```python |
| 41 | +from ellar.common import Module |
| 42 | +from ellar.core import DynamicModule |
| 43 | +from ellar.di import ProviderConfig |
| 44 | + |
| 45 | +@Module(providers=[ServiceA, ServiceB]) |
| 46 | +class ModuleA: |
| 47 | + pass |
| 48 | + |
| 49 | +# we can reconfigure ModuleA dynamically using `DynamicModule` as shown below |
| 50 | + |
| 51 | +@Module( |
| 52 | + modules=[ |
| 53 | + DynamicModule( |
| 54 | + ModuleA, |
| 55 | + providers=[ |
| 56 | + ProviderConfig(ServiceA, use_class=str), |
| 57 | + ProviderConfig(ServiceB, use_class=dict), |
| 58 | + ] |
| 59 | + ) |
| 60 | + ] |
| 61 | +) |
| 62 | +class ApplicationModule: |
| 63 | + pass |
| 64 | +``` |
| 65 | +`ModuleA` has been defined with some arbitrary providers (`ServiceA` and `ServiceB`), but during registration of `ModuleA` in `ApplicationModule`, |
| 66 | +we used `DynamicModule` to **override** its Module attribute `providers` with a new set of data. |
| 67 | + |
| 68 | + |
| 69 | +## **ModuleSetup** |
| 70 | +ModuleSetup is a dataclass type that used to set up a module based on its dependencies. |
| 71 | +It allows you to define the module **dependencies** and allow a **callback factory** function for a module dynamic set up. |
| 72 | + |
| 73 | +**`ModuleSetup` Properties**: |
| 74 | + |
| 75 | +- **`module`:** a required property that defines the type of module to be configured. The value must be a subclass of ModuleBase or IModuleSetup. |
| 76 | +- **`inject`:** a sequence property that holds the types to be injected to the factory method. The order of the types will determine the order at which they are injected. |
| 77 | +- **`factory`:** a factory function used to configure the module and take `Module` type as first argument and other services as listed in `inject` attribute. |
| 78 | + |
| 79 | +Let's look this `ModuleSetup` example code below with our focus on how we eventually configured `DynamicService` type, |
| 80 | +how we used `my_module_configuration_factory` to dynamically build `MyModule` module. |
| 81 | + |
| 82 | +```python |
| 83 | +import typing as t |
| 84 | +from ellar.common import Module, IModuleSetup |
| 85 | +from ellar.di import ProviderConfig |
| 86 | +from ellar.core import DynamicModule, ModuleBase, Config, ModuleSetup, AppFactory |
| 87 | + |
| 88 | + |
| 89 | +class Foo: |
| 90 | + def __init__(self): |
| 91 | + self.foo = 'foo' |
| 92 | + |
| 93 | + |
| 94 | +class DynamicService: |
| 95 | + def __init__(self, param1: t.Any, param2: t.Any, foo: str): |
| 96 | + self.param1 = param1 |
| 97 | + self.param2 = param2 |
| 98 | + self.foo = foo |
| 99 | + |
| 100 | + |
| 101 | +@Module() |
| 102 | +class MyModule(ModuleBase, IModuleSetup): |
| 103 | + @classmethod |
| 104 | + def setup(cls, param1: t.Any, param2: t.Any, foo: Foo) -> DynamicModule: |
| 105 | + return DynamicModule( |
| 106 | + cls, |
| 107 | + providers=[ProviderConfig(DynamicService, use_value=DynamicService(param1, param2, foo.foo))], |
| 108 | + ) |
| 109 | + |
| 110 | + |
| 111 | +def my_module_configuration_factory(module: t.Type[MyModule], config: Config, foo: Foo): |
| 112 | + return module.setup(param1=config.param1, param2=config.param2, foo=foo) |
| 113 | + |
| 114 | + |
| 115 | +@Module(modules=[ModuleSetup(MyModule, inject=[Config, Foo], factory=my_module_configuration_factory),], providers=[Foo]) |
| 116 | +class ApplicationModule(ModuleBase): |
| 117 | + pass |
| 118 | + |
| 119 | + |
| 120 | +app = AppFactory.create_from_app_module(ApplicationModule, config_module=dict( |
| 121 | + param1="param1", |
| 122 | + param2="param2", |
| 123 | +)) |
| 124 | + |
| 125 | +dynamic_service = app.injector.get(DynamicService) |
| 126 | +assert dynamic_service.param1 == "param1" |
| 127 | +assert dynamic_service.param2 == "param2" |
| 128 | +assert dynamic_service.foo == "foo" |
| 129 | +``` |
| 130 | +In the example, we started by defining a service `DynamicService`, whose parameter depended on some values from application config |
| 131 | +and from another service `Foo`. We then set up a `MyModule` and used as **setup** method which takes all parameter needed by |
| 132 | +`DynamicService` after that, we created `DynamicService` as a singleton and registered as a provider in `MyModule` |
| 133 | +for it to be accessible and injectable. |
| 134 | + |
| 135 | +At this point, looking at the setup function of `MyModule`, its clear `MyModule` depends on `Config` and `Foo` service. And this is where `ModuleSetup` usefulness comes in. |
| 136 | + |
| 137 | +During registration in `ApplicationModule`, we wrapped `MyModule` around a `ModuleSetup` and stated its dependencies in the `inject` property and also |
| 138 | +provided a `my_module_configuration_factory` factory that takes in module dependencies and return a `DynamicModule` configuration of `MyModule`. |
| 139 | + |
| 140 | +When `AppFactory` starts module bootstrapping, `my_module_configuration_factory` will be called with |
| 141 | +all the required **parameters** and returned a `DynamicModule` of `MyModule`. |
| 142 | + |
| 143 | +For more example, checkout [Ellar Throttle Module](https://github.com/eadwinCode/ellar-throttler/blob/master/ellar_throttler/module.py){target="_blank"} |
| 144 | +or [Ellar Cache Module](../../techniques/caching){target="_blank"} |
0 commit comments