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.
This commit is contained in:
pgjones 2020-07-06 20:54:26 +01:00 committed by David Lord
parent 85b8fab268
commit 6979265fa6
No known key found for this signature in database
GPG key ID: 7A1C87E3F5BC42A8
11 changed files with 165 additions and 9 deletions

View file

@ -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

46
docs/async_await.rst Normal file
View file

@ -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
<https://gitlab.com/pgjones/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.

View file

@ -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
--------------------------------

View file

@ -59,6 +59,7 @@ instructions for web development with Flask.
patterns/index
deploying/index
becomingbig
async_await
API Reference

View file

@ -1,4 +1,5 @@
pytest
asgiref
blinker
greenlet
python-dotenv

View file

@ -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

View file

@ -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"],
},
)

View file

@ -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

View file

@ -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

View file

@ -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

33
tests/test_async.py Normal file
View file

@ -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