diff --git a/CHANGES.rst b/CHANGES.rst index 7920e5ab..be263970 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -10,6 +10,7 @@ Unreleased decorators. :issue:`4104` - Fixed the issue where typing requires template global decorators to accept functions with no arguments. :issue:`4098` +- Support View and MethodView instances with async handlers. :issue:`4112` Version 2.0.1 diff --git a/docs/async-await.rst b/docs/async-await.rst index ad7cbf38..71e5f452 100644 --- a/docs/async-await.rst +++ b/docs/async-await.rst @@ -18,6 +18,12 @@ defined with ``async def`` and use ``await``. data = await async_db_query(...) return jsonify(data) +Pluggable class-based views also support handlers that are implemented as +coroutines. This applies to the :meth:`~flask.views.View.dispatch_request` +method in views that inherit from the :class:`flask.views.View` class, as +well as all the HTTP method handlers in views that inherit from the +:class:`flask.views.MethodView` class. + .. admonition:: Using ``async`` on Windows on Python 3.8 Python 3.8 has a bug related to asyncio on Windows. If you encounter diff --git a/src/flask/views.py b/src/flask/views.py index 339ffa18..1bd5c68b 100644 --- a/src/flask/views.py +++ b/src/flask/views.py @@ -1,5 +1,6 @@ import typing as t +from .globals import current_app from .globals import request from .typing import ResponseReturnValue @@ -80,7 +81,7 @@ class View: def view(*args: t.Any, **kwargs: t.Any) -> ResponseReturnValue: self = view.view_class(*class_args, **class_kwargs) # type: ignore - return self.dispatch_request(*args, **kwargs) + return current_app.ensure_sync(self.dispatch_request)(*args, **kwargs) if cls.decorators: view.__name__ = name @@ -154,4 +155,4 @@ class MethodView(View, metaclass=MethodViewType): meth = getattr(self, "get", None) assert meth is not None, f"Unimplemented method {request.method!r}" - return meth(*args, **kwargs) + return current_app.ensure_sync(meth)(*args, **kwargs) diff --git a/tests/test_async.py b/tests/test_async.py index 26a91118..344e9fe6 100644 --- a/tests/test_async.py +++ b/tests/test_async.py @@ -6,6 +6,8 @@ import pytest from flask import Blueprint from flask import Flask from flask import request +from flask.views import MethodView +from flask.views import View pytest.importorskip("asgiref") @@ -18,6 +20,24 @@ class BlueprintError(Exception): pass +class AsyncView(View): + methods = ["GET", "POST"] + + async def dispatch_request(self): + await asyncio.sleep(0) + return request.method + + +class AsyncMethodView(MethodView): + async def get(self): + await asyncio.sleep(0) + return 'GET' + + async def post(self): + await asyncio.sleep(0) + return 'POST' + + @pytest.fixture(name="async_app") def _async_app(): app = Flask(__name__) @@ -53,11 +73,14 @@ def _async_app(): app.register_blueprint(blueprint, url_prefix="/bp") + app.add_url_rule('/view', view_func=AsyncView.as_view('view')) + app.add_url_rule('/methodview', view_func=AsyncMethodView.as_view('methodview')) + return app @pytest.mark.skipif(sys.version_info < (3, 7), reason="requires Python >= 3.7") -@pytest.mark.parametrize("path", ["/", "/home", "/bp/"]) +@pytest.mark.parametrize("path", ["/", "/home", "/bp/", "/view", "/methodview"]) def test_async_route(path, async_app): test_client = async_app.test_client() response = test_client.get(path)