Skip to content

Commit 8b4fb64

Browse files
committed
Switch to decorator-based route markers
1 parent aac809c commit 8b4fb64

File tree

8 files changed

+120
-93
lines changed

8 files changed

+120
-93
lines changed

fastapi_controllers/controllers.py

Lines changed: 33 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,26 @@
11
import inspect
2-
import weakref
32
from enum import Enum
43
from typing import Any, Dict, List, Optional, Sequence, Union
54

65
from fastapi import APIRouter, params
76

8-
from fastapi_controllers.definitions import HTTPRouteDefinition, RouteData, WebsocketRouteDefinition
7+
from fastapi_controllers.definitions import HTTPRouteMeta, Route, WebsocketRouteMeta
98
from fastapi_controllers.helpers import _replace_signature, _validate_against_signature
109

1110

11+
def _is_route(obj: Any) -> bool:
12+
"""
13+
Check if an object is an instance of Route.
14+
15+
Args:
16+
obj: The object to be checked.
17+
18+
Returns:
19+
True if the checked object is an instanc of Route.
20+
"""
21+
return isinstance(obj, Route)
22+
23+
1224
class Controller:
1325
prefix: str = ""
1426
dependencies: Optional[Sequence[params.Depends]] = None
@@ -21,7 +33,7 @@ def __init_subclass__(cls) -> None:
2133
for param in ["prefix", "dependencies", "tags"]:
2234
if not cls.__router_params__.get(param):
2335
cls.__router_params__[param] = getattr(cls, param)
24-
_validate_against_signature(weakref.proxy(APIRouter.__init__), kwargs=cls.__router_params__)
36+
_validate_against_signature(APIRouter.__init__, kwargs=cls.__router_params__)
2537

2638
@classmethod
2739
def create_router(cls) -> APIRouter:
@@ -31,24 +43,22 @@ def create_router(cls) -> APIRouter:
3143
Returns:
3244
APIRouter: An APIRouter instance.
3345
"""
34-
router = APIRouter(**cls.__router_params__) # type: ignore
35-
for _, func in inspect.getmembers(cls, predicate=inspect.isfunction):
36-
_replace_signature(cls, func)
37-
route_data: Optional[RouteData] = getattr(func, "__route_data__", None)
38-
if route_data:
39-
if isinstance(route_data.route_definition, HTTPRouteDefinition):
40-
router.add_api_route(
41-
route_data.route_args[0],
42-
func,
43-
*route_data.route_args[1:],
44-
methods=[route_data.route_definition.request_method],
45-
**route_data.route_kwargs,
46-
)
47-
if isinstance(route_data.route_definition, WebsocketRouteDefinition):
48-
router.add_api_websocket_route(
49-
route_data.route_args[0],
50-
func,
51-
*route_data.route_args[1:],
52-
**route_data.route_kwargs,
53-
)
46+
router = APIRouter(**(cls.__router_params__ or {}))
47+
for _, route in inspect.getmembers(cls, predicate=_is_route):
48+
_replace_signature(cls, route.endpoint)
49+
if isinstance(route.route_meta, HTTPRouteMeta):
50+
router.add_api_route(
51+
route.route_args[0],
52+
route.endpoint,
53+
*route.route_args[1:],
54+
methods=[route.route_meta.request_method],
55+
**route.route_kwargs,
56+
)
57+
if isinstance(route.route_meta, WebsocketRouteMeta):
58+
router.add_api_websocket_route(
59+
route.route_args[0],
60+
route.endpoint,
61+
*route.route_args[1:],
62+
**route.route_kwargs,
63+
)
5464
return router

fastapi_controllers/definitions.py

Lines changed: 20 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
1-
import weakref
21
from dataclasses import dataclass
32
from enum import Enum
4-
from typing import Any, Dict, Tuple
3+
from typing import Any, Callable, ClassVar, Dict, Tuple
54

65
from fastapi import APIRouter
76

@@ -17,36 +16,37 @@ class HTTPRequestMethod(str, Enum):
1716
TRACE = "TRACE"
1817

1918

20-
class RouteDefinition:
21-
def __init__(self, *, binds: weakref.CallableProxyType) -> None:
19+
class RouteMeta:
20+
def __init__(self, *, binds: Callable[..., Any]) -> None:
2221
self.binds = binds
2322

2423

25-
class HTTPRouteDefinition(RouteDefinition):
26-
def __init__(self, *, binds: weakref.CallableProxyType, request_method: HTTPRequestMethod) -> None:
24+
class HTTPRouteMeta(RouteMeta):
25+
def __init__(self, *, binds: Callable[..., Any], request_method: HTTPRequestMethod) -> None:
2726
super().__init__(binds=binds)
2827
self.request_method = request_method
2928

3029

31-
class WebsocketRouteDefinition(RouteDefinition):
30+
class WebsocketRouteMeta(RouteMeta):
3231
...
3332

3433

3534
@dataclass
36-
class RouteData:
37-
route_definition: RouteDefinition
38-
route_args: Tuple[Any, ...]
39-
route_kwargs: Dict[str, Any]
35+
class RouteMetadata:
36+
delete: ClassVar[RouteMeta] = HTTPRouteMeta(binds=APIRouter.delete, request_method=HTTPRequestMethod.DELETE)
37+
get: ClassVar[RouteMeta] = HTTPRouteMeta(binds=APIRouter.get, request_method=HTTPRequestMethod.GET)
38+
head: ClassVar[RouteMeta] = HTTPRouteMeta(binds=APIRouter.head, request_method=HTTPRequestMethod.HEAD)
39+
options: ClassVar[RouteMeta] = HTTPRouteMeta(binds=APIRouter.options, request_method=HTTPRequestMethod.OPTIONS)
40+
patch: ClassVar[RouteMeta] = HTTPRouteMeta(binds=APIRouter.patch, request_method=HTTPRequestMethod.PATCH)
41+
post: ClassVar[RouteMeta] = HTTPRouteMeta(binds=APIRouter.post, request_method=HTTPRequestMethod.POST)
42+
put: ClassVar[RouteMeta] = HTTPRouteMeta(binds=APIRouter.put, request_method=HTTPRequestMethod.PUT)
43+
trace: ClassVar[RouteMeta] = HTTPRouteMeta(binds=APIRouter.trace, request_method=HTTPRequestMethod.TRACE)
44+
websocket: ClassVar[RouteMeta] = WebsocketRouteMeta(binds=APIRouter.websocket)
4045

4146

4247
@dataclass
4348
class Route:
44-
delete = HTTPRouteDefinition(binds=weakref.proxy(APIRouter.delete), request_method=HTTPRequestMethod.DELETE)
45-
get = HTTPRouteDefinition(binds=weakref.proxy(APIRouter.get), request_method=HTTPRequestMethod.GET)
46-
head = HTTPRouteDefinition(binds=weakref.proxy(APIRouter.head), request_method=HTTPRequestMethod.HEAD)
47-
options = HTTPRouteDefinition(binds=weakref.proxy(APIRouter.options), request_method=HTTPRequestMethod.OPTIONS)
48-
patch = HTTPRouteDefinition(binds=weakref.proxy(APIRouter.patch), request_method=HTTPRequestMethod.PATCH)
49-
post = HTTPRouteDefinition(binds=weakref.proxy(APIRouter.post), request_method=HTTPRequestMethod.POST)
50-
put = HTTPRouteDefinition(binds=weakref.proxy(APIRouter.put), request_method=HTTPRequestMethod.PUT)
51-
trace = HTTPRouteDefinition(binds=weakref.proxy(APIRouter.trace), request_method=HTTPRequestMethod.TRACE)
52-
websocket = WebsocketRouteDefinition(binds=weakref.proxy(APIRouter.websocket))
49+
endpoint: Callable[..., Any]
50+
route_meta: RouteMeta
51+
route_args: Tuple[Any, ...]
52+
route_kwargs: Dict[str, Any]

fastapi_controllers/helpers.py

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,11 @@
11
import inspect
2-
import weakref
32
from typing import Any, Callable, Dict, Optional, Tuple, Type
43

54
from fastapi import Depends
65

76

87
def _validate_against_signature(
9-
method: weakref.CallableProxyType,
8+
method: Callable[..., Any],
109
args: Optional[Tuple[Any, ...]] = None,
1110
kwargs: Optional[Dict[str, Any]] = None,
1211
) -> None:
@@ -23,14 +22,21 @@ def _validate_against_signature(
2322
valid_sig.bind(*(args or tuple()), **(kwargs or {}))
2423

2524

26-
def _replace_signature(cls: Type, func: Callable[..., Any]) -> None:
25+
def _replace_signature(klass: Type, func: Callable[..., Any]) -> None:
26+
"""
27+
Replace the 'self' attribute with a FastAPI Depends injection.
28+
29+
Args:
30+
klass: The class that will be injected.
31+
func: The function whose signature will be replaced.
32+
"""
2733
orig_sig = inspect.signature(func)
2834
new_params = [
2935
param.replace(
3036
kind=inspect.Parameter.KEYWORD_ONLY,
3137
)
3238
if param.name != "self"
33-
else param.replace(default=Depends(cls))
39+
else param.replace(default=Depends(klass))
3440
for param in list(orig_sig.parameters.values())
3541
]
3642
func.__signature__ = orig_sig.replace(parameters=new_params) # type: ignore

fastapi_controllers/routing.py

Lines changed: 18 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -1,28 +1,28 @@
11
from typing import Any, Callable, Dict, Tuple
22

3-
from fastapi_controllers.definitions import Route, RouteData, RouteDefinition
3+
from fastapi_controllers.definitions import Route, RouteMeta, RouteMetadata
44
from fastapi_controllers.helpers import _validate_against_signature
55

66

77
class _RouteDecorator:
8-
_route_definition: RouteDefinition
8+
route_meta: RouteMeta
99

10-
def __init_subclass__(cls, route_definition: RouteDefinition) -> None:
10+
def __init_subclass__(cls, route_meta: RouteMeta) -> None:
1111
super().__init_subclass__()
12-
cls._route_definition = route_definition
12+
cls.route_meta = route_meta
1313

1414
def __init__(self, *args: Any, **kwargs: Any) -> None:
1515
self.route_args = args
1616
self.route_kwargs = kwargs
17-
_validate_against_signature(self._route_definition.binds, args=args, kwargs=kwargs)
17+
_validate_against_signature(self.route_meta.binds, args=args, kwargs=kwargs)
1818

19-
def __call__(self, func: Callable[..., Any]) -> Callable[..., Any]:
20-
func.__route_data__ = RouteData( # type: ignore
21-
route_definition=self._route_definition,
19+
def __call__(self, endpoint: Callable[..., Any]) -> Route:
20+
return Route(
21+
endpoint=endpoint,
22+
route_meta=self.route_meta,
2223
route_args=self.route_args,
2324
route_kwargs=self.route_kwargs,
2425
)
25-
return func
2626

2727
@property
2828
def route_args(self) -> Tuple[Any, ...]:
@@ -41,37 +41,37 @@ def route_kwargs(self, value: Dict[str, Any]) -> None:
4141
self._route_kwargs = value
4242

4343

44-
class delete(_RouteDecorator, route_definition=Route.delete):
44+
class delete(_RouteDecorator, route_meta=RouteMetadata.delete):
4545
...
4646

4747

48-
class get(_RouteDecorator, route_definition=Route.get):
48+
class get(_RouteDecorator, route_meta=RouteMetadata.get):
4949
...
5050

5151

52-
class head(_RouteDecorator, route_definition=Route.head):
52+
class head(_RouteDecorator, route_meta=RouteMetadata.head):
5353
...
5454

5555

56-
class options(_RouteDecorator, route_definition=Route.options):
56+
class options(_RouteDecorator, route_meta=RouteMetadata.options):
5757
...
5858

5959

60-
class patch(_RouteDecorator, route_definition=Route.patch):
60+
class patch(_RouteDecorator, route_meta=RouteMetadata.patch):
6161
...
6262

6363

64-
class post(_RouteDecorator, route_definition=Route.post):
64+
class post(_RouteDecorator, route_meta=RouteMetadata.post):
6565
...
6666

6767

68-
class put(_RouteDecorator, route_definition=Route.put):
68+
class put(_RouteDecorator, route_meta=RouteMetadata.put):
6969
...
7070

7171

72-
class trace(_RouteDecorator, route_definition=Route.trace):
72+
class trace(_RouteDecorator, route_meta=RouteMetadata.trace):
7373
...
7474

7575

76-
class websocket(_RouteDecorator, route_definition=Route.websocket):
76+
class websocket(_RouteDecorator, route_meta=RouteMetadata.websocket):
7777
...

pyproject.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
[tool.poetry]
22
name = "fastapi-controllers"
3-
version = "0.2.1"
3+
version = "0.3.0"
44
description = "Simple Controller implementation for FastAPI"
55
authors = ["Jerzy Góra <[email protected]>"]
66
license = "MIT"

tests/unit/test_controllers.py

Lines changed: 20 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,12 @@
1+
from typing import Any
12
from unittest.mock import MagicMock
23

34
import pytest
45
from fastapi import APIRouter
56
from pytest_mock import MockerFixture
67

7-
from fastapi_controllers.controllers import Controller
8+
from fastapi_controllers.controllers import Controller, _is_route
9+
from fastapi_controllers.definitions import Route
810
from fastapi_controllers.routing import get, websocket
911

1012

@@ -13,6 +15,18 @@ def validator(mocker: MockerFixture) -> MagicMock:
1315
return mocker.patch("fastapi_controllers.controllers._validate_against_signature")
1416

1517

18+
IS_ROUTE_SCENARIOS = [
19+
(Route(endpoint="TEST", route_meta="TEST", route_args="TEST", route_kwargs="TEST"), True), # type: ignore
20+
(type("NotRoute", (), {}), False),
21+
]
22+
23+
24+
def describe_is_route() -> None:
25+
@pytest.mark.parametrize("obj,expected", IS_ROUTE_SCENARIOS)
26+
def it_validates_if_obj_is_a_route(obj: Any, expected: bool) -> None:
27+
assert _is_route(obj) is expected
28+
29+
1630
def describe_Controller() -> None:
1731
def it_sets_router_params() -> None:
1832
class FakeController(Controller):
@@ -34,14 +48,13 @@ class FakeController(Controller):
3448

3549
def it_validates_apirouter_parameters(mocker: MockerFixture, validator: MagicMock) -> None:
3650

37-
mocked_proxy = mocker.patch("weakref.proxy", return_value="FAKE_PROXY")
51+
mocked_apirouter_init = mocker.patch("fastapi_controllers.controllers.APIRouter.__init__")
3852

3953
class _(Controller):
4054
...
4155

42-
mocked_proxy.assert_called_once_with(APIRouter.__init__)
4356
validator.assert_called_once_with(
44-
"FAKE_PROXY",
57+
mocked_apirouter_init,
4558
kwargs={
4659
"prefix": "",
4760
"dependencies": None,
@@ -72,7 +85,7 @@ def fake_method(self) -> None:
7285
...
7386

7487
FakeController.create_router()
75-
replace.assert_called_once_with(FakeController, FakeController.fake_method)
88+
replace.assert_called_once_with(FakeController, FakeController.fake_method.endpoint)
7689

7790
def it_configures_the_router_and_routes(mocker: MockerFixture) -> None:
7891
apirouter = mocker.patch("fastapi_controllers.controllers.APIRouter")
@@ -92,11 +105,11 @@ def fake_ws(self) -> None:
92105
apirouter.assert_called_once_with(prefix="/test", dependencies=None, tags=None)
93106
apirouter.return_value.add_api_route.assert_called_once_with(
94107
"/get",
95-
FakeController.fake_method,
108+
FakeController.fake_method.endpoint,
96109
deprecated=True,
97110
methods=["GET"],
98111
)
99112
apirouter.return_value.add_api_websocket_route.assert_called_once_with(
100113
"/ws",
101-
FakeController.fake_ws,
114+
FakeController.fake_ws.endpoint,
102115
)

0 commit comments

Comments
 (0)