Neoteroi / BlackSheep

Fast ASGI web framework for Python
https://www.neoteroi.dev/blacksheep/
MIT License
1.8k stars 75 forks source link

Request handler normalization does not play well with pydantic's @validate_arguments decorator #365

Open aldem opened 1 year ago

aldem commented 1 year ago

I tried to use @validate_arguments in my async handler but this attempt failed.

My code:

from typing import Annotated
from pydantic import Field, validate_arguments

from blacksheep import Application, text

app = Application()

@app.route('/test')
@validate_arguments
async def something(i: Annotated[int, Field(ge=1, le=10)] = 1):
    return text(f"i={i}\n")

I use Python 3.9.2, pydantic == 1.10.7, blacksheep == 1.2.14

Once run with uvicorn, request attempt raises an exception:

Unhandled exception - "GET /test"
Traceback (most recent call last):
  File "blacksheep/baseapp.pyx", line 84, in blacksheep.baseapp.BaseApplication.handle
  File "/home/developer/src/python/pvenv/lib/python3.9/site-packages/blacksheep/server/normalization.py", line 545, in handler
    return ensure_response(response)
  File "/home/developer/src/python/pvenv/lib/python3.9/site-packages/blacksheep/server/normalization.py", line 132, in ensure_response
    return responses.json(result)
  File "/home/developer/src/python/pvenv/lib/python3.9/site-packages/blacksheep/server/responses.py", line 198, in json
    json_plugin.dumps(data).encode("utf8"),
  File "/home/developer/src/python/pvenv/lib/python3.9/site-packages/blacksheep/plugins/json.py", line 35, in dumps
    return self._dumps(obj)
  File "/home/developer/src/python/pvenv/lib/python3.9/site-packages/blacksheep/plugins/json.py", line 8, in default_json_dumps
    return dumps(obj, separators=(",", ":"))
  File "/home/developer/src/python/pvenv/lib/python3.9/site-packages/essentials/json.py", line 54, in dumps
    return json.dumps(
  File "/usr/lib/python3.9/json/__init__.py", line 234, in dumps
    return cls(
  File "/usr/lib/python3.9/json/encoder.py", line 199, in encode
    chunks = self.iterencode(o, _one_shot=True)
  File "/usr/lib/python3.9/json/encoder.py", line 257, in iterencode
    return _iterencode(o, 0)
  File "/home/developer/src/python/pvenv/lib/python3.9/site-packages/essentials/json.py", line 18, in default
    return json.JSONEncoder.default(self, obj)
  File "/usr/lib/python3.9/json/encoder.py", line 179, in default
    raise TypeError(f'Object of type {o.__class__.__name__} '
TypeError: Object of type coroutine is not JSON serializable
/home/developer/src/python/pvenv/lib/python3.9/site-packages/blacksheep/server/application.py:790: RuntimeWarning: coroutine 'something' was never awaited
  response = await self.handle(request)
RuntimeWarning: Enable tracemalloc to get the object allocation traceback

Without decorator everything works as it should. I was able to fix this by modifying _get_async_wrapper_for_output function in normalization.py, though I am not sure if my solution does it right and could handle all cases - probably normalization code needs to be updated but I didn't look deep into:

def _get_async_wrapper_for_output(
    method: Callable[[Request], Any],
) -> Callable[[Request], Awaitable[Response]]:
    @wraps(method)
    async def handler(request: Request) -> Response:
        response = await method(request)
        # Handle consequences of @validate_arguments decorator applied to async code
        if inspect.isawaitable(response):
            response = await response
        return ensure_response(response)

    return handler
RobertoPrevato commented 1 year ago

Hi @aldem Thank You for the heads up. I will take a look at this.