diff --git a/docs/release-notes.md b/docs/release-notes.md index 3fe149b0..70171365 100644 --- a/docs/release-notes.md +++ b/docs/release-notes.md @@ -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 diff --git a/fastapi_utils/__init__.py b/fastapi_utils/__init__.py index d3ec452c..3ced3581 100644 --- a/fastapi_utils/__init__.py +++ b/fastapi_utils/__init__.py @@ -1 +1 @@ -__version__ = "0.2.0" +__version__ = "0.2.1" diff --git a/fastapi_utils/cbv.py b/fastapi_utils/cbv.py index af6ed626..cd51323f 100644 --- a/fastapi_utils/cbv.py +++ b/fastapi_utils/cbv.py @@ -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 @@ -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 diff --git a/poetry.lock b/poetry.lock index b76d9b43..8ef126fa 100644 --- a/poetry.lock +++ b/poetry.lock @@ -165,17 +165,17 @@ description = "FastAPI framework, high performance, easy to learn, fast to code, name = "fastapi" optional = false python-versions = ">=3.6" -version = "0.49.0" +version = "0.52.0" [package.dependencies] pydantic = ">=0.32.2,<2.0.0" -starlette = "0.12.9" +starlette = "0.13.2" [package.extras] all = ["requests", "aiofiles", "jinja2", "python-multipart", "itsdangerous", "pyyaml", "graphene", "ujson", "email-validator", "uvicorn", "async-exit-stack", "async-generator"] dev = ["pyjwt", "passlib", "autoflake", "flake8", "uvicorn", "graphene"] doc = ["mkdocs", "mkdocs-material", "markdown-include"] -test = ["pytest (>=4.0.0)", "pytest-cov", "mypy", "black", "isort", "requests", "email-validator", "sqlalchemy", "peewee", "databases", "orjson", "async-exit-stack", "async-generator"] +test = ["pytest (>=4.0.0)", "pytest-cov", "mypy", "black", "isort", "requests", "email-validator", "sqlalchemy", "peewee", "databases", "orjson", "async-exit-stack", "async-generator", "python-multipart", "aiofiles", "ujson", "flask"] [[package]] category = "dev" @@ -414,7 +414,7 @@ description = "Core utilities for Python packages" name = "packaging" optional = false python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" -version = "20.1" +version = "20.3" [package.dependencies] pyparsing = ">=2.0.2" @@ -648,7 +648,7 @@ description = "The little ASGI library that shines." name = "starlette" optional = false python-versions = ">=3.6" -version = "0.12.9" +version = "0.13.2" [package.extras] full = ["aiofiles", "graphene", "itsdangerous", "jinja2", "python-multipart", "pyyaml", "requests", "ujson"] @@ -667,7 +667,7 @@ description = "Tornado is a Python web framework and asynchronous networking lib name = "tornado" optional = false python-versions = ">= 3.5" -version = "6.0.3" +version = "6.0.4" [[package]] category = "dev" @@ -713,7 +713,7 @@ marker = "python_version < \"3.8\"" name = "zipp" optional = false python-versions = ">=3.6" -version = "3.0.0" +version = "3.1.0" [package.extras] docs = ["sphinx", "jaraco.packaging (>=3.2)", "rst.linker (>=1.9)"] @@ -817,8 +817,8 @@ entrypoints = [ {file = "entrypoints-0.3.tar.gz", hash = "sha256:c70dd71abe5a8c85e55e12c19bd91ccfeec11a6e99044204511f9ed547d48451"}, ] fastapi = [ - {file = "fastapi-0.49.0-py3-none-any.whl", hash = "sha256:717dbd2871c270970c70406ef4e550c3504525a7941df817f3a1318de0857c13"}, - {file = "fastapi-0.49.0.tar.gz", hash = "sha256:c9296e05a011a53c5b4f0a12f06c261b95b7199685b3af986486e41a27545081"}, + {file = "fastapi-0.52.0-py3-none-any.whl", hash = "sha256:532648b4e16dd33673d71dc0b35dff1b4d20c709d04078010e258b9f3a79771a"}, + {file = "fastapi-0.52.0.tar.gz", hash = "sha256:721b11d8ffde52c669f52741b6d9d761fe2e98778586f4cfd6f5e47254ba5016"}, ] flake8 = [ {file = "flake8-3.7.9-py2.py3-none-any.whl", hash = "sha256:49356e766643ad15072a789a20915d3c91dc89fd313ccd71802303fd67e4deca"}, @@ -934,8 +934,8 @@ nltk = [ {file = "nltk-3.4.5.zip", hash = "sha256:bed45551259aa2101381bbdd5df37d44ca2669c5c3dad72439fa459b29137d94"}, ] packaging = [ - {file = "packaging-20.1-py2.py3-none-any.whl", hash = "sha256:170748228214b70b672c581a3dd610ee51f733018650740e98c7df862a583f73"}, - {file = "packaging-20.1.tar.gz", hash = "sha256:e665345f9eef0c621aa0bf2f8d78cf6d21904eef16a93f020240b704a57f1334"}, + {file = "packaging-20.3-py2.py3-none-any.whl", hash = "sha256:82f77b9bee21c1bafbf35a84905d604d5d1223801d639cf3ed140bd651c08752"}, + {file = "packaging-20.3.tar.gz", hash = "sha256:3c292b474fda1671ec57d46d739d072bfd495a4f51ad01a055121d81e952b7a3"}, ] pathspec = [ {file = "pathspec-0.7.0-py2.py3-none-any.whl", hash = "sha256:163b0632d4e31cef212976cf57b43d9fd6b0bac6e67c26015d611a647d5e7424"}, @@ -1049,7 +1049,8 @@ sqlalchemy-stubs = [ {file = "sqlalchemy_stubs-0.3-py3-none-any.whl", hash = "sha256:ca1250605a39648cc433f5c70cb1a6f9fe0b60bdda4c51e1f9a2ab3651daadc8"}, ] starlette = [ - {file = "starlette-0.12.9.tar.gz", hash = "sha256:c2ac9a42e0e0328ad20fe444115ac5e3760c1ee2ac1ff8cdb5ec915c4a453411"}, + {file = "starlette-0.13.2-py3-none-any.whl", hash = "sha256:6169ee78ded501095d1dda7b141a1dc9f9934d37ad23196e180150ace2c6449b"}, + {file = "starlette-0.13.2.tar.gz", hash = "sha256:a9bb130fa7aa736eda8a814b6ceb85ccf7a209ed53843d0d61e246b380afa10f"}, ] toml = [ {file = "toml-0.10.0-py2.7.egg", hash = "sha256:f1db651f9657708513243e61e6cc67d101a39bad662eaa9b5546f789338e07a3"}, @@ -1057,13 +1058,15 @@ toml = [ {file = "toml-0.10.0.tar.gz", hash = "sha256:229f81c57791a41d65e399fc06bf0848bab550a9dfd5ed66df18ce5f05e73d5c"}, ] tornado = [ - {file = "tornado-6.0.3-cp35-cp35m-win32.whl", hash = "sha256:c9399267c926a4e7c418baa5cbe91c7d1cf362d505a1ef898fde44a07c9dd8a5"}, - {file = "tornado-6.0.3-cp35-cp35m-win_amd64.whl", hash = "sha256:398e0d35e086ba38a0427c3b37f4337327231942e731edaa6e9fd1865bbd6f60"}, - {file = "tornado-6.0.3-cp36-cp36m-win32.whl", hash = "sha256:4e73ef678b1a859f0cb29e1d895526a20ea64b5ffd510a2307b5998c7df24281"}, - {file = "tornado-6.0.3-cp36-cp36m-win_amd64.whl", hash = "sha256:349884248c36801afa19e342a77cc4458caca694b0eda633f5878e458a44cb2c"}, - {file = "tornado-6.0.3-cp37-cp37m-win32.whl", hash = "sha256:559bce3d31484b665259f50cd94c5c28b961b09315ccd838f284687245f416e5"}, - {file = "tornado-6.0.3-cp37-cp37m-win_amd64.whl", hash = "sha256:abbe53a39734ef4aba061fca54e30c6b4639d3e1f59653f0da37a0003de148c7"}, - {file = "tornado-6.0.3.tar.gz", hash = "sha256:c845db36ba616912074c5b1ee897f8e0124df269468f25e4fe21fe72f6edd7a9"}, + {file = "tornado-6.0.4-cp35-cp35m-win32.whl", hash = "sha256:5217e601700f24e966ddab689f90b7ea4bd91ff3357c3600fa1045e26d68e55d"}, + {file = "tornado-6.0.4-cp35-cp35m-win_amd64.whl", hash = "sha256:c98232a3ac391f5faea6821b53db8db461157baa788f5d6222a193e9456e1740"}, + {file = "tornado-6.0.4-cp36-cp36m-win32.whl", hash = "sha256:5f6a07e62e799be5d2330e68d808c8ac41d4a259b9cea61da4101b83cb5dc673"}, + {file = "tornado-6.0.4-cp36-cp36m-win_amd64.whl", hash = "sha256:c952975c8ba74f546ae6de2e226ab3cc3cc11ae47baf607459a6728585bb542a"}, + {file = "tornado-6.0.4-cp37-cp37m-win32.whl", hash = "sha256:2c027eb2a393d964b22b5c154d1a23a5f8727db6fda837118a776b29e2b8ebc6"}, + {file = "tornado-6.0.4-cp37-cp37m-win_amd64.whl", hash = "sha256:5618f72e947533832cbc3dec54e1dffc1747a5cb17d1fd91577ed14fa0dc081b"}, + {file = "tornado-6.0.4-cp38-cp38-win32.whl", hash = "sha256:22aed82c2ea340c3771e3babc5ef220272f6fd06b5108a53b4976d0d722bcd52"}, + {file = "tornado-6.0.4-cp38-cp38-win_amd64.whl", hash = "sha256:c58d56003daf1b616336781b26d184023ea4af13ae143d9dda65e31e534940b9"}, + {file = "tornado-6.0.4.tar.gz", hash = "sha256:0fe2d45ba43b00a41cd73f8be321a44936dc1aba233dee979f17a042b83eb6dc"}, ] typed-ast = [ {file = "typed_ast-1.4.1-cp35-cp35m-manylinux1_i686.whl", hash = "sha256:73d785a950fc82dd2a25897d525d003f6378d1cb23ab305578394694202a58c3"}, @@ -1102,6 +1105,6 @@ wcwidth = [ {file = "wcwidth-0.1.8.tar.gz", hash = "sha256:f28b3e8a6483e5d49e7f8949ac1a78314e740333ae305b4ba5defd3e74fb37a8"}, ] zipp = [ - {file = "zipp-3.0.0-py3-none-any.whl", hash = "sha256:12248a63bbdf7548f89cb4c7cda4681e537031eda29c02ea29674bc6854460c2"}, - {file = "zipp-3.0.0.tar.gz", hash = "sha256:7c0f8e91abc0dc07a5068f315c52cb30c66bfbc581e5b50704c8a2f6ebae794a"}, + {file = "zipp-3.1.0-py3-none-any.whl", hash = "sha256:aa36550ff0c0b7ef7fa639055d797116ee891440eac1a56f378e2d3179e0320b"}, + {file = "zipp-3.1.0.tar.gz", hash = "sha256:c599e4d75c98f6798c509911d08a22e6c021d074469042177c8c86fb92eefd96"}, ] diff --git a/pyproject.toml b/pyproject.toml index 78d42467..750a4ceb 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -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 "] diff --git a/requirements.txt b/requirements.txt index 13d407e6..c0182e11 100644 --- a/requirements.txt +++ b/requirements.txt @@ -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 @@ -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 @@ -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 \ @@ -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 diff --git a/tests/test_cbv.py b/tests/test_cbv.py index cc7ce562..5fd296a2 100644 --- a/tests/test_cbv.py +++ b/tests/test_cbv.py @@ -1,4 +1,4 @@ -from typing import ClassVar +from typing import Any, ClassVar from fastapi import APIRouter, Depends, FastAPI from starlette.testclient import TestClient @@ -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"}