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

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