diff --git a/CHANGES.rst b/CHANGES.rst
index f4e0b5d0..5717e48d 100644
--- a/CHANGES.rst
+++ b/CHANGES.rst
@@ -13,11 +13,14 @@ Version 2.0.2
Unreleased
-- Fix type annotation for ``teardown_request``. :issue:`4093`
+- Fix type annotation for ``teardown_*`` methods. :issue:`4093`
- Fix type annotation for ``before_request`` and ``before_app_request``
decorators. :issue:`4104`
- Fixed the issue where typing requires template global
decorators to accept functions with no arguments. :issue:`4098`
+- Support View and MethodView instances with async handlers. :issue:`4112`
+- Enhance typing of ``app.errorhandler`` decorator. :issue:`4095`
+- Fix registering a blueprint twice with differing names. :issue:`4124`
Version 2.0.1
diff --git a/CONTRIBUTING.rst b/CONTRIBUTING.rst
index 3a9177a4..a3c8b851 100644
--- a/CONTRIBUTING.rst
+++ b/CONTRIBUTING.rst
@@ -12,14 +12,16 @@ to address bugs and feature requests in Flask itself. Use one of the
following resources for questions about using Flask or issues with your
own code:
-- The ``#get-help`` channel on our Discord chat:
+- The ``#questions`` channel on our Discord chat:
https://discord.gg/pallets
- The mailing list flask@python.org for long term discussion or larger
issues.
- Ask on `Stack Overflow`_. Search with Google first using:
``site:stackoverflow.com flask {search term, exception message, etc.}``
+- Ask on our `GitHub Discussions`_.
.. _Stack Overflow: https://stackoverflow.com/questions/tagged/flask?tab=Frequent
+.. _GitHub Discussions: https://github.com/pallets/flask/discussions
Reporting issues
@@ -92,7 +94,7 @@ First time setup
.. code-block:: text
- git remote add fork https://github.com/{username}/flask
+ $ git remote add fork https://github.com/{username}/flask
- Create a virtualenv.
diff --git a/docs/async-await.rst b/docs/async-await.rst
index 34751d47..71e5f452 100644
--- a/docs/async-await.rst
+++ b/docs/async-await.rst
@@ -7,7 +7,8 @@ Using ``async`` and ``await``
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
+``async`` extra (``pip install flask[async]``). It requires Python 3.7+
+where ``contextvars.ContextVar`` is available. This allows views to be
defined with ``async def`` and use ``await``.
.. code-block:: python
@@ -17,6 +18,12 @@ defined with ``async def`` and use ``await``.
data = await async_db_query(...)
return jsonify(data)
+Pluggable class-based views also support handlers that are implemented as
+coroutines. This applies to the :meth:`~flask.views.View.dispatch_request`
+method in views that inherit from the :class:`flask.views.View` class, as
+well as all the HTTP method handlers in views that inherit from the
+:class:`flask.views.MethodView` class.
+
.. admonition:: Using ``async`` on Windows on Python 3.8
Python 3.8 has a bug related to asyncio on Windows. If you encounter
diff --git a/docs/installation.rst b/docs/installation.rst
index aef7df0c..a5d105f7 100644
--- a/docs/installation.rst
+++ b/docs/installation.rst
@@ -8,6 +8,8 @@ Python Version
We recommend using the latest version of Python. Flask supports Python
3.6 and newer.
+``async`` support in Flask requires Python 3.7+ for ``contextvars.ContextVar``.
+
Dependencies
------------
diff --git a/docs/patterns/celery.rst b/docs/patterns/celery.rst
index e1f6847e..38a9a025 100644
--- a/docs/patterns/celery.rst
+++ b/docs/patterns/celery.rst
@@ -64,7 +64,7 @@ An example task
Let's write a task that adds two numbers together and returns the result. We
configure Celery's broker and backend to use Redis, create a ``celery``
-application using the factor from above, and then use it to define the task. ::
+application using the factory from above, and then use it to define the task. ::
from flask import Flask
diff --git a/docs/quickstart.rst b/docs/quickstart.rst
index 5cd59f41..b5071ab0 100644
--- a/docs/quickstart.rst
+++ b/docs/quickstart.rst
@@ -444,9 +444,9 @@ Here is an example template:
Hello, World!
{% endif %}
-Inside templates you also have access to the :class:`~flask.request`,
-:class:`~flask.session` and :class:`~flask.g` [#]_ objects
-as well as the :func:`~flask.get_flashed_messages` function.
+Inside templates you also have access to the :data:`~flask.Flask.config`,
+:class:`~flask.request`, :class:`~flask.session` and :class:`~flask.g` [#]_ objects
+as well as the :func:`~flask.url_for` and :func:`~flask.get_flashed_messages` functions.
Templates are especially useful if inheritance is used. If you want to
know how that works, see :doc:`patterns/templateinheritance`. Basically
diff --git a/docs/templating.rst b/docs/templating.rst
index b0964df8..dcc757c3 100644
--- a/docs/templating.rst
+++ b/docs/templating.rst
@@ -37,7 +37,7 @@ by default:
.. data:: config
:noindex:
- The current configuration object (:data:`flask.config`)
+ The current configuration object (:data:`flask.Flask.config`)
.. versionadded:: 0.6
diff --git a/docs/testing.rst b/docs/testing.rst
index 2fedc600..061d46d2 100644
--- a/docs/testing.rst
+++ b/docs/testing.rst
@@ -48,20 +48,21 @@ the application for testing and initializes a new database::
import pytest
from flaskr import create_app
+ from flaskr.db import init_db
@pytest.fixture
def client():
- db_fd, flaskr.app.config['DATABASE'] = tempfile.mkstemp()
- flaskr.app.config['TESTING'] = True
+ db_fd, db_path = tempfile.mkstemp()
+ app = create_app({'TESTING': True, 'DATABASE': db_path})
- with flaskr.app.test_client() as client:
- with flaskr.app.app_context():
- flaskr.init_db()
+ with app.test_client() as client:
+ with app.app_context():
+ init_db()
yield client
os.close(db_fd)
- os.unlink(flaskr.app.config['DATABASE'])
+ os.unlink(db_path)
This client fixture will be called by each individual test. It gives us a
simple interface to the application, where we can trigger test requests to the
@@ -224,13 +225,13 @@ temporarily. With this you can access the :class:`~flask.request`,
:class:`~flask.g` and :class:`~flask.session` objects like in view
functions. Here is a full example that demonstrates this approach::
- import flask
+ from flask import Flask, request
- app = flask.Flask(__name__)
+ app = Flask(__name__)
with app.test_request_context('/?name=Peter'):
- assert flask.request.path == '/'
- assert flask.request.args['name'] == 'Peter'
+ assert request.path == '/'
+ assert request.args['name'] == 'Peter'
All the other objects that are context bound can be used in the same
way.
@@ -247,7 +248,7 @@ the test request context leaves the ``with`` block. If you do want the
:meth:`~flask.Flask.before_request` functions to be called as well, you
need to call :meth:`~flask.Flask.preprocess_request` yourself::
- app = flask.Flask(__name__)
+ app = Flask(__name__)
with app.test_request_context('/?name=Peter'):
app.preprocess_request()
@@ -260,7 +261,7 @@ If you want to call the :meth:`~flask.Flask.after_request` functions you
need to call into :meth:`~flask.Flask.process_response` which however
requires that you pass it a response object::
- app = flask.Flask(__name__)
+ app = Flask(__name__)
with app.test_request_context('/?name=Peter'):
resp = Response('...')
@@ -329,7 +330,7 @@ context around for a little longer so that additional introspection can
happen. With Flask 0.4 this is possible by using the
:meth:`~flask.Flask.test_client` with a ``with`` block::
- app = flask.Flask(__name__)
+ app = Flask(__name__)
with app.test_client() as c:
rv = c.get('/?tequila=42')
@@ -353,7 +354,7 @@ keep the context around and access :data:`flask.session`::
with app.test_client() as c:
rv = c.get('/')
- assert flask.session['foo'] == 42
+ assert session['foo'] == 42
This however does not make it possible to also modify the session or to
access the session before a request was fired. Starting with Flask 0.8 we
diff --git a/src/flask/app.py b/src/flask/app.py
index cacb40a5..22cc9abc 100644
--- a/src/flask/app.py
+++ b/src/flask/app.py
@@ -61,7 +61,6 @@ from .templating import Environment
from .typing import AfterRequestCallable
from .typing import BeforeFirstRequestCallable
from .typing import BeforeRequestCallable
-from .typing import ErrorHandlerCallable
from .typing import ResponseReturnValue
from .typing import TeardownCallable
from .typing import TemplateContextProcessorCallable
@@ -78,6 +77,7 @@ if t.TYPE_CHECKING:
from .blueprints import Blueprint
from .testing import FlaskClient
from .testing import FlaskCliRunner
+ from .typing import ErrorHandlerCallable
if sys.version_info >= (3, 8):
iscoroutinefunction = inspect.iscoroutinefunction
@@ -1268,7 +1268,9 @@ class Flask(Scaffold):
self.shell_context_processors.append(f)
return f
- def _find_error_handler(self, e: Exception) -> t.Optional[ErrorHandlerCallable]:
+ def _find_error_handler(
+ self, e: Exception
+ ) -> t.Optional["ErrorHandlerCallable[Exception]"]:
"""Return a registered error handler for an exception in this order:
blueprint handler for a specific code, app handler for a specific code,
blueprint handler for an exception class, app handler for an exception
@@ -1276,7 +1278,7 @@ class Flask(Scaffold):
"""
exc_class, code = self._get_exc_class_and_code(type(e))
- for c in [code, None]:
+ for c in [code, None] if code is not None else [None]:
for name in chain(request.blueprints, [None]):
handler_map = self.error_handler_spec[name][c]
diff --git a/src/flask/blueprints.py b/src/flask/blueprints.py
index 883fc2ff..c8ce67a4 100644
--- a/src/flask/blueprints.py
+++ b/src/flask/blueprints.py
@@ -8,7 +8,6 @@ from .scaffold import Scaffold
from .typing import AfterRequestCallable
from .typing import BeforeFirstRequestCallable
from .typing import BeforeRequestCallable
-from .typing import ErrorHandlerCallable
from .typing import TeardownCallable
from .typing import TemplateContextProcessorCallable
from .typing import TemplateFilterCallable
@@ -19,6 +18,7 @@ from .typing import URLValuePreprocessorCallable
if t.TYPE_CHECKING:
from .app import Flask
+ from .typing import ErrorHandlerCallable
DeferredSetupFunction = t.Callable[["BlueprintSetupState"], t.Callable]
@@ -293,7 +293,6 @@ class Blueprint(Scaffold):
Registering the same blueprint with the same name multiple
times is deprecated and will become an error in Flask 2.1.
"""
- first_registration = not any(bp is self for bp in app.blueprints.values())
name_prefix = options.get("name_prefix", "")
self_name = options.get("name", self.name)
name = f"{name_prefix}.{self_name}".lstrip(".")
@@ -318,9 +317,12 @@ class Blueprint(Scaffold):
stacklevel=4,
)
+ first_bp_registration = not any(bp is self for bp in app.blueprints.values())
+ first_name_registration = name not in app.blueprints
+
app.blueprints[name] = self
self._got_registered_once = True
- state = self.make_setup_state(app, options, first_registration)
+ state = self.make_setup_state(app, options, first_bp_registration)
if self.has_static_folder:
state.add_url_rule(
@@ -330,7 +332,7 @@ class Blueprint(Scaffold):
)
# Merge blueprint data into parent.
- if first_registration:
+ if first_bp_registration or first_name_registration:
def extend(bp_dict, parent_dict):
for key, values in bp_dict.items():
@@ -581,7 +583,9 @@ class Blueprint(Scaffold):
handler is used for all requests, even if outside of the blueprint.
"""
- def decorator(f: ErrorHandlerCallable) -> ErrorHandlerCallable:
+ def decorator(
+ f: "ErrorHandlerCallable[Exception]",
+ ) -> "ErrorHandlerCallable[Exception]":
self.record_once(lambda s: s.app.errorhandler(code)(f))
return f
diff --git a/src/flask/scaffold.py b/src/flask/scaffold.py
index 239bc46a..e80e1915 100644
--- a/src/flask/scaffold.py
+++ b/src/flask/scaffold.py
@@ -21,7 +21,7 @@ from .templating import _default_template_ctx_processor
from .typing import AfterRequestCallable
from .typing import AppOrBlueprintKey
from .typing import BeforeRequestCallable
-from .typing import ErrorHandlerCallable
+from .typing import GenericException
from .typing import TeardownCallable
from .typing import TemplateContextProcessorCallable
from .typing import URLDefaultCallable
@@ -29,6 +29,7 @@ from .typing import URLValuePreprocessorCallable
if t.TYPE_CHECKING:
from .wrappers import Response
+ from .typing import ErrorHandlerCallable
# a singleton sentinel value for parameter defaults
_sentinel = object()
@@ -144,7 +145,10 @@ class Scaffold:
#: directly and its format may change at any time.
self.error_handler_spec: t.Dict[
AppOrBlueprintKey,
- t.Dict[t.Optional[int], t.Dict[t.Type[Exception], ErrorHandlerCallable]],
+ t.Dict[
+ t.Optional[int],
+ t.Dict[t.Type[Exception], "ErrorHandlerCallable[Exception]"],
+ ],
] = defaultdict(lambda: defaultdict(dict))
#: A data structure of functions to call at the beginning of
@@ -643,8 +647,11 @@ class Scaffold:
@setupmethod
def errorhandler(
- self, code_or_exception: t.Union[t.Type[Exception], int]
- ) -> t.Callable[[ErrorHandlerCallable], ErrorHandlerCallable]:
+ self, code_or_exception: t.Union[t.Type[GenericException], int]
+ ) -> t.Callable[
+ ["ErrorHandlerCallable[GenericException]"],
+ "ErrorHandlerCallable[GenericException]",
+ ]:
"""Register a function to handle errors by code or exception class.
A decorator that is used to register a function given an
@@ -674,7 +681,9 @@ class Scaffold:
an arbitrary exception
"""
- def decorator(f: ErrorHandlerCallable) -> ErrorHandlerCallable:
+ def decorator(
+ f: "ErrorHandlerCallable[GenericException]",
+ ) -> "ErrorHandlerCallable[GenericException]":
self.register_error_handler(code_or_exception, f)
return f
@@ -683,8 +692,8 @@ class Scaffold:
@setupmethod
def register_error_handler(
self,
- code_or_exception: t.Union[t.Type[Exception], int],
- f: ErrorHandlerCallable,
+ code_or_exception: t.Union[t.Type[GenericException], int],
+ f: "ErrorHandlerCallable[GenericException]",
) -> None:
"""Alternative error attach function to the :meth:`errorhandler`
decorator that is more straightforward to use for non decorator
@@ -708,7 +717,9 @@ class Scaffold:
" instead."
)
- self.error_handler_spec[None][code][exc_class] = f
+ self.error_handler_spec[None][code][exc_class] = t.cast(
+ "ErrorHandlerCallable[Exception]", f
+ )
@staticmethod
def _get_exc_class_and_code(
diff --git a/src/flask/typing.py b/src/flask/typing.py
index b1a6cbdc..f1c84670 100644
--- a/src/flask/typing.py
+++ b/src/flask/typing.py
@@ -33,11 +33,12 @@ ResponseReturnValue = t.Union[
"WSGIApplication",
]
+GenericException = t.TypeVar("GenericException", bound=Exception, contravariant=True)
+
AppOrBlueprintKey = t.Optional[str] # The App key is None, whereas blueprints are named
AfterRequestCallable = t.Callable[["Response"], "Response"]
BeforeFirstRequestCallable = t.Callable[[], None]
BeforeRequestCallable = t.Callable[[], t.Optional[ResponseReturnValue]]
-ErrorHandlerCallable = t.Callable[[Exception], ResponseReturnValue]
TeardownCallable = t.Callable[[t.Optional[BaseException]], None]
TemplateContextProcessorCallable = t.Callable[[], t.Dict[str, t.Any]]
TemplateFilterCallable = t.Callable[..., t.Any]
@@ -45,3 +46,11 @@ TemplateGlobalCallable = t.Callable[..., t.Any]
TemplateTestCallable = t.Callable[..., bool]
URLDefaultCallable = t.Callable[[str, dict], None]
URLValuePreprocessorCallable = t.Callable[[t.Optional[str], t.Optional[dict]], None]
+
+
+if t.TYPE_CHECKING:
+ import typing_extensions as te
+
+ class ErrorHandlerCallable(te.Protocol[GenericException]):
+ def __call__(self, error: GenericException) -> ResponseReturnValue:
+ ...
diff --git a/src/flask/views.py b/src/flask/views.py
index 339ffa18..1bd5c68b 100644
--- a/src/flask/views.py
+++ b/src/flask/views.py
@@ -1,5 +1,6 @@
import typing as t
+from .globals import current_app
from .globals import request
from .typing import ResponseReturnValue
@@ -80,7 +81,7 @@ class View:
def view(*args: t.Any, **kwargs: t.Any) -> ResponseReturnValue:
self = view.view_class(*class_args, **class_kwargs) # type: ignore
- return self.dispatch_request(*args, **kwargs)
+ return current_app.ensure_sync(self.dispatch_request)(*args, **kwargs)
if cls.decorators:
view.__name__ = name
@@ -154,4 +155,4 @@ class MethodView(View, metaclass=MethodViewType):
meth = getattr(self, "get", None)
assert meth is not None, f"Unimplemented method {request.method!r}"
- return meth(*args, **kwargs)
+ return current_app.ensure_sync(meth)(*args, **kwargs)
diff --git a/tests/test_async.py b/tests/test_async.py
index 26a91118..8276c4a8 100644
--- a/tests/test_async.py
+++ b/tests/test_async.py
@@ -6,6 +6,8 @@ import pytest
from flask import Blueprint
from flask import Flask
from flask import request
+from flask.views import MethodView
+from flask.views import View
pytest.importorskip("asgiref")
@@ -18,6 +20,24 @@ class BlueprintError(Exception):
pass
+class AsyncView(View):
+ methods = ["GET", "POST"]
+
+ async def dispatch_request(self):
+ await asyncio.sleep(0)
+ return request.method
+
+
+class AsyncMethodView(MethodView):
+ async def get(self):
+ await asyncio.sleep(0)
+ return "GET"
+
+ async def post(self):
+ await asyncio.sleep(0)
+ return "POST"
+
+
@pytest.fixture(name="async_app")
def _async_app():
app = Flask(__name__)
@@ -53,11 +73,14 @@ def _async_app():
app.register_blueprint(blueprint, url_prefix="/bp")
+ app.add_url_rule("/view", view_func=AsyncView.as_view("view"))
+ app.add_url_rule("/methodview", view_func=AsyncMethodView.as_view("methodview"))
+
return app
@pytest.mark.skipif(sys.version_info < (3, 7), reason="requires Python >= 3.7")
-@pytest.mark.parametrize("path", ["/", "/home", "/bp/"])
+@pytest.mark.parametrize("path", ["/", "/home", "/bp/", "/view", "/methodview"])
def test_async_route(path, async_app):
test_client = async_app.test_client()
response = test_client.get(path)
diff --git a/tests/test_blueprints.py b/tests/test_blueprints.py
index 088ad779..a124c612 100644
--- a/tests/test_blueprints.py
+++ b/tests/test_blueprints.py
@@ -899,6 +899,14 @@ def test_blueprint_renaming(app, client) -> None:
def index():
return flask.request.endpoint
+ @bp.get("/error")
+ def error():
+ flask.abort(403)
+
+ @bp.errorhandler(403)
+ def forbidden(_: Exception):
+ return "Error", 403
+
@bp2.get("/")
def index2():
return flask.request.endpoint
@@ -911,3 +919,5 @@ def test_blueprint_renaming(app, client) -> None:
assert client.get("/b/").data == b"alt.index"
assert client.get("/a/a/").data == b"bp.sub.index2"
assert client.get("/b/a/").data == b"alt.sub.index2"
+ assert client.get("/a/error").data == b"Error"
+ assert client.get("/b/error").data == b"Error"