Merge remote-tracking branch 'origin/2.0.x' into main

This commit is contained in:
Grey Li 2021-06-26 23:35:24 +08:00
commit ba6db2e307
15 changed files with 118 additions and 43 deletions

View file

@ -13,11 +13,14 @@ Version 2.0.2
Unreleased 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`` - Fix type annotation for ``before_request`` and ``before_app_request``
decorators. :issue:`4104` decorators. :issue:`4104`
- Fixed the issue where typing requires template global - Fixed the issue where typing requires template global
decorators to accept functions with no arguments. :issue:`4098` 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 Version 2.0.1

View file

@ -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 following resources for questions about using Flask or issues with your
own code: own code:
- The ``#get-help`` channel on our Discord chat: - The ``#questions`` channel on our Discord chat:
https://discord.gg/pallets https://discord.gg/pallets
- The mailing list flask@python.org for long term discussion or larger - The mailing list flask@python.org for long term discussion or larger
issues. issues.
- Ask on `Stack Overflow`_. Search with Google first using: - Ask on `Stack Overflow`_. Search with Google first using:
``site:stackoverflow.com flask {search term, exception message, etc.}`` ``site:stackoverflow.com flask {search term, exception message, etc.}``
- Ask on our `GitHub Discussions`_.
.. _Stack Overflow: https://stackoverflow.com/questions/tagged/flask?tab=Frequent .. _Stack Overflow: https://stackoverflow.com/questions/tagged/flask?tab=Frequent
.. _GitHub Discussions: https://github.com/pallets/flask/discussions
Reporting issues Reporting issues
@ -92,7 +94,7 @@ First time setup
.. code-block:: text .. code-block:: text
git remote add fork https://github.com/{username}/flask $ git remote add fork https://github.com/{username}/flask
- Create a virtualenv. - Create a virtualenv.

View file

@ -7,7 +7,8 @@ Using ``async`` and ``await``
Routes, error handlers, before request, after request, and teardown Routes, error handlers, before request, after request, and teardown
functions can all be coroutine functions if Flask is installed with the 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``. defined with ``async def`` and use ``await``.
.. code-block:: python .. code-block:: python
@ -17,6 +18,12 @@ defined with ``async def`` and use ``await``.
data = await async_db_query(...) data = await async_db_query(...)
return jsonify(data) 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 .. admonition:: Using ``async`` on Windows on Python 3.8
Python 3.8 has a bug related to asyncio on Windows. If you encounter Python 3.8 has a bug related to asyncio on Windows. If you encounter

View file

@ -8,6 +8,8 @@ Python Version
We recommend using the latest version of Python. Flask supports Python We recommend using the latest version of Python. Flask supports Python
3.6 and newer. 3.6 and newer.
``async`` support in Flask requires Python 3.7+ for ``contextvars.ContextVar``.
Dependencies Dependencies
------------ ------------

View file

@ -64,7 +64,7 @@ An example task
Let's write a task that adds two numbers together and returns the result. We 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`` 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 from flask import Flask

View file

@ -444,9 +444,9 @@ Here is an example template:
<h1>Hello, World!</h1> <h1>Hello, World!</h1>
{% endif %} {% endif %}
Inside templates you also have access to the :class:`~flask.request`, Inside templates you also have access to the :data:`~flask.Flask.config`,
:class:`~flask.session` and :class:`~flask.g` [#]_ objects :class:`~flask.request`, :class:`~flask.session` and :class:`~flask.g` [#]_ objects
as well as the :func:`~flask.get_flashed_messages` function. 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 Templates are especially useful if inheritance is used. If you want to
know how that works, see :doc:`patterns/templateinheritance`. Basically know how that works, see :doc:`patterns/templateinheritance`. Basically

View file

@ -37,7 +37,7 @@ by default:
.. data:: config .. data:: config
:noindex: :noindex:
The current configuration object (:data:`flask.config`) The current configuration object (:data:`flask.Flask.config`)
.. versionadded:: 0.6 .. versionadded:: 0.6

View file

@ -48,20 +48,21 @@ the application for testing and initializes a new database::
import pytest import pytest
from flaskr import create_app from flaskr import create_app
from flaskr.db import init_db
@pytest.fixture @pytest.fixture
def client(): def client():
db_fd, flaskr.app.config['DATABASE'] = tempfile.mkstemp() db_fd, db_path = tempfile.mkstemp()
flaskr.app.config['TESTING'] = True app = create_app({'TESTING': True, 'DATABASE': db_path})
with flaskr.app.test_client() as client: with app.test_client() as client:
with flaskr.app.app_context(): with app.app_context():
flaskr.init_db() init_db()
yield client yield client
os.close(db_fd) 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 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 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 :class:`~flask.g` and :class:`~flask.session` objects like in view
functions. Here is a full example that demonstrates this approach:: 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'): with app.test_request_context('/?name=Peter'):
assert flask.request.path == '/' assert request.path == '/'
assert flask.request.args['name'] == 'Peter' assert request.args['name'] == 'Peter'
All the other objects that are context bound can be used in the same All the other objects that are context bound can be used in the same
way. 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 :meth:`~flask.Flask.before_request` functions to be called as well, you
need to call :meth:`~flask.Flask.preprocess_request` yourself:: need to call :meth:`~flask.Flask.preprocess_request` yourself::
app = flask.Flask(__name__) app = Flask(__name__)
with app.test_request_context('/?name=Peter'): with app.test_request_context('/?name=Peter'):
app.preprocess_request() 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 need to call into :meth:`~flask.Flask.process_response` which however
requires that you pass it a response object:: requires that you pass it a response object::
app = flask.Flask(__name__) app = Flask(__name__)
with app.test_request_context('/?name=Peter'): with app.test_request_context('/?name=Peter'):
resp = Response('...') 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 happen. With Flask 0.4 this is possible by using the
:meth:`~flask.Flask.test_client` with a ``with`` block:: :meth:`~flask.Flask.test_client` with a ``with`` block::
app = flask.Flask(__name__) app = Flask(__name__)
with app.test_client() as c: with app.test_client() as c:
rv = c.get('/?tequila=42') rv = c.get('/?tequila=42')
@ -353,7 +354,7 @@ keep the context around and access :data:`flask.session`::
with app.test_client() as c: with app.test_client() as c:
rv = c.get('/') 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 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 access the session before a request was fired. Starting with Flask 0.8 we

View file

@ -61,7 +61,6 @@ from .templating import Environment
from .typing import AfterRequestCallable from .typing import AfterRequestCallable
from .typing import BeforeFirstRequestCallable from .typing import BeforeFirstRequestCallable
from .typing import BeforeRequestCallable from .typing import BeforeRequestCallable
from .typing import ErrorHandlerCallable
from .typing import ResponseReturnValue from .typing import ResponseReturnValue
from .typing import TeardownCallable from .typing import TeardownCallable
from .typing import TemplateContextProcessorCallable from .typing import TemplateContextProcessorCallable
@ -78,6 +77,7 @@ if t.TYPE_CHECKING:
from .blueprints import Blueprint from .blueprints import Blueprint
from .testing import FlaskClient from .testing import FlaskClient
from .testing import FlaskCliRunner from .testing import FlaskCliRunner
from .typing import ErrorHandlerCallable
if sys.version_info >= (3, 8): if sys.version_info >= (3, 8):
iscoroutinefunction = inspect.iscoroutinefunction iscoroutinefunction = inspect.iscoroutinefunction
@ -1268,7 +1268,9 @@ class Flask(Scaffold):
self.shell_context_processors.append(f) self.shell_context_processors.append(f)
return 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: """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 a specific code, app handler for a specific code,
blueprint handler for an exception class, app handler for an exception 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)) 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]): for name in chain(request.blueprints, [None]):
handler_map = self.error_handler_spec[name][c] handler_map = self.error_handler_spec[name][c]

View file

@ -8,7 +8,6 @@ from .scaffold import Scaffold
from .typing import AfterRequestCallable from .typing import AfterRequestCallable
from .typing import BeforeFirstRequestCallable from .typing import BeforeFirstRequestCallable
from .typing import BeforeRequestCallable from .typing import BeforeRequestCallable
from .typing import ErrorHandlerCallable
from .typing import TeardownCallable from .typing import TeardownCallable
from .typing import TemplateContextProcessorCallable from .typing import TemplateContextProcessorCallable
from .typing import TemplateFilterCallable from .typing import TemplateFilterCallable
@ -19,6 +18,7 @@ from .typing import URLValuePreprocessorCallable
if t.TYPE_CHECKING: if t.TYPE_CHECKING:
from .app import Flask from .app import Flask
from .typing import ErrorHandlerCallable
DeferredSetupFunction = t.Callable[["BlueprintSetupState"], t.Callable] DeferredSetupFunction = t.Callable[["BlueprintSetupState"], t.Callable]
@ -293,7 +293,6 @@ class Blueprint(Scaffold):
Registering the same blueprint with the same name multiple Registering the same blueprint with the same name multiple
times is deprecated and will become an error in Flask 2.1. 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", "") name_prefix = options.get("name_prefix", "")
self_name = options.get("name", self.name) self_name = options.get("name", self.name)
name = f"{name_prefix}.{self_name}".lstrip(".") name = f"{name_prefix}.{self_name}".lstrip(".")
@ -318,9 +317,12 @@ class Blueprint(Scaffold):
stacklevel=4, 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 app.blueprints[name] = self
self._got_registered_once = True 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: if self.has_static_folder:
state.add_url_rule( state.add_url_rule(
@ -330,7 +332,7 @@ class Blueprint(Scaffold):
) )
# Merge blueprint data into parent. # Merge blueprint data into parent.
if first_registration: if first_bp_registration or first_name_registration:
def extend(bp_dict, parent_dict): def extend(bp_dict, parent_dict):
for key, values in bp_dict.items(): 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. 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)) self.record_once(lambda s: s.app.errorhandler(code)(f))
return f return f

View file

@ -21,7 +21,7 @@ from .templating import _default_template_ctx_processor
from .typing import AfterRequestCallable from .typing import AfterRequestCallable
from .typing import AppOrBlueprintKey from .typing import AppOrBlueprintKey
from .typing import BeforeRequestCallable from .typing import BeforeRequestCallable
from .typing import ErrorHandlerCallable from .typing import GenericException
from .typing import TeardownCallable from .typing import TeardownCallable
from .typing import TemplateContextProcessorCallable from .typing import TemplateContextProcessorCallable
from .typing import URLDefaultCallable from .typing import URLDefaultCallable
@ -29,6 +29,7 @@ from .typing import URLValuePreprocessorCallable
if t.TYPE_CHECKING: if t.TYPE_CHECKING:
from .wrappers import Response from .wrappers import Response
from .typing import ErrorHandlerCallable
# a singleton sentinel value for parameter defaults # a singleton sentinel value for parameter defaults
_sentinel = object() _sentinel = object()
@ -144,7 +145,10 @@ class Scaffold:
#: directly and its format may change at any time. #: directly and its format may change at any time.
self.error_handler_spec: t.Dict[ self.error_handler_spec: t.Dict[
AppOrBlueprintKey, 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)) ] = defaultdict(lambda: defaultdict(dict))
#: A data structure of functions to call at the beginning of #: A data structure of functions to call at the beginning of
@ -643,8 +647,11 @@ class Scaffold:
@setupmethod @setupmethod
def errorhandler( def errorhandler(
self, code_or_exception: t.Union[t.Type[Exception], int] self, code_or_exception: t.Union[t.Type[GenericException], int]
) -> t.Callable[[ErrorHandlerCallable], ErrorHandlerCallable]: ) -> t.Callable[
["ErrorHandlerCallable[GenericException]"],
"ErrorHandlerCallable[GenericException]",
]:
"""Register a function to handle errors by code or exception class. """Register a function to handle errors by code or exception class.
A decorator that is used to register a function given an A decorator that is used to register a function given an
@ -674,7 +681,9 @@ class Scaffold:
an arbitrary exception an arbitrary exception
""" """
def decorator(f: ErrorHandlerCallable) -> ErrorHandlerCallable: def decorator(
f: "ErrorHandlerCallable[GenericException]",
) -> "ErrorHandlerCallable[GenericException]":
self.register_error_handler(code_or_exception, f) self.register_error_handler(code_or_exception, f)
return f return f
@ -683,8 +692,8 @@ class Scaffold:
@setupmethod @setupmethod
def register_error_handler( def register_error_handler(
self, self,
code_or_exception: t.Union[t.Type[Exception], int], code_or_exception: t.Union[t.Type[GenericException], int],
f: ErrorHandlerCallable, f: "ErrorHandlerCallable[GenericException]",
) -> None: ) -> None:
"""Alternative error attach function to the :meth:`errorhandler` """Alternative error attach function to the :meth:`errorhandler`
decorator that is more straightforward to use for non decorator decorator that is more straightforward to use for non decorator
@ -708,7 +717,9 @@ class Scaffold:
" instead." " instead."
) )
self.error_handler_spec[None][code][exc_class] = f self.error_handler_spec[None][code][exc_class] = t.cast(
"ErrorHandlerCallable[Exception]", f
)
@staticmethod @staticmethod
def _get_exc_class_and_code( def _get_exc_class_and_code(

View file

@ -33,11 +33,12 @@ ResponseReturnValue = t.Union[
"WSGIApplication", "WSGIApplication",
] ]
GenericException = t.TypeVar("GenericException", bound=Exception, contravariant=True)
AppOrBlueprintKey = t.Optional[str] # The App key is None, whereas blueprints are named AppOrBlueprintKey = t.Optional[str] # The App key is None, whereas blueprints are named
AfterRequestCallable = t.Callable[["Response"], "Response"] AfterRequestCallable = t.Callable[["Response"], "Response"]
BeforeFirstRequestCallable = t.Callable[[], None] BeforeFirstRequestCallable = t.Callable[[], None]
BeforeRequestCallable = t.Callable[[], t.Optional[ResponseReturnValue]] BeforeRequestCallable = t.Callable[[], t.Optional[ResponseReturnValue]]
ErrorHandlerCallable = t.Callable[[Exception], ResponseReturnValue]
TeardownCallable = t.Callable[[t.Optional[BaseException]], None] TeardownCallable = t.Callable[[t.Optional[BaseException]], None]
TemplateContextProcessorCallable = t.Callable[[], t.Dict[str, t.Any]] TemplateContextProcessorCallable = t.Callable[[], t.Dict[str, t.Any]]
TemplateFilterCallable = t.Callable[..., t.Any] TemplateFilterCallable = t.Callable[..., t.Any]
@ -45,3 +46,11 @@ TemplateGlobalCallable = t.Callable[..., t.Any]
TemplateTestCallable = t.Callable[..., bool] TemplateTestCallable = t.Callable[..., bool]
URLDefaultCallable = t.Callable[[str, dict], None] URLDefaultCallable = t.Callable[[str, dict], None]
URLValuePreprocessorCallable = t.Callable[[t.Optional[str], t.Optional[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:
...

View file

@ -1,5 +1,6 @@
import typing as t import typing as t
from .globals import current_app
from .globals import request from .globals import request
from .typing import ResponseReturnValue from .typing import ResponseReturnValue
@ -80,7 +81,7 @@ class View:
def view(*args: t.Any, **kwargs: t.Any) -> ResponseReturnValue: def view(*args: t.Any, **kwargs: t.Any) -> ResponseReturnValue:
self = view.view_class(*class_args, **class_kwargs) # type: ignore 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: if cls.decorators:
view.__name__ = name view.__name__ = name
@ -154,4 +155,4 @@ class MethodView(View, metaclass=MethodViewType):
meth = getattr(self, "get", None) meth = getattr(self, "get", None)
assert meth is not None, f"Unimplemented method {request.method!r}" assert meth is not None, f"Unimplemented method {request.method!r}"
return meth(*args, **kwargs) return current_app.ensure_sync(meth)(*args, **kwargs)

View file

@ -6,6 +6,8 @@ import pytest
from flask import Blueprint from flask import Blueprint
from flask import Flask from flask import Flask
from flask import request from flask import request
from flask.views import MethodView
from flask.views import View
pytest.importorskip("asgiref") pytest.importorskip("asgiref")
@ -18,6 +20,24 @@ class BlueprintError(Exception):
pass 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") @pytest.fixture(name="async_app")
def _async_app(): def _async_app():
app = Flask(__name__) app = Flask(__name__)
@ -53,11 +73,14 @@ def _async_app():
app.register_blueprint(blueprint, url_prefix="/bp") 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 return app
@pytest.mark.skipif(sys.version_info < (3, 7), reason="requires Python >= 3.7") @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): def test_async_route(path, async_app):
test_client = async_app.test_client() test_client = async_app.test_client()
response = test_client.get(path) response = test_client.get(path)

View file

@ -899,6 +899,14 @@ def test_blueprint_renaming(app, client) -> None:
def index(): def index():
return flask.request.endpoint return flask.request.endpoint
@bp.get("/error")
def error():
flask.abort(403)
@bp.errorhandler(403)
def forbidden(_: Exception):
return "Error", 403
@bp2.get("/") @bp2.get("/")
def index2(): def index2():
return flask.request.endpoint 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("/b/").data == b"alt.index"
assert client.get("/a/a/").data == b"bp.sub.index2" assert client.get("/a/a/").data == b"bp.sub.index2"
assert client.get("/b/a/").data == b"alt.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"