Skip to content

Commit dce02ec

Browse files
authored
Merge pull request #100 from eadwinCode/ellar_life_span_wrapper
Extended Lifespan to Module classes
2 parents 743dd95 + ae39b35 commit dce02ec

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

55 files changed

+631
-340
lines changed

docs/basics/custom-providers.md

Whitespace-only changes.

docs/basics/dynamic-modules.md

Lines changed: 144 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,144 @@
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"}

docs/basics/events.md

Lines changed: 0 additions & 1 deletion
This file was deleted.

docs/basics/execution-context.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -163,7 +163,7 @@ class ExecutionContext(HostContext):
163163

164164
These extra information are necessary for reading `metadata` properties set on controllers or the route handler function.
165165

166-
### How to access the current execution context
166+
### **How to access the current execution context**
167167
You can access the current execution context using the `Context()` function.
168168
This decorator can be applied to a parameter of a controller or service method,
169169
and it will inject the current `ExecutionContext` object into the method.

docs/basics/injection-scope.md

Whitespace-only changes.

docs/basics/injector-scopes.md

Lines changed: 104 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,104 @@
1+
# **Injector Scopes**
2+
There are 3 different scopes which defines ways a service/provider is instantiated.
3+
4+
- [TRANSIENT SCOPE](#transientscope-)
5+
- [SINGLETON SCOPE](#singletonscope-)
6+
- [REQUEST SCOPE](#requestscope-)
7+
8+
## **`transient_scope`**:
9+
Whenever a transient scoped provider is required, a new instance of the provider is created
10+
11+
```python
12+
# main.py
13+
14+
from ellar.di import EllarInjector, transient_scope, injectable
15+
16+
injector = EllarInjector(auto_bind=False)
17+
18+
19+
@injectable(scope=transient_scope)
20+
class ATransientClass:
21+
pass
22+
23+
injector.container.register(ATransientClass)
24+
# OR
25+
# injector.container.register_transient(ATransientClass)
26+
27+
def validate_transient_scope():
28+
a_transient_instance_1 = injector.get(ATransientClass)
29+
a_transient_instance_2 = injector.get(ATransientClass)
30+
31+
assert a_transient_instance_2 != a_transient_instance_1 # True
32+
33+
34+
if __name__ == "__main__":
35+
validate_transient_scope()
36+
```
37+
38+
## **`singleton_scope`**:
39+
A singleton scoped provider is created once throughout the lifespan of the Container instance.
40+
41+
For example:
42+
```python
43+
# main.py
44+
45+
from ellar.di import EllarInjector, singleton_scope, injectable
46+
47+
injector = EllarInjector(auto_bind=False)
48+
# OR
49+
50+
@injectable(scope=singleton_scope)
51+
class ASingletonClass:
52+
pass
53+
54+
injector.container.register(ASingletonClass)
55+
# OR
56+
# injector.container.register_singleton(ASingletonClass)
57+
58+
def validate_singleton_scope():
59+
a_singleton_instance_1 = injector.get(ASingletonClass)
60+
a_singleton_instance_2 = injector.get(ASingletonClass)
61+
62+
assert a_singleton_instance_2 == a_singleton_instance_1 # True
63+
64+
if __name__ == "__main__":
65+
validate_singleton_scope()
66+
```
67+
68+
## **`request_scope`**:
69+
A request scoped provider is instantiated once during the scope of the request. And it's destroyed once the request is complete.
70+
It is important to note that `request_scope` behaves like a `singleton_scope` during HTTPConnection mode and behaves like a `transient_scope` outside HTTPConnection mode.
71+
72+
```python
73+
# main.py
74+
75+
import uvicorn
76+
from ellar.di import EllarInjector, request_scope, injectable
77+
78+
injector = EllarInjector(auto_bind=False)
79+
80+
81+
@injectable(scope=request_scope)
82+
class ARequestScopeClass:
83+
pass
84+
85+
86+
injector.container.register(ARequestScopeClass)
87+
88+
89+
async def scoped_request(scope, receive, send):
90+
async with injector.create_asgi_args(scope, receive, send) as request_injector:
91+
request_instance_1 = request_injector.get(ARequestScopeClass)
92+
request_instance_2 = request_injector.get(ARequestScopeClass)
93+
assert request_instance_2 == request_instance_1
94+
95+
request_instance_1 = injector.get(ARequestScopeClass)
96+
request_instance_2 = injector.get(ARequestScopeClass)
97+
98+
assert request_instance_2 != request_instance_1
99+
100+
101+
if __name__ == "__main__":
102+
uvicorn.run("main:scoped_request", port=5000, log_level="info")
103+
104+
```

docs/basics/lifespan.md

Lines changed: 108 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,108 @@
1+
# **Lifespan**
2+
Ellar applications registers a lifespan manager. The manager handles lifespan handler registered in the configuration under the variable name
3+
**`DEFAULT_LIFESPAN_HANDLER`**.
4+
5+
It also executes code that needs to run before the application starts up, or when the application is shutting down.
6+
The lifespan manager must be run before ellar starts serving incoming request.
7+
8+
```python
9+
import uvicorn
10+
import contextlib
11+
from ellar.core import App, AppFactory
12+
13+
@contextlib.asynccontextmanager
14+
async def some_async_resource():
15+
print("running some-async-resource function")
16+
yield
17+
print("existing some-async-resource function")
18+
19+
20+
@contextlib.asynccontextmanager
21+
async def lifespan(app: App):
22+
async with some_async_resource():
23+
print("Run at startup!")
24+
yield
25+
print("Run on shutdown!")
26+
27+
28+
application = AppFactory.create_app(config_module=dict(
29+
DEFAULT_LIFESPAN_HANDLER=lifespan
30+
))
31+
32+
if __name__ == "__main__":
33+
uvicorn.run(application, port=5000, log_level="info")
34+
```
35+
The construct above will generate the output below:
36+
```shell
37+
INFO: Started server process [11772]
38+
INFO: Waiting for application startup.
39+
INFO: Application startup complete.
40+
INFO: Uvicorn running on http://127.0.0.1:5000 (Press CTRL+C to quit)
41+
42+
running some-async-resource function
43+
Run at startup!
44+
45+
INFO: Shutting down
46+
INFO: Waiting for application shutdown.
47+
INFO: Application shutdown complete.
48+
INFO: Finished server process [11772]
49+
50+
Run on shutdown!
51+
existing some-async-resource function
52+
```
53+
54+
## **Modules and Lifespan**
55+
Any module that wants to engage in application lifespan must inherit `IApplicationStartup` for startup actions or `IApplicationShutdown` for shutdown actions
56+
or inherit both for startup and shutdown actions.
57+
58+
`IApplicationStartup` has an abstractmethod `on_startup` function and `IApplicationShutdown` has an abstractmethod `on_shutdown` function.
59+
60+
```python
61+
from abc import abstractmethod
62+
63+
64+
class IApplicationStartup:
65+
@abstractmethod
66+
async def on_startup(self, app: "App") -> None:
67+
...
68+
69+
70+
class IApplicationShutdown:
71+
@abstractmethod
72+
async def on_shutdown(self) -> None:
73+
...
74+
```
75+
Let's assume we have a module that extends both `IApplicationStartup` and `IApplicationShutdown` to execute some actions on startup and on shutdown as shown below:
76+
77+
```python
78+
from ellar.common import IApplicationShutdown, IApplicationStartup, Module
79+
80+
@Module()
81+
class SampleModule(IApplicationShutdown, IApplicationStartup):
82+
83+
async def on_startup(self, app) -> None:
84+
print("Run at startup! in SampleModule")
85+
86+
async def on_shutdown(self) -> None:
87+
print("Run on shutdown! in SampleModule")
88+
89+
```
90+
91+
## **Running lifespan in tests**
92+
You should use `TestClient` as a context manager, to ensure that the lifespan is called.
93+
94+
```python
95+
from ellar.testing import Test
96+
from .main import SampleModule
97+
98+
test_module = Test.create_test_module(modules=[SampleModule])
99+
100+
def test_lifespan():
101+
with test_module.get_test_client() as client:
102+
# Application's lifespan is called on entering the block.
103+
response = client.get("/")
104+
assert response.status_code == 200
105+
106+
# And the lifespan's teardown is run when exiting the block.
107+
108+
```

docs/basics/testing.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -290,7 +290,7 @@ To execute e2e tests, we adopt a similar configuration to that of unit testing,
290290
and Ellar's use of **TestClient**, a tool provided by Starlette, to facilitates the simulation of HTTP requests
291291

292292
### **TestClient**
293-
Starlette provides a [TestClient](https://www.starlette.io/testclient/) for making requests ASGI Applications, and it's based on [httpx](https://www.python-httpx.org/) library similar to requests.
293+
Starlette provides a [TestClient](https://www.starlette.io/testclient/){target="_blank"} for making requests ASGI Applications, and it's based on [httpx](https://www.python-httpx.org/) library similar to requests.
294294
```python
295295
from starlette.responses import HTMLResponse
296296
from starlette.testclient import TestClient
File renamed without changes.
File renamed without changes.

0 commit comments

Comments
 (0)