Skip to content

Commit 7fe0f76

Browse files
authored
Merge pull request #52 from eadwinCode/injectable_guards
Make Guards Injectable
2 parents 5668f65 + 9935446 commit 7fe0f76

33 files changed

+673
-345
lines changed

docs/basics/execution-context.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -236,3 +236,6 @@ class DogsController(ControllerBase):
236236
result.update(message='This action adds a new dog')
237237
return result
238238
```
239+
240+
!!! info
241+
It's important to note that `ExecutionContext` becomes available when there is route handler found to handle the current request.

docs/overview/providers.md

Lines changed: 65 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -128,6 +128,8 @@ There are different scope which defines different ways a service is instantiated
128128
Whenever a transient scoped provider is required, a new instance of the provider is created
129129

130130
```python
131+
# main.py
132+
131133
from ellar.di import EllarInjector, transient_scope, injectable
132134

133135
injector = EllarInjector(auto_bind=False)
@@ -141,17 +143,24 @@ injector.container.register(ATransientClass)
141143
# OR
142144
# injector.container.register_transient(ATransientClass)
143145

144-
a_transient_instance_1 = injector.get(ATransientClass)
145-
a_transient_instance_2 = injector.get(ATransientClass)
146+
def validate_transient_scope():
147+
a_transient_instance_1 = injector.get(ATransientClass)
148+
a_transient_instance_2 = injector.get(ATransientClass)
149+
150+
assert a_transient_instance_2 != a_transient_instance_1 # True
146151

147-
assert a_transient_instance_2 != a_transient_instance_1 # True
152+
153+
if __name__ == "__main__":
154+
validate_transient_scope()
148155
```
149156

150157
### **`singleton_scope`**:
151158
A singleton scoped provider is created once throughout the lifespan of the Container instance.
152159

153160
For example:
154161
```python
162+
# main.py
163+
155164
from ellar.di import EllarInjector, singleton_scope, injectable
156165

157166
injector = EllarInjector(auto_bind=False)
@@ -165,18 +174,25 @@ injector.container.register(ASingletonClass)
165174
# OR
166175
# injector.container.register_singleton(ASingletonClass)
167176

168-
a_singleton_instance_1 = injector.get(ASingletonClass)
169-
a_singleton_instance_2 = injector.get(ASingletonClass)
177+
def validate_singleton_scope():
178+
a_singleton_instance_1 = injector.get(ASingletonClass)
179+
a_singleton_instance_2 = injector.get(ASingletonClass)
180+
181+
assert a_singleton_instance_2 == a_singleton_instance_1 # True
170182

171-
assert a_singleton_instance_2 == a_singleton_instance_1 # True
183+
if __name__ == "__main__":
184+
validate_singleton_scope()
172185
```
173186

174187
### **`request_scope`**:
175188
A request scoped provider is instantiated once during the scope of the request. And it's destroyed once the request is complete.
176189
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.
177190

178191
```python
179-
from ellar.di import EllarInjector, request_scope, injectable, RequestServiceProvider
192+
# main.py
193+
194+
import uvicorn
195+
from ellar.di import EllarInjector, request_scope, injectable
180196

181197
injector = EllarInjector(auto_bind=False)
182198

@@ -187,16 +203,22 @@ class ARequestScopeClass:
187203

188204

189205
injector.container.register(ARequestScopeClass)
190-
request_injector = RequestServiceProvider(injector.container)
191206

192-
request_instance_1 = request_injector.get(ARequestScopeClass)
193-
request_instance_2 = request_injector.get(ARequestScopeClass)
194-
assert request_instance_2 == request_instance_1
195207

196-
request_instance_1 = injector.get(ARequestScopeClass)
197-
request_instance_2 = injector.get(ARequestScopeClass)
208+
async def scoped_request(scope, receive, send):
209+
async with injector.create_asgi_args(scope, receive, send) as request_injector:
210+
request_instance_1 = request_injector.get(ARequestScopeClass)
211+
request_instance_2 = request_injector.get(ARequestScopeClass)
212+
assert request_instance_2 == request_instance_1
213+
214+
request_instance_1 = injector.get(ARequestScopeClass)
215+
request_instance_2 = injector.get(ARequestScopeClass)
216+
217+
assert request_instance_2 != request_instance_1
218+
198219

199-
assert request_instance_2 != request_instance_1
220+
if __name__ == "__main__":
221+
uvicorn.run("main:scoped_request", port=5000, log_level="info")
200222

201223
```
202224

@@ -208,6 +230,8 @@ With `ProviderConfig`, we can register a `base_type` against a `concrete_type` O
208230

209231
For example:
210232
```python
233+
# main.py
234+
211235
from ellar.common import Module
212236
from ellar.core import ModuleBase, Config
213237
from ellar.di import ProviderConfig, injectable, EllarInjector
@@ -244,15 +268,20 @@ class AModule(ModuleBase):
244268
self.ifoo_b = ifoo_b
245269

246270

247-
module_ref = create_module_ref_factor(
271+
def validate_provider_config():
272+
module_ref = create_module_ref_factor(
248273
AModule, container=injector.container, config=Config(),
249-
)
250-
module_ref.run_module_register_services()
251-
a_module_instance: AModule = injector.get(AModule)
274+
)
275+
module_ref.run_module_register_services()
276+
a_module_instance: AModule = injector.get(AModule)
277+
278+
assert isinstance(a_module_instance.ifoo, AFooClass)
279+
assert isinstance(a_module_instance.ifoo_b, AFooClass)
280+
assert a_module_instance.ifoo_b == a_foo_instance
252281

253-
assert isinstance(a_module_instance.ifoo, AFooClass)
254-
assert isinstance(a_module_instance.ifoo_b, AFooClass)
255-
assert a_module_instance.ifoo_b == a_foo_instance
282+
283+
if __name__ == "__main__":
284+
validate_provider_config()
256285
```
257286
In above example, we used `ProviderConfig` as a value type as in the case of `IFooB` type and
258287
as a concrete type as in the case of `IFoo` type.
@@ -262,6 +291,8 @@ We can also achieve the same by overriding `register_providers` in any Module cl
262291

263292
For example:
264293
```python
294+
# main.py
295+
265296
from ellar.common import Module
266297
from ellar.core import ModuleBase, Config
267298
from ellar.di import Container, EllarInjector, injectable, ProviderConfig
@@ -293,15 +324,20 @@ class AModule(ModuleBase):
293324
container.register(IFooB, a_foo_instance)
294325

295326

296-
module_ref = create_module_ref_factor(
327+
def validate_register_services():
328+
module_ref = create_module_ref_factor(
297329
AModule, container=injector.container, config=Config(),
298-
)
299-
module_ref.run_module_register_services()
330+
)
331+
module_ref.run_module_register_services()
332+
333+
ifoo_b = injector.get(IFooB)
334+
ifoo = injector.get(IFoo)
335+
336+
assert isinstance(ifoo_b, AFooClass)
337+
assert isinstance(ifoo, AFooClass)
338+
assert ifoo_b == a_foo_instance
300339

301-
ifoo_b = injector.get(IFooB)
302-
ifoo = injector.get(IFoo)
340+
if __name__ == "__main__":
341+
validate_register_services()
303342

304-
assert isinstance(ifoo_b, AFooClass)
305-
assert isinstance(ifoo, AFooClass)
306-
assert ifoo_b == a_foo_instance
307343
```

ellar/asgi_args.py

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
import typing as t
2+
3+
from ellar.types import TReceive, TScope, TSend
4+
5+
if t.TYPE_CHECKING: # pragma: no cover
6+
from ellar.di.providers import Provider
7+
8+
9+
class ASGIArgs:
10+
__slots__ = ("scope", "receive", "send", "context")
11+
12+
def __init__(self, scope: TScope, receive: TReceive, send: TSend) -> None:
13+
self.scope = scope
14+
self.receive = receive
15+
self.send = send
16+
self.context: t.Dict[t.Type, "Provider"] = {}
17+
18+
def get_args(self) -> t.Tuple[TScope, TReceive, TSend]:
19+
return self.scope, self.receive, self.send

ellar/constants.py

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
1+
import contextvars
12
import logging
23
from enum import Enum
3-
from typing import Any, Dict, List, no_type_check
4+
from typing import Any, Dict, List, Optional, no_type_check
45

56
from pydantic.fields import (
67
SHAPE_LIST,
@@ -10,6 +11,13 @@
1011
SHAPE_TUPLE_ELLIPSIS,
1112
)
1213

14+
from .asgi_args import ASGIArgs
15+
16+
ASGI_CONTEXT_VAR: contextvars.ContextVar[Optional[ASGIArgs]] = contextvars.ContextVar(
17+
"ASGI-CONTEXT-VAR"
18+
)
19+
ASGI_CONTEXT_VAR.set(None)
20+
1321

1422
class _AnnotationToValue(type):
1523
keys: List[str]

ellar/core/connection/http.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -8,16 +8,16 @@
88
from ellar.constants import SCOPE_SERVICE_PROVIDER
99

1010
if t.TYPE_CHECKING: # pragma: no cover
11-
from ellar.di.injector import RequestServiceProvider
11+
from ellar.di import EllarInjector
1212

1313

1414
class HTTPConnection(StarletteHTTPConnection):
1515
@property
16-
def service_provider(self) -> "RequestServiceProvider":
16+
def service_provider(self) -> "EllarInjector":
1717
assert (
1818
SCOPE_SERVICE_PROVIDER in self.scope
1919
), "RequestServiceProviderMiddleware must be installed to access request.service_provider"
20-
return t.cast("RequestServiceProvider", self.scope[SCOPE_SERVICE_PROVIDER])
20+
return t.cast("EllarInjector", self.scope[SCOPE_SERVICE_PROVIDER])
2121

2222

2323
class Request(StarletteRequest, HTTPConnection):

ellar/core/context/__init__.py

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,12 @@
11
from .exceptions import HostContextException
22
from .execution import ExecutionContext
3+
from .factory import ExecutionContextFactory, HostContextFactory
34
from .host import HostContext
45
from .interface import (
56
IExecutionContext,
7+
IExecutionContextFactory,
68
IHostContext,
9+
IHostContextFactory,
710
IHTTPHostContext,
811
IWebSocketHostContext,
912
)
@@ -16,4 +19,8 @@
1619
"IWebSocketHostContext",
1720
"HostContext",
1821
"HostContextException",
22+
"IExecutionContextFactory",
23+
"IHostContextFactory",
24+
"ExecutionContextFactory",
25+
"HostContextFactory",
1926
]

ellar/core/context/execution.py

Lines changed: 1 addition & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -1,20 +1,16 @@
11
import typing as t
22

33
from ellar.compatible import cached_property
4-
from ellar.constants import CONTROLLER_CLASS_KEY, SCOPE_SERVICE_PROVIDER
4+
from ellar.constants import CONTROLLER_CLASS_KEY
55
from ellar.di import injectable
6-
from ellar.di.providers import ModuleProvider
76
from ellar.services.reflector import Reflector
87
from ellar.types import TReceive, TScope, TSend
98

10-
from .exceptions import ExecutionContextException
119
from .host import HostContext
1210
from .interface import IExecutionContext
1311

1412
if t.TYPE_CHECKING: # pragma: no cover
1513
from ellar.core.controller import ControllerBase
16-
from ellar.core.routing import RouteOperationBase
17-
from ellar.di.injector import RequestServiceProvider
1814

1915

2016
@injectable()
@@ -49,33 +45,3 @@ def _get_class(self) -> t.Optional[t.Type["ControllerBase"]]:
4945

5046
def get_class(self) -> t.Optional[t.Type["ControllerBase"]]:
5147
return self._get_class # type: ignore
52-
53-
@classmethod
54-
def create_context(
55-
cls,
56-
*,
57-
scope: TScope,
58-
receive: TReceive,
59-
send: TSend,
60-
operation: "RouteOperationBase",
61-
) -> IExecutionContext:
62-
provider = ModuleProvider(
63-
cls,
64-
scope=scope,
65-
receive=receive,
66-
send=send,
67-
operation_handler=operation.endpoint,
68-
)
69-
request_provider: "RequestServiceProvider" = t.cast(
70-
"RequestServiceProvider", scope.get(SCOPE_SERVICE_PROVIDER)
71-
)
72-
if not request_provider:
73-
raise ExecutionContextException(
74-
"RequestServiceProvider is not configured. "
75-
"Please ensure `RequestServiceProviderMiddleware` is registered."
76-
)
77-
78-
request_provider.update_context(IExecutionContext, provider)
79-
execution_context = request_provider.get(IExecutionContext) # type: ignore
80-
81-
return execution_context

0 commit comments

Comments
 (0)