forked from orbit-oss/flask
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:
parent
85b8fab268
commit
6979265fa6
11 changed files with 165 additions and 9 deletions
|
|
@ -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
46
docs/async_await.rst
Normal 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.
|
||||
|
|
@ -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
|
||||
--------------------------------
|
||||
|
||||
|
|
|
|||
|
|
@ -59,6 +59,7 @@ instructions for web development with Flask.
|
|||
patterns/index
|
||||
deploying/index
|
||||
becomingbig
|
||||
async_await
|
||||
|
||||
|
||||
API Reference
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
pytest
|
||||
asgiref
|
||||
blinker
|
||||
greenlet
|
||||
python-dotenv
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
5
setup.py
5
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"],
|
||||
},
|
||||
)
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
33
tests/test_async.py
Normal 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
|
||||
Loading…
Add table
Add a link
Reference in a new issue