Skip to content

Commit af95ff4

Browse files
dmontaguDavid Montague
andauthored
Fix bug with multiple decorators on the same cbv endpoint (#18)
Co-authored-by: David Montague <[email protected]>
1 parent 42c6898 commit af95ff4

File tree

7 files changed

+85
-61
lines changed

7 files changed

+85
-61
lines changed

docs/release-notes.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,9 @@
11
## Latest changes
22

3+
## 0.2.1
4+
5+
* Fix bug with multiple decorators on same method
6+
37
## 0.2.0
48

59
* Make some of the functions/classes in `fastapi_utils.timing` private to clarify the intended public API

fastapi_utils/__init__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
__version__ = "0.2.0"
1+
__version__ = "0.2.1"

fastapi_utils/cbv.py

Lines changed: 10 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import inspect
2-
from typing import Any, Callable, List, Tuple, Type, TypeVar, Union, get_type_hints
2+
from typing import Any, Callable, List, Type, TypeVar, Union, get_type_hints
33

44
from fastapi import APIRouter, Depends
55
from pydantic.typing import is_classvar
@@ -35,25 +35,17 @@ def _cbv(router: APIRouter, cls: Type[T]) -> Type[T]:
3535
"""
3636
_init_cbv(cls)
3737
cbv_router = APIRouter()
38-
functions = inspect.getmembers(cls, inspect.isfunction)
39-
# Note inspect.getmembers returns results ordered alphabetically
40-
# Need to preserve ordering of routes in router to preserve matching logic
41-
numbered_routes_by_endpoint = {
42-
route.endpoint: (i, route)
43-
for i, route in enumerate(router.routes)
44-
if isinstance(route, (Route, WebSocketRoute))
45-
}
46-
routes_to_append: List[Tuple[int, Union[Route, WebSocketRoute]]] = []
47-
for _, func in functions:
48-
index_route = numbered_routes_by_endpoint.get(func)
49-
if index_route is None:
50-
continue
51-
_, route = index_route
52-
routes_to_append.append(index_route)
38+
function_members = inspect.getmembers(cls, inspect.isfunction)
39+
functions_set = set(func for _, func in function_members)
40+
cbv_routes = [
41+
route
42+
for route in router.routes
43+
if isinstance(route, (Route, WebSocketRoute)) and route.endpoint in functions_set
44+
]
45+
for route in cbv_routes:
5346
router.routes.remove(route)
5447
_update_cbv_route_endpoint_signature(cls, route)
55-
routes_to_append.sort(key=lambda x: x[0])
56-
cbv_router.routes.extend(route for _, route in routes_to_append)
48+
cbv_router.routes.append(route)
5749
router.include_router(cbv_router)
5850
return cls
5951

poetry.lock

Lines changed: 24 additions & 21 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

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-utils"
3-
version = "0.2.0"
3+
version = "0.2.1"
44
description = "Reusable utilities for FastAPI"
55
license = "MIT"
66
authors = ["David Montague <[email protected]>"]

requirements.txt

Lines changed: 22 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -74,9 +74,9 @@ dataclasses==0.6; python_version < "3.7" \
7474
entrypoints==0.3 \
7575
--hash=sha256:589f874b313739ad35be6e0cd7efde2a4e9b6fea91edcc34e58ecbb8dbe56d19 \
7676
--hash=sha256:c70dd71abe5a8c85e55e12c19bd91ccfeec11a6e99044204511f9ed547d48451
77-
fastapi==0.49.0 \
78-
--hash=sha256:717dbd2871c270970c70406ef4e550c3504525a7941df817f3a1318de0857c13 \
79-
--hash=sha256:c9296e05a011a53c5b4f0a12f06c261b95b7199685b3af986486e41a27545081
77+
fastapi==0.52.0 \
78+
--hash=sha256:532648b4e16dd33673d71dc0b35dff1b4d20c709d04078010e258b9f3a79771a \
79+
--hash=sha256:721b11d8ffde52c669f52741b6d9d761fe2e98778586f4cfd6f5e47254ba5016
8080
flake8==3.7.9 \
8181
--hash=sha256:49356e766643ad15072a789a20915d3c91dc89fd313ccd71802303fd67e4deca \
8282
--hash=sha256:45681a117ecc81e870cbf1262835ae4af5e7a8b08e40b944a8a6e6b895914cfb
@@ -172,9 +172,9 @@ mypy-extensions==0.4.3 \
172172
nltk==3.4.5 \
173173
--hash=sha256:a08bdb4b8a1c13de16743068d9eb61c8c71c2e5d642e8e08205c528035843f82 \
174174
--hash=sha256:bed45551259aa2101381bbdd5df37d44ca2669c5c3dad72439fa459b29137d94
175-
packaging==20.1 \
176-
--hash=sha256:170748228214b70b672c581a3dd610ee51f733018650740e98c7df862a583f73 \
177-
--hash=sha256:e665345f9eef0c621aa0bf2f8d78cf6d21904eef16a93f020240b704a57f1334
175+
packaging==20.3 \
176+
--hash=sha256:82f77b9bee21c1bafbf35a84905d604d5d1223801d639cf3ed140bd651c08752 \
177+
--hash=sha256:3c292b474fda1671ec57d46d739d072bfd495a4f51ad01a055121d81e952b7a3
178178
pathspec==0.7.0 \
179179
--hash=sha256:163b0632d4e31cef212976cf57b43d9fd6b0bac6e67c26015d611a647d5e7424 \
180180
--hash=sha256:562aa70af2e0d434367d9790ad37aed893de47f1693e4201fd1d3dca15d19b96
@@ -268,20 +268,23 @@ sqlalchemy==1.3.13 \
268268
sqlalchemy-stubs==0.3 \
269269
--hash=sha256:a3318c810697164e8c818aa2d90bac570c1a0e752ced3ec25455b309c0bee8fd \
270270
--hash=sha256:ca1250605a39648cc433f5c70cb1a6f9fe0b60bdda4c51e1f9a2ab3651daadc8
271-
starlette==0.12.9 \
272-
--hash=sha256:c2ac9a42e0e0328ad20fe444115ac5e3760c1ee2ac1ff8cdb5ec915c4a453411
271+
starlette==0.13.2 \
272+
--hash=sha256:6169ee78ded501095d1dda7b141a1dc9f9934d37ad23196e180150ace2c6449b \
273+
--hash=sha256:a9bb130fa7aa736eda8a814b6ceb85ccf7a209ed53843d0d61e246b380afa10f
273274
toml==0.10.0 \
274275
--hash=sha256:f1db651f9657708513243e61e6cc67d101a39bad662eaa9b5546f789338e07a3 \
275276
--hash=sha256:235682dd292d5899d361a811df37e04a8828a5b1da3115886b73cf81ebc9100e \
276277
--hash=sha256:229f81c57791a41d65e399fc06bf0848bab550a9dfd5ed66df18ce5f05e73d5c
277-
tornado==6.0.3 \
278-
--hash=sha256:c9399267c926a4e7c418baa5cbe91c7d1cf362d505a1ef898fde44a07c9dd8a5 \
279-
--hash=sha256:398e0d35e086ba38a0427c3b37f4337327231942e731edaa6e9fd1865bbd6f60 \
280-
--hash=sha256:4e73ef678b1a859f0cb29e1d895526a20ea64b5ffd510a2307b5998c7df24281 \
281-
--hash=sha256:349884248c36801afa19e342a77cc4458caca694b0eda633f5878e458a44cb2c \
282-
--hash=sha256:559bce3d31484b665259f50cd94c5c28b961b09315ccd838f284687245f416e5 \
283-
--hash=sha256:abbe53a39734ef4aba061fca54e30c6b4639d3e1f59653f0da37a0003de148c7 \
284-
--hash=sha256:c845db36ba616912074c5b1ee897f8e0124df269468f25e4fe21fe72f6edd7a9
278+
tornado==6.0.4 \
279+
--hash=sha256:5217e601700f24e966ddab689f90b7ea4bd91ff3357c3600fa1045e26d68e55d \
280+
--hash=sha256:c98232a3ac391f5faea6821b53db8db461157baa788f5d6222a193e9456e1740 \
281+
--hash=sha256:5f6a07e62e799be5d2330e68d808c8ac41d4a259b9cea61da4101b83cb5dc673 \
282+
--hash=sha256:c952975c8ba74f546ae6de2e226ab3cc3cc11ae47baf607459a6728585bb542a \
283+
--hash=sha256:2c027eb2a393d964b22b5c154d1a23a5f8727db6fda837118a776b29e2b8ebc6 \
284+
--hash=sha256:5618f72e947533832cbc3dec54e1dffc1747a5cb17d1fd91577ed14fa0dc081b \
285+
--hash=sha256:22aed82c2ea340c3771e3babc5ef220272f6fd06b5108a53b4976d0d722bcd52 \
286+
--hash=sha256:c58d56003daf1b616336781b26d184023ea4af13ae143d9dda65e31e534940b9 \
287+
--hash=sha256:0fe2d45ba43b00a41cd73f8be321a44936dc1aba233dee979f17a042b83eb6dc
285288
typed-ast==1.4.1 \
286289
--hash=sha256:73d785a950fc82dd2a25897d525d003f6378d1cb23ab305578394694202a58c3 \
287290
--hash=sha256:aaee9905aee35ba5905cfb3c62f3e83b3bec7b39413f0a7f19be4e547ea01ebb \
@@ -314,6 +317,6 @@ urllib3==1.25.8 \
314317
wcwidth==0.1.8 \
315318
--hash=sha256:8fd29383f539be45b20bd4df0dc29c20ba48654a41e661925e612311e9f3c603 \
316319
--hash=sha256:f28b3e8a6483e5d49e7f8949ac1a78314e740333ae305b4ba5defd3e74fb37a8
317-
zipp==3.0.0; python_version < "3.8" \
318-
--hash=sha256:12248a63bbdf7548f89cb4c7cda4681e537031eda29c02ea29674bc6854460c2 \
319-
--hash=sha256:7c0f8e91abc0dc07a5068f315c52cb30c66bfbc581e5b50704c8a2f6ebae794a
320+
zipp==3.1.0; python_version < "3.8" \
321+
--hash=sha256:aa36550ff0c0b7ef7fa639055d797116ee891440eac1a56f378e2d3179e0320b \
322+
--hash=sha256:c599e4d75c98f6798c509911d08a22e6c021d074469042177c8c86fb92eefd96

tests/test_cbv.py

Lines changed: 23 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
from typing import ClassVar
1+
from typing import Any, ClassVar
22

33
from fastapi import APIRouter, Depends, FastAPI
44
from starlette.testclient import TestClient
@@ -60,3 +60,25 @@ def get_item(self) -> int: # Alphabetically before `get_test`
6060

6161
assert TestClient(app).get("/test").json() == 1
6262
assert TestClient(app).get("/other").json() == 2
63+
64+
65+
def test_multiple_decorators() -> None:
66+
router = APIRouter()
67+
68+
@cbv(router)
69+
class RootHandler:
70+
@router.get("/items/?")
71+
@router.get("/items/{item_path:path}")
72+
@router.get("/database/{item_path:path}")
73+
def root(self, item_path: str = None, item_query: str = None) -> Any:
74+
if item_path:
75+
return {"item_path": item_path}
76+
if item_query:
77+
return {"item_query": item_query}
78+
return []
79+
80+
client = TestClient(router)
81+
82+
assert client.get("/items").json() == []
83+
assert client.get("/items/1").json() == {"item_path": "1"}
84+
assert client.get("/database/abc").json() == {"item_path": "abc"}

0 commit comments

Comments
 (0)