From 6979265fa643ed982d062f38d386c37bbbef0d9b Mon Sep 17 00:00:00 2001 From: pgjones Date: Mon, 6 Jul 2020 20:54:26 +0100 Subject: [PATCH 1/5] Add `async` support This allows for async functions to be passed to the Flask class instance, for example as a view function, @app.route("/") async def index(): return "Async hello" this comes with a cost though of poorer performance than using the sync equivalent. asgiref is the standard way to run async code within a sync context, and is used in Django making it a safe and sane choice for this. --- CHANGES.rst | 2 ++ docs/async_await.rst | 46 ++++++++++++++++++++++++++++++++++++++++++ docs/design.rst | 12 +++++++++++ docs/index.rst | 1 + requirements/tests.in | 1 + requirements/tests.txt | 2 ++ setup.py | 5 ++++- src/flask/app.py | 6 +++--- src/flask/helpers.py | 41 +++++++++++++++++++++++++++++++++++++ src/flask/scaffold.py | 25 ++++++++++++++++++----- tests/test_async.py | 33 ++++++++++++++++++++++++++++++ 11 files changed, 165 insertions(+), 9 deletions(-) create mode 100644 docs/async_await.rst create mode 100644 tests/test_async.py diff --git a/CHANGES.rst b/CHANGES.rst index d98d91fe..280a2dd5 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -67,6 +67,8 @@ Unreleased - Add route decorators for common HTTP methods. For example, ``@app.post("/login")`` is a shortcut for ``@app.route("/login", methods=["POST"])``. :pr:`3907` +- Support async views, error handlers, before and after request, and + teardown functions. :pr:`3412` Version 1.1.2 diff --git a/docs/async_await.rst b/docs/async_await.rst new file mode 100644 index 00000000..b46fad3b --- /dev/null +++ b/docs/async_await.rst @@ -0,0 +1,46 @@ +.. _async_await: + +Using async and await +===================== + +.. versionadded:: 2.0 + +Routes, error handlers, before request, after request, and teardown +functions can all be coroutine functions if Flask is installed with +the ``async`` extra (``pip install flask[async]``). This allows code +such as, + +.. code-block:: python + + @app.route("/") + async def index(): + return await ... + +including the usage of any asyncio based libraries. + + +When to use Quart instead +------------------------- + +Flask's ``async/await`` support is less performant than async first +frameworks due to the way it is implemented. Therefore if you have a +mainly async codebase it would make sense to consider `Quart +`_. Quart is a reimplementation of +the Flask using ``async/await`` based on the ASGI standard (Flask is +based on the WSGI standard). + + +Decorators +---------- + +Decorators designed for Flask, such as those in Flask extensions are +unlikely to work. This is because the decorator will not await the +coroutine function nor will they themselves be awaitable. + + +Other event loops +----------------- + +At the moment Flask only supports asyncio - the +:meth:`flask.Flask.ensure_sync` should be overridden to support +alternative event loops. diff --git a/docs/design.rst b/docs/design.rst index ae76c921..b41a08c2 100644 --- a/docs/design.rst +++ b/docs/design.rst @@ -171,6 +171,18 @@ Also see the :doc:`/becomingbig` section of the documentation for some inspiration for larger applications based on Flask. +Async-await and ASGI support +---------------------------- + +Flask supports ``async`` coroutines for view functions, and certain +others by executing the coroutine on a seperate thread instead of +utilising an event loop on the main thread as an async first (ASGI) +frameworks would. This is necessary for Flask to remain backwards +compatibility with extensions and code built before ``async`` was +introduced into Python. This compromise introduces a performance cost +compared with the ASGI frameworks, due to the overhead of the threads. + + What Flask is, What Flask is Not -------------------------------- diff --git a/docs/index.rst b/docs/index.rst index 151dde92..a1c49a90 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -59,6 +59,7 @@ instructions for web development with Flask. patterns/index deploying/index becomingbig + async_await API Reference diff --git a/requirements/tests.in b/requirements/tests.in index b5f5c912..88fe5481 100644 --- a/requirements/tests.in +++ b/requirements/tests.in @@ -1,4 +1,5 @@ pytest +asgiref blinker greenlet python-dotenv diff --git a/requirements/tests.txt b/requirements/tests.txt index 50c58b65..a44b876f 100644 --- a/requirements/tests.txt +++ b/requirements/tests.txt @@ -4,6 +4,8 @@ # # pip-compile requirements/tests.in # +asgiref==3.2.10 + # via -r requirements/tests.in attrs==20.3.0 # via pytest blinker==1.4 diff --git a/setup.py b/setup.py index 7ec4196f..88889f4c 100644 --- a/setup.py +++ b/setup.py @@ -9,5 +9,8 @@ setup( "itsdangerous>=0.24", "click>=5.1", ], - extras_require={"dotenv": ["python-dotenv"]}, + extras_require={ + "async": ["asgiref>=3.2"], + "dotenv": ["python-dotenv"], + }, ) diff --git a/src/flask/app.py b/src/flask/app.py index 5be079c7..65ec5046 100644 --- a/src/flask/app.py +++ b/src/flask/app.py @@ -1050,7 +1050,7 @@ class Flask(Scaffold): "View function mapping is overwriting an existing" f" endpoint function: {endpoint}" ) - self.view_functions[endpoint] = view_func + self.view_functions[endpoint] = self.ensure_sync(view_func) @setupmethod def template_filter(self, name=None): @@ -1165,7 +1165,7 @@ class Flask(Scaffold): .. versionadded:: 0.8 """ - self.before_first_request_funcs.append(f) + self.before_first_request_funcs.append(self.ensure_sync(f)) return f @setupmethod @@ -1198,7 +1198,7 @@ class Flask(Scaffold): .. versionadded:: 0.9 """ - self.teardown_appcontext_funcs.append(f) + self.teardown_appcontext_funcs.append(self.ensure_sync(f)) return f @setupmethod diff --git a/src/flask/helpers.py b/src/flask/helpers.py index 73a3fd82..46244d29 100644 --- a/src/flask/helpers.py +++ b/src/flask/helpers.py @@ -2,6 +2,7 @@ import os import socket import warnings from functools import update_wrapper +from functools import wraps from threading import RLock import werkzeug.utils @@ -729,3 +730,43 @@ def is_ip(value): return True return False + + +def run_async(func): + """Return a sync function that will run the coroutine function *func*.""" + try: + from asgiref.sync import async_to_sync + except ImportError: + raise RuntimeError( + "Install Flask with the 'async' extra in order to use async views." + ) + + @wraps(func) + def outer(*args, **kwargs): + """This function grabs the current context for the inner function. + + This is similar to the copy_current_xxx_context functions in the + ctx module, except it has an async inner. + """ + ctx = None + if _request_ctx_stack.top is not None: + ctx = _request_ctx_stack.top.copy() + + @wraps(func) + async def inner(*a, **k): + """This restores the context before awaiting the func. + + This is required as the func must be awaited within the + context. Simply calling func (as per the + copy_current_xxx_context functions) doesn't work as the + with block will close before the coroutine is awaited. + """ + if ctx is not None: + with ctx: + return await func(*a, **k) + else: + return await func(*a, **k) + + return async_to_sync(inner)(*args, **kwargs) + + return outer diff --git a/src/flask/scaffold.py b/src/flask/scaffold.py index 735c142c..7911bc71 100644 --- a/src/flask/scaffold.py +++ b/src/flask/scaffold.py @@ -4,6 +4,7 @@ import pkgutil import sys from collections import defaultdict from functools import update_wrapper +from inspect import iscoroutinefunction from jinja2 import FileSystemLoader from werkzeug.exceptions import default_exceptions @@ -12,6 +13,7 @@ from werkzeug.exceptions import HTTPException from .cli import AppGroup from .globals import current_app from .helpers import locked_cached_property +from .helpers import run_async from .helpers import send_from_directory from .templating import _default_template_ctx_processor @@ -484,7 +486,7 @@ class Scaffold: """ def decorator(f): - self.view_functions[endpoint] = f + self.view_functions[endpoint] = self.ensure_sync(f) return f return decorator @@ -508,7 +510,7 @@ class Scaffold: return value from the view, and further request handling is stopped. """ - self.before_request_funcs[None].append(f) + self.before_request_funcs.setdefault(None, []).append(self.ensure_sync(f)) return f @setupmethod @@ -524,7 +526,7 @@ class Scaffold: should not be used for actions that must execute, such as to close resources. Use :meth:`teardown_request` for that. """ - self.after_request_funcs[None].append(f) + self.after_request_funcs.setdefault(None, []).append(self.ensure_sync(f)) return f @setupmethod @@ -563,7 +565,7 @@ class Scaffold: debugger can still access it. This behavior can be controlled by the ``PRESERVE_CONTEXT_ON_EXCEPTION`` configuration variable. """ - self.teardown_request_funcs[None].append(f) + self.teardown_request_funcs.setdefault(None, []).append(self.ensure_sync(f)) return f @setupmethod @@ -659,7 +661,7 @@ class Scaffold: " instead." ) - self.error_handler_spec[None][code][exc_class] = f + self.error_handler_spec[None][code][exc_class] = self.ensure_sync(f) @staticmethod def _get_exc_class_and_code(exc_class_or_code): @@ -684,6 +686,19 @@ class Scaffold: else: return exc_class, None + def ensure_sync(self, func): + """Ensure that the returned function is sync and calls the async func. + + .. versionadded:: 2.0 + + Override if you wish to change how asynchronous functions are + run. + """ + if iscoroutinefunction(func): + return run_async(func) + else: + return func + def _endpoint_from_view_func(view_func): """Internal helper that returns the default endpoint for a given diff --git a/tests/test_async.py b/tests/test_async.py new file mode 100644 index 00000000..d47d36ce --- /dev/null +++ b/tests/test_async.py @@ -0,0 +1,33 @@ +import asyncio + +import pytest + +from flask import abort +from flask import Flask +from flask import request + + +@pytest.fixture(name="async_app") +def _async_app(): + app = Flask(__name__) + + @app.route("/", methods=["GET", "POST"]) + async def index(): + await asyncio.sleep(0) + return request.method + + @app.route("/error") + async def error(): + abort(412) + + return app + + +def test_async_request_context(async_app): + test_client = async_app.test_client() + response = test_client.get("/") + assert b"GET" in response.get_data() + response = test_client.post("/") + assert b"POST" in response.get_data() + response = test_client.get("/error") + assert response.status_code == 412 From c6c6408c3fb96245a2e2afc4b754cdf065fdad47 Mon Sep 17 00:00:00 2001 From: pgjones Date: Wed, 10 Feb 2021 21:14:58 +0000 Subject: [PATCH 2/5] Raise a runtime error if run_async is called without real ContextVars Werkzeug offers a ContextVar replacement for Python < 3.7, however it doesn't work across asyncio tasks, hence it makes sense to error out rather than find there are odd bugs. Note the docs build requires the latest (dev) Werkzeug due to this change (to import ContextVar from werkzeug.local). --- src/flask/helpers.py | 6 ++++++ tests/test_async.py | 9 +++++++++ tox.ini | 5 ++++- 3 files changed, 19 insertions(+), 1 deletion(-) diff --git a/src/flask/helpers.py b/src/flask/helpers.py index 46244d29..5933f42a 100644 --- a/src/flask/helpers.py +++ b/src/flask/helpers.py @@ -7,6 +7,7 @@ from threading import RLock import werkzeug.utils from werkzeug.exceptions import NotFound +from werkzeug.local import ContextVar from werkzeug.routing import BuildError from werkzeug.urls import url_quote @@ -741,6 +742,11 @@ def run_async(func): "Install Flask with the 'async' extra in order to use async views." ) + if ContextVar.__module__ == "werkzeug.local": + raise RuntimeError( + "async cannot be used with this combination of Python & Greenlet versions" + ) + @wraps(func) def outer(*args, **kwargs): """This function grabs the current context for the inner function. diff --git a/tests/test_async.py b/tests/test_async.py index d47d36ce..12784c34 100644 --- a/tests/test_async.py +++ b/tests/test_async.py @@ -1,10 +1,12 @@ import asyncio +import sys import pytest from flask import abort from flask import Flask from flask import request +from flask.helpers import run_async @pytest.fixture(name="async_app") @@ -23,6 +25,7 @@ def _async_app(): return app +@pytest.mark.skipif(sys.version_info < (3, 7), reason="requires Python >= 3.7") def test_async_request_context(async_app): test_client = async_app.test_client() response = test_client.get("/") @@ -31,3 +34,9 @@ def test_async_request_context(async_app): assert b"POST" in response.get_data() response = test_client.get("/error") assert response.status_code == 412 + + +@pytest.mark.skipif(sys.version_info >= (3, 7), reason="should only raise Python < 3.7") +def test_async_runtime_error(): + with pytest.raises(RuntimeError): + run_async(None) diff --git a/tox.ini b/tox.ini index e0f666d8..cf12c0eb 100644 --- a/tox.ini +++ b/tox.ini @@ -25,5 +25,8 @@ skip_install = true commands = pre-commit run --all-files --show-diff-on-failure [testenv:docs] -deps = -r requirements/docs.txt +deps = + -r requirements/docs.txt + + https://github.com/pallets/werkzeug/archive/master.tar.gz commands = sphinx-build -W -b html -d {envtmpdir}/doctrees docs {envtmpdir}/html From 00f5a3e55ca3b6dd4e98044ab76f055dc74997ac Mon Sep 17 00:00:00 2001 From: pgjones Date: Wed, 24 Mar 2021 20:47:55 +0000 Subject: [PATCH 3/5] Alter ensure_sync implementation to support extensions This allows extensions to override the Flask.ensure_sync method and have the change apply to blueprints as well. Without this change it is possible for differing blueprints to have differing ensure_sync approaches depending on the extension used - which would likely result in event-loop blocking issues. This also allows blueprints to have a custom ensure_sync, although this is a by product rather than an expected use case. --- src/flask/app.py | 15 ++++++ src/flask/blueprints.py | 64 +++++++++++++++++++----- src/flask/helpers.py | 1 + src/flask/scaffold.py | 14 +----- tests/test_async.py | 107 +++++++++++++++++++++++++++++++++++++--- 5 files changed, 169 insertions(+), 32 deletions(-) diff --git a/src/flask/app.py b/src/flask/app.py index 65ec5046..c1743e6e 100644 --- a/src/flask/app.py +++ b/src/flask/app.py @@ -2,6 +2,7 @@ import os import sys import weakref from datetime import timedelta +from inspect import iscoroutinefunction from itertools import chain from threading import Lock @@ -34,6 +35,7 @@ from .helpers import get_env from .helpers import get_flashed_messages from .helpers import get_load_dotenv from .helpers import locked_cached_property +from .helpers import run_async from .helpers import url_for from .json import jsonify from .logging import create_logger @@ -1517,6 +1519,19 @@ class Flask(Scaffold): """ return False + def ensure_sync(self, func): + """Ensure that the returned function is sync and calls the async func. + + .. versionadded:: 2.0 + + Override if you wish to change how asynchronous functions are + run. + """ + if iscoroutinefunction(func): + return run_async(func) + + return func + def make_response(self, rv): """Convert the return value from a view function to an instance of :attr:`response_class`. diff --git a/src/flask/blueprints.py b/src/flask/blueprints.py index 7c314203..c4f94a06 100644 --- a/src/flask/blueprints.py +++ b/src/flask/blueprints.py @@ -1,3 +1,4 @@ +from collections import defaultdict from functools import update_wrapper from .scaffold import _endpoint_from_view_func @@ -235,24 +236,44 @@ class Blueprint(Scaffold): # Merge blueprint data into parent. if first_registration: - def extend(bp_dict, parent_dict): + def extend(bp_dict, parent_dict, ensure_sync=False): for key, values in bp_dict.items(): key = self.name if key is None else f"{self.name}.{key}" + + if ensure_sync: + values = [app.ensure_sync(func) for func in values] + parent_dict[key].extend(values) - def update(bp_dict, parent_dict): - for key, value in bp_dict.items(): - key = self.name if key is None else f"{self.name}.{key}" - parent_dict[key] = value + for key, value in self.error_handler_spec.items(): + key = self.name if key is None else f"{self.name}.{key}" + value = defaultdict( + dict, + { + code: { + exc_class: app.ensure_sync(func) + for exc_class, func in code_values.items() + } + for code, code_values in value.items() + }, + ) + app.error_handler_spec[key] = value - app.view_functions.update(self.view_functions) - extend(self.before_request_funcs, app.before_request_funcs) - extend(self.after_request_funcs, app.after_request_funcs) - extend(self.teardown_request_funcs, app.teardown_request_funcs) + for endpoint, func in self.view_functions.items(): + app.view_functions[endpoint] = app.ensure_sync(func) + + extend( + self.before_request_funcs, app.before_request_funcs, ensure_sync=True + ) + extend(self.after_request_funcs, app.after_request_funcs, ensure_sync=True) + extend( + self.teardown_request_funcs, + app.teardown_request_funcs, + ensure_sync=True, + ) extend(self.url_default_functions, app.url_default_functions) extend(self.url_value_preprocessors, app.url_value_preprocessors) extend(self.template_context_processors, app.template_context_processors) - update(self.error_handler_spec, app.error_handler_spec) for deferred in self.deferred_functions: deferred(state) @@ -380,7 +401,9 @@ class Blueprint(Scaffold): before each request, even if outside of a blueprint. """ self.record_once( - lambda s: s.app.before_request_funcs.setdefault(None, []).append(f) + lambda s: s.app.before_request_funcs.setdefault(None, []).append( + s.app.ensure_sync(f) + ) ) return f @@ -388,7 +411,9 @@ class Blueprint(Scaffold): """Like :meth:`Flask.before_first_request`. Such a function is executed before the first request to the application. """ - self.record_once(lambda s: s.app.before_first_request_funcs.append(f)) + self.record_once( + lambda s: s.app.before_first_request_funcs.append(s.app.ensure_sync(f)) + ) return f def after_app_request(self, f): @@ -396,7 +421,9 @@ class Blueprint(Scaffold): is executed after each request, even if outside of the blueprint. """ self.record_once( - lambda s: s.app.after_request_funcs.setdefault(None, []).append(f) + lambda s: s.app.after_request_funcs.setdefault(None, []).append( + s.app.ensure_sync(f) + ) ) return f @@ -443,3 +470,14 @@ class Blueprint(Scaffold): lambda s: s.app.url_default_functions.setdefault(None, []).append(f) ) return f + + def ensure_sync(self, f): + """Ensure the function is synchronous. + + Override if you would like custom async to sync behaviour in + this blueprint. Otherwise :meth:`~flask.Flask..ensure_sync` is + used. + + .. versionadded:: 2.0 + """ + return f diff --git a/src/flask/helpers.py b/src/flask/helpers.py index 5933f42a..3b4377c3 100644 --- a/src/flask/helpers.py +++ b/src/flask/helpers.py @@ -755,6 +755,7 @@ def run_async(func): ctx module, except it has an async inner. """ ctx = None + if _request_ctx_stack.top is not None: ctx = _request_ctx_stack.top.copy() diff --git a/src/flask/scaffold.py b/src/flask/scaffold.py index 7911bc71..bfa53068 100644 --- a/src/flask/scaffold.py +++ b/src/flask/scaffold.py @@ -4,7 +4,6 @@ import pkgutil import sys from collections import defaultdict from functools import update_wrapper -from inspect import iscoroutinefunction from jinja2 import FileSystemLoader from werkzeug.exceptions import default_exceptions @@ -13,7 +12,6 @@ from werkzeug.exceptions import HTTPException from .cli import AppGroup from .globals import current_app from .helpers import locked_cached_property -from .helpers import run_async from .helpers import send_from_directory from .templating import _default_template_ctx_processor @@ -687,17 +685,7 @@ class Scaffold: return exc_class, None def ensure_sync(self, func): - """Ensure that the returned function is sync and calls the async func. - - .. versionadded:: 2.0 - - Override if you wish to change how asynchronous functions are - run. - """ - if iscoroutinefunction(func): - return run_async(func) - else: - return func + raise NotImplementedError() def _endpoint_from_view_func(view_func): diff --git a/tests/test_async.py b/tests/test_async.py index 12784c34..de7d89b6 100644 --- a/tests/test_async.py +++ b/tests/test_async.py @@ -3,12 +3,20 @@ import sys import pytest -from flask import abort +from flask import Blueprint from flask import Flask from flask import request from flask.helpers import run_async +class AppError(Exception): + pass + + +class BlueprintError(Exception): + pass + + @pytest.fixture(name="async_app") def _async_app(): app = Flask(__name__) @@ -18,24 +26,111 @@ def _async_app(): await asyncio.sleep(0) return request.method + @app.errorhandler(AppError) + async def handle(_): + return "", 412 + @app.route("/error") async def error(): - abort(412) + raise AppError() + + blueprint = Blueprint("bp", __name__) + + @blueprint.route("/", methods=["GET", "POST"]) + async def bp_index(): + await asyncio.sleep(0) + return request.method + + @blueprint.errorhandler(BlueprintError) + async def bp_handle(_): + return "", 412 + + @blueprint.route("/error") + async def bp_error(): + raise BlueprintError() + + app.register_blueprint(blueprint, url_prefix="/bp") return app @pytest.mark.skipif(sys.version_info < (3, 7), reason="requires Python >= 3.7") -def test_async_request_context(async_app): +@pytest.mark.parametrize("path", ["/", "/bp/"]) +def test_async_route(path, async_app): test_client = async_app.test_client() - response = test_client.get("/") + response = test_client.get(path) assert b"GET" in response.get_data() - response = test_client.post("/") + response = test_client.post(path) assert b"POST" in response.get_data() - response = test_client.get("/error") + + +@pytest.mark.skipif(sys.version_info < (3, 7), reason="requires Python >= 3.7") +@pytest.mark.parametrize("path", ["/error", "/bp/error"]) +def test_async_error_handler(path, async_app): + test_client = async_app.test_client() + response = test_client.get(path) assert response.status_code == 412 +@pytest.mark.skipif(sys.version_info < (3, 7), reason="requires Python >= 3.7") +def test_async_before_after_request(): + app_first_called = False + app_before_called = False + app_after_called = False + bp_before_called = False + bp_after_called = False + + app = Flask(__name__) + + @app.route("/") + def index(): + return "" + + @app.before_first_request + async def before_first(): + nonlocal app_first_called + app_first_called = True + + @app.before_request + async def before(): + nonlocal app_before_called + app_before_called = True + + @app.after_request + async def after(response): + nonlocal app_after_called + app_after_called = True + return response + + blueprint = Blueprint("bp", __name__) + + @blueprint.route("/") + def bp_index(): + return "" + + @blueprint.before_request + async def bp_before(): + nonlocal bp_before_called + bp_before_called = True + + @blueprint.after_request + async def bp_after(response): + nonlocal bp_after_called + bp_after_called = True + return response + + app.register_blueprint(blueprint, url_prefix="/bp") + + test_client = app.test_client() + test_client.get("/") + assert app_first_called + assert app_before_called + assert app_after_called + test_client.get("/bp/") + assert bp_before_called + assert bp_after_called + + @pytest.mark.skipif(sys.version_info >= (3, 7), reason="should only raise Python < 3.7") def test_async_runtime_error(): with pytest.raises(RuntimeError): From 61fbae866478c19ea5f5c3b9b571a4fad0ba7e5c Mon Sep 17 00:00:00 2001 From: David Lord Date: Tue, 6 Apr 2021 15:31:16 -0700 Subject: [PATCH 4/5] skip async tests if asgiref isn't installed --- tests/test_async.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/tests/test_async.py b/tests/test_async.py index de7d89b6..5893ff69 100644 --- a/tests/test_async.py +++ b/tests/test_async.py @@ -8,6 +8,8 @@ from flask import Flask from flask import request from flask.helpers import run_async +pytest.importorskip("asgiref") + class AppError(Exception): pass From dc3e9c0cc36b7945431454ea1543da4e33023bb2 Mon Sep 17 00:00:00 2001 From: David Lord Date: Tue, 6 Apr 2021 15:31:28 -0700 Subject: [PATCH 5/5] update async docs --- docs/async-await.rst | 81 +++++++++++++++++++++++++++++++++++++++++ docs/async_await.rst | 46 ----------------------- docs/design.rst | 23 ++++++++---- docs/index.rst | 2 +- src/flask/app.py | 9 +++-- src/flask/blueprints.py | 4 +- src/flask/helpers.py | 9 +++-- 7 files changed, 109 insertions(+), 65 deletions(-) create mode 100644 docs/async-await.rst delete mode 100644 docs/async_await.rst diff --git a/docs/async-await.rst b/docs/async-await.rst new file mode 100644 index 00000000..c8981f88 --- /dev/null +++ b/docs/async-await.rst @@ -0,0 +1,81 @@ +.. _async_await: + +Using ``async`` and ``await`` +============================= + +.. versionadded:: 2.0 + +Routes, error handlers, before request, after request, and teardown +functions can all be coroutine functions if Flask is installed with the +``async`` extra (``pip install flask[async]``). This allows views to be +defined with ``async def`` and use ``await``. + +.. code-block:: python + + @app.route("/get-data") + async def get_data(): + data = await async_db_query(...) + return jsonify(data) + + +Performance +----------- + +Async functions require an event loop to run. Flask, as a WSGI +application, uses one worker to handle one request/response cycle. +When a request comes in to an async view, Flask will start an event loop +in a thread, run the view function there, then return the result. + +Each request still ties up one worker, even for async views. The upside +is that you can run async code within a view, for example to make +multiple concurrent database queries, HTTP requests to an external API, +etc. However, the number of requests your application can handle at one +time will remain the same. + +**Async is not inherently faster than sync code.** Async is beneficial +when performing concurrent IO-bound tasks, but will probably not improve +CPU-bound tasks. Traditional Flask views will still be appropriate for +most use cases, but Flask's async support enables writing and using +code that wasn't possible natively before. + + +When to use Quart instead +------------------------- + +Flask's async support is less performant than async-first frameworks due +to the way it is implemented. If you have a mainly async codebase it +would make sense to consider `Quart`_. Quart is a reimplementation of +Flask based on the `ASGI`_ standard instead of WSGI. This allows it to +handle many concurrent requests, long running requests, and websockets +without requiring individual worker processes or threads. + +It has also already been possible to run Flask with Gevent or Eventlet +to get many of the benefits of async request handling. These libraries +patch low-level Python functions to accomplish this, whereas ``async``/ +``await`` and ASGI use standard, modern Python capabilities. Deciding +whether you should use Flask, Quart, or something else is ultimately up +to understanding the specific needs of your project. + +.. _Quart: https://gitlab.com/pgjones/quart +.. _ASGI: https://asgi.readthedocs.io/en/latest/ + + +Extensions +---------- + +Existing Flask extensions only expect views to be synchronous. If they +provide decorators to add functionality to views, those will probably +not work with async views because they will not await the function or be +awaitable. Other functions they provide will not be awaitable either and +will probably be blocking if called within an async view. + +Check the changelog of the extension you want to use to see if they've +implemented async support, or make a feature request or PR to them. + + +Other event loops +----------------- + +At the moment Flask only supports :mod:`asyncio`. It's possible to +override :meth:`flask.Flask.ensure_sync` to change how async functions +are wrapped to use a different library. diff --git a/docs/async_await.rst b/docs/async_await.rst deleted file mode 100644 index b46fad3b..00000000 --- a/docs/async_await.rst +++ /dev/null @@ -1,46 +0,0 @@ -.. _async_await: - -Using async and await -===================== - -.. versionadded:: 2.0 - -Routes, error handlers, before request, after request, and teardown -functions can all be coroutine functions if Flask is installed with -the ``async`` extra (``pip install flask[async]``). This allows code -such as, - -.. code-block:: python - - @app.route("/") - async def index(): - return await ... - -including the usage of any asyncio based libraries. - - -When to use Quart instead -------------------------- - -Flask's ``async/await`` support is less performant than async first -frameworks due to the way it is implemented. Therefore if you have a -mainly async codebase it would make sense to consider `Quart -`_. Quart is a reimplementation of -the Flask using ``async/await`` based on the ASGI standard (Flask is -based on the WSGI standard). - - -Decorators ----------- - -Decorators designed for Flask, such as those in Flask extensions are -unlikely to work. This is because the decorator will not await the -coroutine function nor will they themselves be awaitable. - - -Other event loops ------------------ - -At the moment Flask only supports asyncio - the -:meth:`flask.Flask.ensure_sync` should be overridden to support -alternative event loops. diff --git a/docs/design.rst b/docs/design.rst index b41a08c2..5d57063e 100644 --- a/docs/design.rst +++ b/docs/design.rst @@ -171,16 +171,23 @@ Also see the :doc:`/becomingbig` section of the documentation for some inspiration for larger applications based on Flask. -Async-await and ASGI support +Async/await and ASGI support ---------------------------- -Flask supports ``async`` coroutines for view functions, and certain -others by executing the coroutine on a seperate thread instead of -utilising an event loop on the main thread as an async first (ASGI) -frameworks would. This is necessary for Flask to remain backwards -compatibility with extensions and code built before ``async`` was -introduced into Python. This compromise introduces a performance cost -compared with the ASGI frameworks, due to the overhead of the threads. +Flask supports ``async`` coroutines for view functions by executing the +coroutine on a separate thread instead of using an event loop on the +main thread as an async-first (ASGI) framework would. This is necessary +for Flask to remain backwards compatible with extensions and code built +before ``async`` was introduced into Python. This compromise introduces +a performance cost compared with the ASGI frameworks, due to the +overhead of the threads. + +Due to how tied to WSGI Flask's code is, it's not clear if it's possible +to make the ``Flask`` class support ASGI and WSGI at the same time. Work +is currently being done in Werkzeug to work with ASGI, which may +eventually enable support in Flask as well. + +See :doc:`/async-await` for more discussion. What Flask is, What Flask is Not diff --git a/docs/index.rst b/docs/index.rst index a1c49a90..6ff62529 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -59,7 +59,7 @@ instructions for web development with Flask. patterns/index deploying/index becomingbig - async_await + async-await API Reference diff --git a/src/flask/app.py b/src/flask/app.py index c1743e6e..eefd361a 100644 --- a/src/flask/app.py +++ b/src/flask/app.py @@ -1520,12 +1520,13 @@ class Flask(Scaffold): return False def ensure_sync(self, func): - """Ensure that the returned function is sync and calls the async func. + """Ensure that the function is synchronous for WSGI workers. + Plain ``def`` functions are returned as-is. ``async def`` + functions are wrapped to run and wait for the response. + + Override this method to change how the app runs async views. .. versionadded:: 2.0 - - Override if you wish to change how asynchronous functions are - run. """ if iscoroutinefunction(func): return run_async(func) diff --git a/src/flask/blueprints.py b/src/flask/blueprints.py index c4f94a06..d769cd58 100644 --- a/src/flask/blueprints.py +++ b/src/flask/blueprints.py @@ -475,8 +475,8 @@ class Blueprint(Scaffold): """Ensure the function is synchronous. Override if you would like custom async to sync behaviour in - this blueprint. Otherwise :meth:`~flask.Flask..ensure_sync` is - used. + this blueprint. Otherwise the app's + :meth:`~flask.Flask.ensure_sync` is used. .. versionadded:: 2.0 """ diff --git a/src/flask/helpers.py b/src/flask/helpers.py index 3b4377c3..a4d03861 100644 --- a/src/flask/helpers.py +++ b/src/flask/helpers.py @@ -742,9 +742,10 @@ def run_async(func): "Install Flask with the 'async' extra in order to use async views." ) + # Check that Werkzeug isn't using its fallback ContextVar class. if ContextVar.__module__ == "werkzeug.local": raise RuntimeError( - "async cannot be used with this combination of Python & Greenlet versions" + "Async cannot be used with this combination of Python & Greenlet versions." ) @wraps(func) @@ -763,9 +764,9 @@ def run_async(func): async def inner(*a, **k): """This restores the context before awaiting the func. - This is required as the func must be awaited within the - context. Simply calling func (as per the - copy_current_xxx_context functions) doesn't work as the + This is required as the function must be awaited within the + context. Only calling ``func`` (as per the + ``copy_current_xxx_context`` functions) doesn't work as the with block will close before the coroutine is awaited. """ if ctx is not None: