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

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

View file

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

View file

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

View file

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

View file

@ -444,9 +444,9 @@ Here is an example template:
<h1>Hello, World!</h1>
{% 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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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