Skip to content

Commit f50cffc

Browse files
committed
Added support for injector to be nested, dropped controller class key and added middleware support to controllers
1 parent 7f883c3 commit f50cffc

File tree

9 files changed

+105
-100
lines changed

9 files changed

+105
-100
lines changed

ellar/app/factory.py

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -128,6 +128,7 @@ def _create_app(
128128
t.List[t.Union[t.Type["GuardCanActivate"], "GuardCanActivate"]]
129129
] = None,
130130
config_module: t.Union[str, t.Dict, None] = None,
131+
injector: t.Optional[EllarInjector] = None,
131132
) -> App:
132133
def _get_config_kwargs() -> t.Dict:
133134
if config_module is None:
@@ -144,7 +145,7 @@ def _get_config_kwargs() -> t.Dict:
144145

145146
config = Config(app_configured=True, **_get_config_kwargs())
146147

147-
injector = EllarInjector(auto_bind=config.INJECTOR_AUTO_BIND)
148+
injector = EllarInjector(auto_bind=config.INJECTOR_AUTO_BIND, parent=injector)
148149
injector.container.register_instance(config, concrete_type=Config)
149150

150151
service = EllarAppService(injector, config)
@@ -210,6 +211,7 @@ def create_app(
210211
] = None,
211212
commands: t.Sequence[t.Union[click.Command, click.Group, t.Any]] = (),
212213
config_module: t.Union[str, t.Dict, None] = None,
214+
injector: t.Optional[EllarInjector] = None,
213215
) -> App:
214216
module = Module(
215217
controllers=controllers,
@@ -227,6 +229,7 @@ def create_app(
227229
module=app_factory_module,
228230
config_module=config_module,
229231
global_guards=global_guards,
232+
injector=injector,
230233
)
231234
return t.cast(App, app)
232235

@@ -237,10 +240,14 @@ def create_from_app_module(
237240
global_guards: t.Optional[
238241
t.List[t.Union[t.Type["GuardCanActivate"], "GuardCanActivate"]]
239242
] = None,
243+
injector: t.Optional[EllarInjector] = None,
240244
config_module: t.Union[str, t.Dict, None] = None,
241245
) -> App:
242246
app = cls._create_app(
243-
module, config_module=config_module, global_guards=global_guards
247+
module,
248+
config_module=config_module,
249+
global_guards=global_guards,
250+
injector=injector,
244251
)
245252

246253
return t.cast(App, app)

ellar/common/constants.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -72,6 +72,7 @@ class CONTROLLER_METADATA(metaclass=AnnotationToValue):
7272
NAME: str
7373
INCLUDE_IN_SCHEMA: str
7474
PROCESSED: str
75+
MIDDLEWARE: str
7576

7677

7778
sequence_types = (list, set, tuple)

ellar/common/decorators/controller.py

Lines changed: 31 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -10,8 +10,11 @@
1010
from ellar.common.models import ControllerBase, ControllerType
1111
from ellar.di import RequestORTransientScope, injectable
1212
from ellar.reflect import REFLECT_TYPE, reflect
13-
from ellar.utils import get_type_of_base
1413
from injector import Scope
14+
from starlette.middleware import Middleware
15+
16+
if t.TYPE_CHECKING:
17+
from ellar.core.middleware.middleware import EllarMiddleware
1518

1619

1720
@t.no_type_check
@@ -20,6 +23,7 @@ def Controller(
2023
*,
2124
name: t.Optional[str] = None,
2225
include_in_schema: bool = True,
26+
middleware: t.Optional[t.Sequence[t.Union[Middleware, "EllarMiddleware"]]] = None,
2327
scope: t.Optional[t.Union[t.Type[Scope], Scope]] = RequestORTransientScope,
2428
) -> t.Union[t.Type[ControllerBase], t.Callable[..., t.Any], t.Any]:
2529
"""
@@ -30,6 +34,7 @@ def Controller(
3034
:param name: route name prefix for url reversing, eg name:route_name default=''
3135
:param include_in_schema: include controller in OPENAPI schema
3236
:param scope: Controller Instance Lifetime scope
37+
:param middleware: Controller Middlewares
3338
:return: t.Type[ControllerBase]
3439
"""
3540
_prefix: t.Optional[t.Any] = prefix if prefix is not None else NOT_SET
@@ -42,7 +47,11 @@ def Controller(
4247
), "Controller Prefix must start with '/'"
4348
# TODO: replace with a ControllerTypeDict and OpenAPITypeDict
4449
kwargs = AttributeDict(
45-
path=_prefix, name=name, include_in_schema=include_in_schema, processed=False
50+
path=_prefix,
51+
name=name,
52+
include_in_schema=include_in_schema,
53+
processed=False,
54+
middleware=middleware,
4655
)
4756

4857
def _decorator(cls: t.Type) -> t.Type[ControllerBase]:
@@ -73,25 +82,26 @@ def _decorator(cls: t.Type) -> t.Type[ControllerBase]:
7382
.replace("controller", "")
7483
)
7584

76-
for base in get_type_of_base(ControllerBase, _controller_type):
77-
if reflect.has_metadata(CONTROLLER_WATERMARK, base) and hasattr(
78-
cls, "__CONTROLLER_WATERMARK__"
79-
):
80-
raise ImproperConfiguration(
81-
f"`@Controller` decorated classes does not support inheritance. \n"
82-
f"{_controller_type}"
83-
)
84-
85-
if not reflect.has_metadata(
86-
CONTROLLER_WATERMARK, _controller_type
87-
) and not hasattr(cls, "__CONTROLLER_WATERMARK__"):
88-
reflect.define_metadata(CONTROLLER_WATERMARK, True, _controller_type)
89-
# reflect_all_controller_type_routes(_controller_type)
90-
91-
injectable(scope or RequestORTransientScope)(cls)
92-
93-
for key in CONTROLLER_METADATA.keys:
94-
reflect.define_metadata(key, kwargs[key], _controller_type)
85+
# for base in get_type_of_base(ControllerBase, _controller_type):
86+
# if reflect.has_metadata(CONTROLLER_WATERMARK, base) and hasattr(
87+
# cls, "__CONTROLLER_WATERMARK__"
88+
# ):
89+
# raise ImproperConfiguration(
90+
# f"`@Controller` decorated classes does not support inheritance. \n"
91+
# f"{_controller_type}"
92+
# )
93+
94+
# if not reflect.has_metadata(
95+
# CONTROLLER_WATERMARK, _controller_type
96+
# ) and not hasattr(cls, "__CONTROLLER_WATERMARK__"):
97+
98+
reflect.define_metadata(CONTROLLER_WATERMARK, True, _controller_type)
99+
# reflect_all_controller_type_routes(_controller_type)
100+
101+
injectable(scope or RequestORTransientScope)(cls)
102+
103+
for key in CONTROLLER_METADATA.keys:
104+
reflect.define_metadata(key, kwargs[key], _controller_type)
95105

96106
if new_cls:
97107
# if we forced cls to inherit from ControllerBase, we need to block it from been processed

ellar/core/router_builders/controller.py

Lines changed: 37 additions & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,7 @@
11
import inspect
22
import typing as t
3-
from abc import ABC
43

54
from ellar.common.constants import (
6-
CONTROLLER_CLASS_KEY,
75
CONTROLLER_METADATA,
86
CONTROLLER_OPERATION_HANDLER_KEY,
97
CONTROLLER_WATERMARK,
@@ -31,63 +29,51 @@ def _get_route_functions(
3129
cls,
3230
klass: t.Type,
3331
) -> t.Iterable[t.Callable]:
34-
for method in klass.__dict__.values():
32+
for _method_name, method in inspect.getmembers(
33+
klass, predicate=inspect.isfunction
34+
):
3535
if hasattr(method, OPERATION_ENDPOINT_KEY):
3636
yield method
3737

3838
@classmethod
3939
def _process_controller_routes(
4040
cls, controller: t.Type[ControllerBase]
4141
) -> t.Sequence[BaseRoute]:
42-
bases = inspect.getmro(controller)
4342
res = []
4443

4544
if reflect.get_metadata(CONTROLLER_METADATA.PROCESSED, controller):
4645
return (
4746
reflect.get_metadata(CONTROLLER_OPERATION_HANDLER_KEY, controller) or []
4847
)
4948

50-
for base_cls in reversed(bases):
51-
if base_cls not in [ABC, ControllerBase, object]:
52-
for item in cls._get_route_functions(base_cls):
53-
if reflect.has_metadata(CONTROLLER_CLASS_KEY, item):
54-
raise Exception(
55-
f"{controller.__name__} Controller route tried to be processed more than once."
56-
f"\n-RouteFunction - {item}."
57-
f"\n-Controller route function can not be reused once its under a `@Controller` decorator."
58-
)
59-
60-
reflect.define_metadata(CONTROLLER_CLASS_KEY, controller, item)
61-
62-
parameters = item.__dict__[ROUTE_OPERATION_PARAMETERS]
63-
operation: t.Union[
64-
ControllerRouteOperation, ControllerWebsocketRouteOperation
65-
]
66-
67-
if not isinstance(parameters, list):
68-
parameters = [parameters]
69-
70-
for parameter in parameters:
71-
if isinstance(parameter, RouteParameters):
72-
operation = ControllerRouteOperation(**parameter.dict())
73-
elif isinstance(parameter, WsRouteParameters):
74-
operation = ControllerWebsocketRouteOperation(
75-
**parameter.dict()
76-
)
77-
else: # pragma: no cover
78-
logger.warning(
79-
f"Parameter type is not recognized. {type(parameter) if not isinstance(parameter, type) else parameter}"
80-
)
81-
continue
82-
83-
reflect.define_metadata(
84-
CONTROLLER_OPERATION_HANDLER_KEY,
85-
[operation],
86-
controller,
87-
)
88-
res.append(operation)
89-
90-
del item.__dict__[ROUTE_OPERATION_PARAMETERS]
49+
for item in cls._get_route_functions(controller):
50+
parameters = item.__dict__[ROUTE_OPERATION_PARAMETERS]
51+
operation: t.Union[
52+
ControllerRouteOperation, ControllerWebsocketRouteOperation
53+
]
54+
55+
if not isinstance(parameters, list):
56+
parameters = [parameters]
57+
58+
for parameter in parameters:
59+
if isinstance(parameter, RouteParameters):
60+
operation = ControllerRouteOperation(controller, **parameter.dict())
61+
elif isinstance(parameter, WsRouteParameters):
62+
operation = ControllerWebsocketRouteOperation(
63+
controller, **parameter.dict()
64+
)
65+
else: # pragma: no cover
66+
logger.warning(
67+
f"Parameter type is not recognized. {type(parameter) if not isinstance(parameter, type) else parameter}"
68+
)
69+
continue
70+
71+
reflect.define_metadata(
72+
CONTROLLER_OPERATION_HANDLER_KEY,
73+
[operation],
74+
controller,
75+
)
76+
res.append(operation)
9177
reflect.define_metadata(CONTROLLER_METADATA.PROCESSED, True, controller)
9278
return res
9379

@@ -103,6 +89,12 @@ def build(
10389
include_in_schema = reflect.get_metadata_or_raise_exception(
10490
CONTROLLER_METADATA.INCLUDE_IN_SCHEMA, controller_type
10591
)
92+
93+
middleware = reflect.get_metadata(
94+
CONTROLLER_METADATA.MIDDLEWARE, controller_type
95+
)
96+
97+
kwargs.setdefault("middleware", middleware)
10698
router = EllarMount(
10799
app=app,
108100
path=reflect.get_metadata_or_raise_exception(

ellar/core/routing/base.py

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -31,14 +31,13 @@
3131

3232
class RouteOperationBase:
3333
methods: t.Set[str]
34+
_controller_type: t.Optional[t.Union[t.Type, t.Type["ControllerBase"]]] = None
3435

3536
def __init__(self, endpoint: t.Callable) -> None:
3637
self.endpoint = endpoint
37-
38-
self._controller_type = None
3938
_controller_type: t.Type = self.get_controller_type()
4039

41-
self._controller_type: t.Union[t.Type, t.Type["ControllerBase"]] = t.cast( # type:ignore[no-redef]
40+
self._controller_type: t.Union[t.Type, t.Type["ControllerBase"]] = t.cast(
4241
t.Union[t.Type, t.Type["ControllerBase"]], _controller_type
4342
)
4443

@@ -86,7 +85,7 @@ def get_controller_type(self) -> t.Type:
8685
_controller_type = reflect.get_metadata(CONTROLLER_CLASS_KEY, self.endpoint)
8786
if _controller_type is None or not isinstance(_controller_type, type):
8887
raise Exception("Operation must have a single control type.")
89-
self._controller_type = t.cast(t.Type, _controller_type)
88+
return t.cast(t.Type, _controller_type)
9089

9190
return self._controller_type
9291

ellar/core/routing/controller/base.py

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,12 +6,20 @@
66

77

88
class ControllerRouteOperationBase:
9+
_controller_type: t.Optional[t.Type["ControllerBase"]] = None
910
endpoint: t.Callable
1011
get_controller_type: t.Callable
1112

13+
def __init__(
14+
self, controller_type: t.Type[ControllerBase], *args: t.Any, **kwargs: t.Any
15+
) -> None:
16+
self._controller_type = controller_type
17+
18+
super().__init__(*args, **kwargs)
19+
1220
def _get_controller_instance(self, ctx: IExecutionContext) -> ControllerBase:
1321
request_logger.debug("Getting Controller Instance")
14-
controller_type: t.Optional[t.Type[ControllerBase]] = self.get_controller_type()
22+
controller_type: t.Type[ControllerBase] = self.get_controller_type()
1523

1624
service_provider = ctx.get_service_provider()
1725

ellar/openapi/route_doc_models.py

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,6 @@
33

44
from ellar.common.compatible import cached_property
55
from ellar.common.constants import (
6-
CONTROLLER_CLASS_KEY,
76
GUARDS_KEY,
87
METHODS_WITH_BODY,
98
REF_PREFIX,
@@ -246,7 +245,7 @@ def get_openapi_operation_metadata(self, method: str) -> t.Dict[str, t.Any]:
246245
methods=method,
247246
controller=None
248247
if ignore_controller
249-
else reflector.get(CONTROLLER_CLASS_KEY, self.route.endpoint),
248+
else self.route.get_controller_type(),
250249
)
251250
)
252251
if self.deprecated:

tests/test_controller/test_controller_decorator.py

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
import pytest
22
from ellar.common import Controller, ControllerBase, get, http_route, ws_route
33
from ellar.common.constants import (
4-
CONTROLLER_CLASS_KEY,
54
CONTROLLER_OPERATION_HANDLER_KEY,
65
)
76
from ellar.core.router_builders import ControllerRouterBuilder
@@ -52,7 +51,7 @@ def test_controller_routes_has_controller_type():
5251
assert routes
5352

5453
for route in routes:
55-
controller_type = reflect.get_metadata(CONTROLLER_CLASS_KEY, route.endpoint)
54+
controller_type = route.get_controller_type()
5655
assert controller_type == SampleController
5756

5857

0 commit comments

Comments
 (0)