Skip to content

Commit 5ef2b6c

Browse files
authored
Merge pull request #218 from python-ellar/openapi_mount_support
feat: Nested ModuleRouter support
2 parents d9ca03f + edf3d99 commit 5ef2b6c

File tree

22 files changed

+479
-121
lines changed

22 files changed

+479
-121
lines changed

ellar/common/constants.py

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

3434
TEMPLATE_GLOBAL_KEY = "TEMPLATE_GLOBAL_FILTERS"
3535
TEMPLATE_FILTER_KEY = "TEMPLATE_FILTERS"
36+
NESTED_ROUTERS_KEY = "NESTED_ROUTERS_KEY"
3637

3738
MIDDLEWARE_HANDLERS_KEY = "MIDDLEWARE"
3839

ellar/common/models/controller.py

Lines changed: 25 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,18 @@
1+
import dataclasses
12
import typing as t
23

3-
from ..interfaces import IExecutionContext
4+
from ellar.common.constants import NESTED_ROUTERS_KEY
5+
from ellar.common.interfaces import IExecutionContext
6+
from ellar.reflect import reflect
7+
8+
if t.TYPE_CHECKING:
9+
from ellar.common.operations import ModuleRouter
10+
11+
12+
@dataclasses.dataclass
13+
class NestedRouterInfo:
14+
router: t.Union["ModuleRouter", "ControllerBase"]
15+
prefix: t.Optional[str] = None
416

517

618
class ControllerType(type):
@@ -16,6 +28,18 @@ def full_view_name(cls, name: str) -> str:
1628
""" """
1729
return f"{cls.controller_class_name()}/{name}"
1830

31+
def add_router(
32+
cls,
33+
router: t.Union["ControllerBase", "ModuleRouter"],
34+
prefix: t.Optional[str] = None,
35+
) -> None:
36+
if prefix:
37+
assert prefix.startswith("/"), "'prefix' must start with '/'"
38+
39+
reflect.define_metadata(
40+
NESTED_ROUTERS_KEY, [NestedRouterInfo(prefix=prefix, router=router)], cls
41+
)
42+
1943

2044
class ControllerBase(metaclass=ControllerType):
2145
# `context` variable will change based on the route function called on the APIController

ellar/common/operations/router.py

Lines changed: 23 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,23 @@
11
import typing as t
22

3-
from ellar.common.constants import CONTROLLER_CLASS_KEY, GUARDS_KEY, VERSIONING_KEY
3+
from ellar.common.constants import (
4+
CONTROLLER_CLASS_KEY,
5+
GUARDS_KEY,
6+
NESTED_ROUTERS_KEY,
7+
VERSIONING_KEY,
8+
)
49
from ellar.common.models import GuardCanActivate
10+
from ellar.common.models.controller import NestedRouterInfo
511
from ellar.reflect import reflect
612
from ellar.utils import get_unique_type
713
from starlette.middleware import Middleware
814

915
from .base import OperationDefinitions
1016
from .schema import RouteParameters, WsRouteParameters
1117

18+
if t.TYPE_CHECKING: # pragma: no cover
19+
from ellar.common import ControllerBase
20+
1221

1322
class ModuleRouter(OperationDefinitions):
1423
def __init__(
@@ -40,6 +49,19 @@ def __init__(
4049
def control_type(self) -> t.Type[t.Any]:
4150
return self._control_type
4251

52+
def add_router(
53+
self,
54+
router: t.Union["ModuleRouter", "ControllerBase"],
55+
prefix: t.Optional[str] = None,
56+
) -> None:
57+
if prefix:
58+
assert prefix.startswith("/"), "'prefix' must start with '/'"
59+
reflect.define_metadata(
60+
NESTED_ROUTERS_KEY,
61+
[NestedRouterInfo(prefix=prefix, router=router)],
62+
self.control_type,
63+
)
64+
4365
def get_mount_init(self) -> t.Dict[str, t.Any]:
4466
return self._kwargs.copy()
4567

ellar/core/router_builders/base.py

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,11 +2,9 @@
22
from abc import abstractmethod
33

44
from ellar.common.logging import logger
5+
from ellar.core.routing.mount import EllarMount
56
from starlette.routing import Host, Mount
67

7-
if t.TYPE_CHECKING: # pragma: no cover
8-
from ellar.core.routing.mount import EllarMount
9-
108
_router_builder_factory: t.Dict[t.Type, t.Type["RouterBuilder"]] = {}
119

1210

@@ -66,3 +64,4 @@ def build(
6664

6765

6866
_register_controller_builder(Host, _DefaultRouterBuilder)
67+
_register_controller_builder(EllarMount, _DefaultRouterBuilder)

ellar/core/router_builders/controller.py

Lines changed: 48 additions & 57 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,15 @@
1-
import inspect
21
import typing as t
32

43
from ellar.common.constants import (
54
CONTROLLER_METADATA,
65
CONTROLLER_OPERATION_HANDLER_KEY,
76
CONTROLLER_WATERMARK,
8-
OPERATION_ENDPOINT_KEY,
97
ROUTE_OPERATION_PARAMETERS,
108
)
119
from ellar.common.logging import logger
12-
from ellar.common.models import ControllerBase, ControllerType
10+
from ellar.common.models.controller import ControllerBase, ControllerType
1311
from ellar.common.operations import RouteParameters, WsRouteParameters
12+
from ellar.common.shortcuts import normalize_path
1413
from ellar.core.routing import (
1514
ControllerRouteOperation,
1615
ControllerWebsocketRouteOperation,
@@ -20,72 +19,56 @@
2019
from starlette.routing import BaseRoute
2120

2221
from .base import RouterBuilder
22+
from .utils import get_route_functions, process_nested_routes
2323

2424
T_ = t.TypeVar("T_")
2525

2626

27-
class ControllerRouterBuilder(RouterBuilder, controller_type=type(ControllerBase)):
28-
@classmethod
29-
def _get_route_functions(
30-
cls,
31-
klass: t.Type,
32-
) -> t.Iterable[t.Callable]:
33-
for _method_name, method in inspect.getmembers(
34-
klass, predicate=inspect.isfunction
35-
):
36-
if hasattr(method, OPERATION_ENDPOINT_KEY):
37-
yield method
27+
def process_controller_routes(controller: t.Type[ControllerBase]) -> t.List[BaseRoute]:
28+
res: t.List[BaseRoute] = []
3829

39-
@classmethod
40-
def _process_controller_routes(
41-
cls, controller: t.Type[ControllerBase]
42-
) -> t.Sequence[BaseRoute]:
43-
res = []
44-
45-
if reflect.get_metadata(CONTROLLER_METADATA.PROCESSED, controller):
46-
return (
47-
reflect.get_metadata(CONTROLLER_OPERATION_HANDLER_KEY, controller) or []
48-
)
30+
if reflect.get_metadata(CONTROLLER_METADATA.PROCESSED, controller):
31+
return reflect.get_metadata(CONTROLLER_OPERATION_HANDLER_KEY, controller) or []
4932

50-
for item in cls._get_route_functions(controller):
51-
parameters = item.__dict__[ROUTE_OPERATION_PARAMETERS]
52-
operation: t.Union[
53-
ControllerRouteOperation, ControllerWebsocketRouteOperation
54-
]
55-
56-
if not isinstance(parameters, list):
57-
parameters = [parameters]
58-
59-
for parameter in parameters:
60-
if isinstance(parameter, RouteParameters):
61-
operation = ControllerRouteOperation(controller, **parameter.dict())
62-
elif isinstance(parameter, WsRouteParameters):
63-
operation = ControllerWebsocketRouteOperation(
64-
controller, **parameter.dict()
65-
)
66-
else: # pragma: no cover
67-
logger.warning(
68-
f"Parameter type is not recognized. {type(parameter) if not isinstance(parameter, type) else parameter}"
69-
)
70-
continue
71-
72-
reflect.define_metadata(
73-
CONTROLLER_OPERATION_HANDLER_KEY,
74-
[operation],
75-
controller,
33+
for _, item in get_route_functions(controller):
34+
parameters = item.__dict__[ROUTE_OPERATION_PARAMETERS]
35+
operation: t.Union[ControllerRouteOperation, ControllerWebsocketRouteOperation]
36+
37+
if not isinstance(parameters, list):
38+
parameters = [parameters]
39+
40+
for parameter in parameters:
41+
if isinstance(parameter, RouteParameters):
42+
operation = ControllerRouteOperation(controller, **parameter.dict())
43+
elif isinstance(parameter, WsRouteParameters):
44+
operation = ControllerWebsocketRouteOperation(
45+
controller, **parameter.dict()
7646
)
77-
res.append(operation)
78-
reflect.define_metadata(CONTROLLER_METADATA.PROCESSED, True, controller)
79-
return res
47+
else: # pragma: no cover
48+
logger.warning(
49+
f"Parameter type is not recognized. {type(parameter) if not isinstance(parameter, type) else parameter}"
50+
)
51+
continue
8052

53+
reflect.define_metadata(
54+
CONTROLLER_OPERATION_HANDLER_KEY,
55+
[operation],
56+
controller,
57+
)
58+
res.append(operation)
59+
return res
60+
61+
62+
class ControllerRouterBuilder(RouterBuilder, controller_type=type(ControllerBase)):
8163
@classmethod
8264
def build(
8365
cls,
8466
controller_type: t.Union[t.Type[ControllerBase], t.Any],
8567
base_route_type: t.Type[t.Union[EllarMount, T_]] = EllarMount,
8668
**kwargs: t.Any,
8769
) -> t.Union[T_, EllarMount]:
88-
routes = cls._process_controller_routes(controller_type)
70+
routes = process_controller_routes(controller_type)
71+
routes.extend(process_nested_routes(controller_type))
8972

9073
include_in_schema = reflect.get_metadata_or_raise_exception(
9174
CONTROLLER_METADATA.INCLUDE_IN_SCHEMA, controller_type
@@ -96,11 +79,18 @@ def build(
9679
)
9780

9881
kwargs.setdefault("middleware", middleware)
82+
83+
path = reflect.get_metadata_or_raise_exception(
84+
CONTROLLER_METADATA.PATH, controller_type
85+
)
86+
87+
if "prefix" in kwargs:
88+
prefix = kwargs.pop("prefix")
89+
path = normalize_path(f"{prefix}/{path}")
90+
9991
router = base_route_type( # type:ignore[call-arg]
10092
routes=routes,
101-
path=reflect.get_metadata_or_raise_exception(
102-
CONTROLLER_METADATA.PATH, controller_type
103-
),
93+
path=path,
10494
name=reflect.get_metadata_or_raise_exception(
10595
CONTROLLER_METADATA.NAME, controller_type
10696
),
@@ -110,6 +100,7 @@ def build(
110100
control_type=controller_type,
111101
**kwargs,
112102
)
103+
reflect.define_metadata(CONTROLLER_METADATA.PROCESSED, True, controller_type)
113104
return router
114105

115106
@classmethod

ellar/core/router_builders/module_router.py

Lines changed: 24 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,14 @@
11
import typing as t
22

33
from ellar.common import ModuleRouter
4-
from ellar.common.constants import CONTROLLER_OPERATION_HANDLER_KEY
4+
from ellar.common.compatible import AttributeDict
5+
from ellar.common.constants import CONTROLLER_METADATA, CONTROLLER_OPERATION_HANDLER_KEY
6+
from ellar.common.shortcuts import normalize_path
57
from ellar.core.routing import EllarMount
6-
from ellar.core.routing.utils import build_route_parameters
78
from ellar.reflect import reflect
89

910
from .base import RouterBuilder
11+
from .utils import build_route_parameters, process_nested_routes
1012

1113
T_ = t.TypeVar("T_")
1214

@@ -19,17 +21,31 @@ def build(
1921
base_route_type: t.Type[t.Union[EllarMount, T_]] = EllarMount,
2022
**kwargs: t.Any,
2123
) -> t.Union[T_, EllarMount]:
22-
if controller_type.get_pre_build_routes():
23-
routes = build_route_parameters(
24-
controller_type.get_pre_build_routes(), controller_type.control_type
25-
)
26-
else:
24+
if reflect.get_metadata(
25+
CONTROLLER_METADATA.PROCESSED, controller_type.control_type
26+
):
2727
routes = reflect.get_metadata(
2828
CONTROLLER_OPERATION_HANDLER_KEY, controller_type.control_type
2929
)
30+
else:
31+
routes = build_route_parameters(
32+
controller_type.get_pre_build_routes(), controller_type.control_type
33+
)
34+
routes.extend(process_nested_routes(controller_type.control_type))
35+
36+
init_kwargs = AttributeDict(controller_type.get_mount_init())
37+
38+
if "prefix" in kwargs:
39+
prefix = kwargs.pop("prefix")
40+
init_kwargs.path = normalize_path(f"{prefix}/{init_kwargs.path}")
3041

3142
controller_type.clear_pre_build_routes()
32-
return base_route_type(**controller_type.get_mount_init(), routes=routes) # type:ignore[call-arg]
43+
router = base_route_type(**init_kwargs, routes=routes) # type:ignore[call-arg]
44+
45+
reflect.define_metadata(
46+
CONTROLLER_METADATA.PROCESSED, True, controller_type.control_type
47+
)
48+
return router
3349

3450
@classmethod
3551
def check_type(cls, controller_type: t.Union[t.Type, t.Any]) -> None:

0 commit comments

Comments
 (0)