diff --git a/src/flask/app.py b/src/flask/app.py index 7bc952b1..85306d7c 100644 --- a/src/flask/app.py +++ b/src/flask/app.py @@ -16,6 +16,7 @@ from werkzeug.exceptions import BadRequest from werkzeug.exceptions import BadRequestKeyError from werkzeug.exceptions import HTTPException from werkzeug.exceptions import InternalServerError +from werkzeug.local import ContextVar from werkzeug.routing import BuildError from werkzeug.routing import Map from werkzeug.routing import MapAdapter @@ -35,7 +36,6 @@ from .globals import _request_ctx_stack from .globals import g from .globals import request from .globals import session -from .helpers import async_to_sync from .helpers import get_debug_flag from .helpers import get_env from .helpers import get_flashed_messages @@ -1579,10 +1579,40 @@ class Flask(Scaffold): .. versionadded:: 2.0 """ if iscoroutinefunction(func): - return async_to_sync(func) + return self.async_to_sync(func) return func + def async_to_sync( + self, func: t.Callable[..., t.Coroutine] + ) -> t.Callable[..., t.Any]: + """Return a sync function that will run the coroutine function. + + .. code-block:: python + + result = app.async_to_sync(func)(*args, **kwargs) + + Override this method to change how the app converts async code + to be synchronously callable. + + .. versionadded:: 2.0 + """ + try: + from asgiref.sync import async_to_sync as asgiref_async_to_sync + except ImportError: + raise RuntimeError( + "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 " + "and Greenlet versions." + ) + + return asgiref_async_to_sync(func) + def make_response(self, rv: ResponseReturnValue) -> Response: """Convert the return value from a view function to an instance of :attr:`response_class`. diff --git a/src/flask/helpers.py b/src/flask/helpers.py index 621d51e6..109f544f 100644 --- a/src/flask/helpers.py +++ b/src/flask/helpers.py @@ -10,7 +10,6 @@ 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 @@ -800,26 +799,3 @@ def is_ip(value: str) -> bool: return True return False - - -def async_to_sync(func: t.Callable[..., t.Coroutine]) -> t.Callable[..., t.Any]: - """Return a sync function that will run the coroutine function *func*. - - This can be used as so - - result = async_to_async(func)(*args, **kwargs) - """ - try: - from asgiref.sync import async_to_sync as asgiref_async_to_sync - except ImportError: - raise RuntimeError( - "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." - ) - - return asgiref_async_to_sync(func) diff --git a/tests/test_async.py b/tests/test_async.py index 798eed85..26a91118 100644 --- a/tests/test_async.py +++ b/tests/test_async.py @@ -6,7 +6,6 @@ import pytest from flask import Blueprint from flask import Flask from flask import request -from flask.helpers import async_to_sync pytest.importorskip("asgiref") @@ -136,5 +135,6 @@ def test_async_before_after_request(): @pytest.mark.skipif(sys.version_info >= (3, 7), reason="should only raise Python < 3.7") def test_async_runtime_error(): + app = Flask(__name__) with pytest.raises(RuntimeError): - async_to_sync(None) + app.async_to_sync(None)