forked from orbit-oss/flask
Merge remote-tracking branch 'origin/2.0.x' into main
This commit is contained in:
commit
ba6db2e307
15 changed files with 118 additions and 43 deletions
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
------------
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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]
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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(
|
||||
|
|
|
|||
|
|
@ -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:
|
||||
...
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue