dmontagu / fastapi-utils

Reusable utilities for FastAPI

Geek Repo:Geek Repo

Github PK Tool:Github PK Tool

[BUG]/[FEATURE] multiple path's on the same method results in errors

falkben opened this issue · comments

Not quite clear to me if this is a bug or a feature request as I may be doing something out of the ordinary.

Using class based views, my tests return errors using methods (inside cbv) that have multiple path decorators attached to them. When using vanilla FastAPI without cbv the tests pass.

To Reproduce

Credit to @smparekh for the demo below, which I modified slightly to show the issue.

Simple main.py app:

from fastapi import FastAPI, APIRouter

from fastapi_utils.cbv import cbv

router = APIRouter()


fake_items_db = [{"item_name": "Foo"}, {"item_name": "Bar"}, {"item_name": "Baz"}]


@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):
        if item_path:
            return {"item_path": item_path}
        elif item_query:
            return {"item_query": item_query}
        else:
            return fake_items_db


app = FastAPI()
app.include_router(router)

simple test_main.py

from .main import router


from starlette.testclient import TestClient
from .main import fake_items_db

client = TestClient(router)


def test_item_path():
    resp = client.get("items/Bar")
    assert resp.status_code == 200
    assert resp.json() == {"item_path": "Bar"}


def test_item_query():
    resp = client.get("items/?item_query=Bar")
    assert resp.status_code == 200
    assert resp.json() == {"item_query": "Bar"}


def test_list():
    resp = client.get("items/")
    assert resp.status_code == 200
    assert resp.json() == fake_items_db


def test_database():
    resp = client.get("database/")
    assert resp.status_code == 200
    assert resp.json() == fake_items_db

traceback

traceback from the first test (others are the same)

=================================== FAILURES ===================================
________________________________ test_item_path ________________________________

    def test_item_path():
>       resp = client.get("items/Bar")

app/test_main_mult_decorators.py:11: 
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ 
../../../miniconda3/envs/web-server-eval/lib/python3.8/site-packages/requests/sessions.py:546: in get
    return self.request('GET', url, **kwargs)
../../../miniconda3/envs/web-server-eval/lib/python3.8/site-packages/starlette/testclient.py:413: in request
    return super().request(
../../../miniconda3/envs/web-server-eval/lib/python3.8/site-packages/requests/sessions.py:533: in request
    resp = self.send(prep, **send_kwargs)
../../../miniconda3/envs/web-server-eval/lib/python3.8/site-packages/requests/sessions.py:646: in send
    r = adapter.send(request, **kwargs)
../../../miniconda3/envs/web-server-eval/lib/python3.8/site-packages/starlette/testclient.py:243: in send
    raise exc from None
../../../miniconda3/envs/web-server-eval/lib/python3.8/site-packages/starlette/testclient.py:240: in send
    loop.run_until_complete(self.app(scope, receive, send))
../../../miniconda3/envs/web-server-eval/lib/python3.8/asyncio/base_events.py:612: in run_until_complete
    return future.result()
../../../miniconda3/envs/web-server-eval/lib/python3.8/site-packages/starlette/routing.py:550: in __call__
    await route.handle(scope, receive, send)
../../../miniconda3/envs/web-server-eval/lib/python3.8/site-packages/starlette/routing.py:227: in handle
    await self.app(scope, receive, send)
../../../miniconda3/envs/web-server-eval/lib/python3.8/site-packages/starlette/routing.py:41: in app
    response = await func(request)
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ 

request = <starlette.requests.Request object at 0x118cd3670>

    async def app(request: Request) -> Response:
        try:
            body = None
            if body_field:
                if is_body_form:
                    body = await request.form()
                else:
                    body_bytes = await request.body()
                    if body_bytes:
                        body = await request.json()
        except Exception as e:
            logger.error(f"Error getting request body: {e}")
            raise HTTPException(
                status_code=400, detail="There was an error parsing the body"
            ) from e
        solved_result = await solve_dependencies(
            request=request,
            dependant=dependant,
            body=body,
            dependency_overrides_provider=dependency_overrides_provider,
        )
        values, errors, background_tasks, sub_response, _ = solved_result
        if errors:
>           raise RequestValidationError(errors, body=body)
E           fastapi.exceptions.RequestValidationError: 1 validation error for Request
E           query -> self
E             field required (type=value_error.missing)

../../../miniconda3/envs/web-server-eval/lib/python3.8/site-packages/fastapi/routing.py:145: RequestValidationError

Expected behavior
Ideally tests work as in vanilla FastAPI without cbv.

Environment:

  • OS: tested on macOS and Linux
>>> import fastapi_utils
>>> import fastapi
>>> import pydantic.utils
>>> import pytest
>>>
>>> print(fastapi_utils.__version__)
0.2.0
>>> print(fastapi.__version__)
0.52.0
>>> print(pydantic.utils.version_info())
             pydantic version: 1.4
            pydantic compiled: False
                 install path: /Users/bfalk/miniconda3/envs/web-server-eval/lib/python3.8/site-packages/pydantic
               python version: 3.8.1 (default, Jan  8 2020, 16:15:59)  [Clang 4.0.1 (tags/RELEASE_401/final)]
                     platform: macOS-10.14.6-x86_64-i386-64bit
     optional deps. installed: ['typing-extensions']
>>> print(pytest.__version__)
5.3.5

Yeah, I think the approach currently in use might not play nice with using multiple decorators on the same method. I didn’t expect that to be a common use case but since someone wants to use it I’m happy to try to make it work.

PR definitely welcome if you want to get your hands dirty but I’ll take a look at this as soon as I can.

Okay yeah I see the problem and it should be an easy fix. I can’t promise, but I should be able to release 0.2.1 with a fix for this some time tomorrow evening.

Just pushed v0.2.1 to PyPI with this fix. Let me know if it doesn't solve your problem!

Yup, works great, thanks!