Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions docs/release-notes.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,9 @@
## Latest changes

## 0.2.1

* Fix bug with multiple decorators on same method

## 0.2.0

* Make some of the functions/classes in `fastapi_utils.timing` private to clarify the intended public API
Expand Down
2 changes: 1 addition & 1 deletion fastapi_utils/__init__.py
Original file line number Diff line number Diff line change
@@ -1 +1 @@
__version__ = "0.2.0"
__version__ = "0.2.1"
28 changes: 10 additions & 18 deletions fastapi_utils/cbv.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import inspect
from typing import Any, Callable, List, Tuple, Type, TypeVar, Union, get_type_hints
from typing import Any, Callable, List, Type, TypeVar, Union, get_type_hints

from fastapi import APIRouter, Depends
from pydantic.typing import is_classvar
Expand Down Expand Up @@ -35,25 +35,17 @@ def _cbv(router: APIRouter, cls: Type[T]) -> Type[T]:
"""
_init_cbv(cls)
cbv_router = APIRouter()
functions = inspect.getmembers(cls, inspect.isfunction)
# Note inspect.getmembers returns results ordered alphabetically
# Need to preserve ordering of routes in router to preserve matching logic
numbered_routes_by_endpoint = {
route.endpoint: (i, route)
for i, route in enumerate(router.routes)
if isinstance(route, (Route, WebSocketRoute))
}
routes_to_append: List[Tuple[int, Union[Route, WebSocketRoute]]] = []
for _, func in functions:
index_route = numbered_routes_by_endpoint.get(func)
if index_route is None:
continue
_, route = index_route
routes_to_append.append(index_route)
function_members = inspect.getmembers(cls, inspect.isfunction)
functions_set = set(func for _, func in function_members)
cbv_routes = [
route
for route in router.routes
if isinstance(route, (Route, WebSocketRoute)) and route.endpoint in functions_set
]
for route in cbv_routes:
router.routes.remove(route)
_update_cbv_route_endpoint_signature(cls, route)
routes_to_append.sort(key=lambda x: x[0])
cbv_router.routes.extend(route for _, route in routes_to_append)
cbv_router.routes.append(route)
router.include_router(cbv_router)
return cls

Expand Down
45 changes: 24 additions & 21 deletions poetry.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[tool.poetry]
name = "fastapi-utils"
version = "0.2.0"
version = "0.2.1"
description = "Reusable utilities for FastAPI"
license = "MIT"
authors = ["David Montague <davwmont@gmail.com>"]
Expand Down
41 changes: 22 additions & 19 deletions requirements.txt
Original file line number Diff line number Diff line change
Expand Up @@ -74,9 +74,9 @@ dataclasses==0.6; python_version < "3.7" \
entrypoints==0.3 \
--hash=sha256:589f874b313739ad35be6e0cd7efde2a4e9b6fea91edcc34e58ecbb8dbe56d19 \
--hash=sha256:c70dd71abe5a8c85e55e12c19bd91ccfeec11a6e99044204511f9ed547d48451
fastapi==0.49.0 \
--hash=sha256:717dbd2871c270970c70406ef4e550c3504525a7941df817f3a1318de0857c13 \
--hash=sha256:c9296e05a011a53c5b4f0a12f06c261b95b7199685b3af986486e41a27545081
fastapi==0.52.0 \
--hash=sha256:532648b4e16dd33673d71dc0b35dff1b4d20c709d04078010e258b9f3a79771a \
--hash=sha256:721b11d8ffde52c669f52741b6d9d761fe2e98778586f4cfd6f5e47254ba5016
flake8==3.7.9 \
--hash=sha256:49356e766643ad15072a789a20915d3c91dc89fd313ccd71802303fd67e4deca \
--hash=sha256:45681a117ecc81e870cbf1262835ae4af5e7a8b08e40b944a8a6e6b895914cfb
Expand Down Expand Up @@ -172,9 +172,9 @@ mypy-extensions==0.4.3 \
nltk==3.4.5 \
--hash=sha256:a08bdb4b8a1c13de16743068d9eb61c8c71c2e5d642e8e08205c528035843f82 \
--hash=sha256:bed45551259aa2101381bbdd5df37d44ca2669c5c3dad72439fa459b29137d94
packaging==20.1 \
--hash=sha256:170748228214b70b672c581a3dd610ee51f733018650740e98c7df862a583f73 \
--hash=sha256:e665345f9eef0c621aa0bf2f8d78cf6d21904eef16a93f020240b704a57f1334
packaging==20.3 \
--hash=sha256:82f77b9bee21c1bafbf35a84905d604d5d1223801d639cf3ed140bd651c08752 \
--hash=sha256:3c292b474fda1671ec57d46d739d072bfd495a4f51ad01a055121d81e952b7a3
pathspec==0.7.0 \
--hash=sha256:163b0632d4e31cef212976cf57b43d9fd6b0bac6e67c26015d611a647d5e7424 \
--hash=sha256:562aa70af2e0d434367d9790ad37aed893de47f1693e4201fd1d3dca15d19b96
Expand Down Expand Up @@ -268,20 +268,23 @@ sqlalchemy==1.3.13 \
sqlalchemy-stubs==0.3 \
--hash=sha256:a3318c810697164e8c818aa2d90bac570c1a0e752ced3ec25455b309c0bee8fd \
--hash=sha256:ca1250605a39648cc433f5c70cb1a6f9fe0b60bdda4c51e1f9a2ab3651daadc8
starlette==0.12.9 \
--hash=sha256:c2ac9a42e0e0328ad20fe444115ac5e3760c1ee2ac1ff8cdb5ec915c4a453411
starlette==0.13.2 \
--hash=sha256:6169ee78ded501095d1dda7b141a1dc9f9934d37ad23196e180150ace2c6449b \
--hash=sha256:a9bb130fa7aa736eda8a814b6ceb85ccf7a209ed53843d0d61e246b380afa10f
toml==0.10.0 \
--hash=sha256:f1db651f9657708513243e61e6cc67d101a39bad662eaa9b5546f789338e07a3 \
--hash=sha256:235682dd292d5899d361a811df37e04a8828a5b1da3115886b73cf81ebc9100e \
--hash=sha256:229f81c57791a41d65e399fc06bf0848bab550a9dfd5ed66df18ce5f05e73d5c
tornado==6.0.3 \
--hash=sha256:c9399267c926a4e7c418baa5cbe91c7d1cf362d505a1ef898fde44a07c9dd8a5 \
--hash=sha256:398e0d35e086ba38a0427c3b37f4337327231942e731edaa6e9fd1865bbd6f60 \
--hash=sha256:4e73ef678b1a859f0cb29e1d895526a20ea64b5ffd510a2307b5998c7df24281 \
--hash=sha256:349884248c36801afa19e342a77cc4458caca694b0eda633f5878e458a44cb2c \
--hash=sha256:559bce3d31484b665259f50cd94c5c28b961b09315ccd838f284687245f416e5 \
--hash=sha256:abbe53a39734ef4aba061fca54e30c6b4639d3e1f59653f0da37a0003de148c7 \
--hash=sha256:c845db36ba616912074c5b1ee897f8e0124df269468f25e4fe21fe72f6edd7a9
tornado==6.0.4 \
--hash=sha256:5217e601700f24e966ddab689f90b7ea4bd91ff3357c3600fa1045e26d68e55d \
--hash=sha256:c98232a3ac391f5faea6821b53db8db461157baa788f5d6222a193e9456e1740 \
--hash=sha256:5f6a07e62e799be5d2330e68d808c8ac41d4a259b9cea61da4101b83cb5dc673 \
--hash=sha256:c952975c8ba74f546ae6de2e226ab3cc3cc11ae47baf607459a6728585bb542a \
--hash=sha256:2c027eb2a393d964b22b5c154d1a23a5f8727db6fda837118a776b29e2b8ebc6 \
--hash=sha256:5618f72e947533832cbc3dec54e1dffc1747a5cb17d1fd91577ed14fa0dc081b \
--hash=sha256:22aed82c2ea340c3771e3babc5ef220272f6fd06b5108a53b4976d0d722bcd52 \
--hash=sha256:c58d56003daf1b616336781b26d184023ea4af13ae143d9dda65e31e534940b9 \
--hash=sha256:0fe2d45ba43b00a41cd73f8be321a44936dc1aba233dee979f17a042b83eb6dc
typed-ast==1.4.1 \
--hash=sha256:73d785a950fc82dd2a25897d525d003f6378d1cb23ab305578394694202a58c3 \
--hash=sha256:aaee9905aee35ba5905cfb3c62f3e83b3bec7b39413f0a7f19be4e547ea01ebb \
Expand Down Expand Up @@ -314,6 +317,6 @@ urllib3==1.25.8 \
wcwidth==0.1.8 \
--hash=sha256:8fd29383f539be45b20bd4df0dc29c20ba48654a41e661925e612311e9f3c603 \
--hash=sha256:f28b3e8a6483e5d49e7f8949ac1a78314e740333ae305b4ba5defd3e74fb37a8
zipp==3.0.0; python_version < "3.8" \
--hash=sha256:12248a63bbdf7548f89cb4c7cda4681e537031eda29c02ea29674bc6854460c2 \
--hash=sha256:7c0f8e91abc0dc07a5068f315c52cb30c66bfbc581e5b50704c8a2f6ebae794a
zipp==3.1.0; python_version < "3.8" \
--hash=sha256:aa36550ff0c0b7ef7fa639055d797116ee891440eac1a56f378e2d3179e0320b \
--hash=sha256:c599e4d75c98f6798c509911d08a22e6c021d074469042177c8c86fb92eefd96
24 changes: 23 additions & 1 deletion tests/test_cbv.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
from typing import ClassVar
from typing import Any, ClassVar

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

assert TestClient(app).get("/test").json() == 1
assert TestClient(app).get("/other").json() == 2


def test_multiple_decorators() -> None:
router = APIRouter()

@cbv(router)
class RootHandler:
@router.get("/items/?")
@router.get("/items/{item_path:path}")
@router.get("/database/{item_path:path}")
def root(self, item_path: str = None, item_query: str = None) -> Any:
if item_path:
return {"item_path": item_path}
if item_query:
return {"item_query": item_query}
return []

client = TestClient(router)

assert client.get("/items").json() == []
assert client.get("/items/1").json() == {"item_path": "1"}
assert client.get("/database/abc").json() == {"item_path": "abc"}