update changes.rst

This commit is contained in:
Rohan-Salwan 2021-10-06 03:58:27 +05:30
commit 9287b792c4
20 changed files with 199 additions and 114 deletions

View file

@ -2,7 +2,7 @@ ci:
autoupdate_schedule: monthly autoupdate_schedule: monthly
repos: repos:
- repo: https://github.com/asottile/pyupgrade - repo: https://github.com/asottile/pyupgrade
rev: v2.25.0 rev: v2.29.0
hooks: hooks:
- id: pyupgrade - id: pyupgrade
args: ["--py36-plus"] args: ["--py36-plus"]
@ -14,7 +14,7 @@ repos:
files: "^(?!examples/)" files: "^(?!examples/)"
args: ["--application-directories", "src"] args: ["--application-directories", "src"]
- repo: https://github.com/psf/black - repo: https://github.com/psf/black
rev: 21.8b0 rev: 21.9b0
hooks: hooks:
- id: black - id: black
- repo: https://github.com/PyCQA/flake8 - repo: https://github.com/PyCQA/flake8

View file

@ -3,7 +3,7 @@
Version 2.0.2 Version 2.0.2
------------- -------------
Unreleased Released 2021-10-04
- Fix type annotation for ``teardown_*`` methods. :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``
@ -21,6 +21,9 @@ Unreleased
:issue:`4096` :issue:`4096`
- The CLI loader handles ``**kwargs`` in a ``create_app`` function. - The CLI loader handles ``**kwargs`` in a ``create_app`` function.
:issue:`4170` :issue:`4170`
- Fix the order of ``before_request`` and other callbacks that trigger
before the view returns. They are called from the app down to the
closest nested blueprint. :issue:`4229`
- Fix Unable to extend FlaskClient with follow_redirects. :issue:`3396` - Fix Unable to extend FlaskClient with follow_redirects. :issue:`3396`
Version 2.0.1 Version 2.0.1

View file

@ -10,7 +10,7 @@ following conditions are met:
1. Redistributions of source code must retain the above copyright 1. Redistributions of source code must retain the above copyright
notice and this list of conditions. notice and this list of conditions.
3. Neither the name of the copyright holder nor the names of its 2. Neither the name of the copyright holder nor the names of its
contributors may be used to endorse or promote products derived from contributors may be used to endorse or promote products derived from
this software without specific prior written permission. this software without specific prior written permission.

View file

@ -8,7 +8,7 @@ a request, CLI command, or other activity. Rather than passing the
application around to each function, the :data:`current_app` and application around to each function, the :data:`current_app` and
:data:`g` proxies are accessed instead. :data:`g` proxies are accessed instead.
This is similar to the :doc:`/reqcontext`, which keeps track of This is similar to :doc:`/reqcontext`, which keeps track of
request-level data during a request. A corresponding application context request-level data during a request. A corresponding application context
is pushed when a request context is pushed. is pushed when a request context is pushed.

View file

@ -69,7 +69,7 @@ enables the debugger and reloader.
``FLASK_ENV`` can only be set as an environment variable. When running ``FLASK_ENV`` can only be set as an environment variable. When running
from Python code, passing ``debug=True`` enables debug mode, which is from Python code, passing ``debug=True`` enables debug mode, which is
mostly equivalent. Debug mode can be controled separately from mostly equivalent. Debug mode can be controlled separately from
``FLASK_ENV`` with the ``FLASK_DEBUG`` environment variable as well. ``FLASK_ENV`` with the ``FLASK_DEBUG`` environment variable as well.
.. code-block:: python .. code-block:: python

View file

@ -19,7 +19,7 @@ forms.
fun. You can get it from `PyPI fun. You can get it from `PyPI
<https://pypi.org/project/Flask-WTF/>`_. <https://pypi.org/project/Flask-WTF/>`_.
.. _Flask-WTF: https://flask-wtf.readthedocs.io/en/stable/ .. _Flask-WTF: https://flask-wtf.readthedocs.io/
The Forms The Forms
--------- ---------

View file

@ -8,7 +8,7 @@ request. Rather than passing the request object to each function that
runs during a request, the :data:`request` and :data:`session` proxies runs during a request, the :data:`request` and :data:`session` proxies
are accessed instead. are accessed instead.
This is similar to the :doc:`/appcontext`, which keeps track of the This is similar to :doc:`/appcontext`, which keeps track of the
application-level data independent of a request. A corresponding application-level data independent of a request. A corresponding
application context is pushed when a request context is pushed. application context is pushed when a request context is pushed.
@ -33,8 +33,8 @@ Lifetime of the Context
----------------------- -----------------------
When a Flask application begins handling a request, it pushes a request When a Flask application begins handling a request, it pushes a request
context, which also pushes an :doc:`/appcontext`. When the request ends context, which also pushes an :doc:`app context </appcontext>`. When the
it pops the request context then the application context. request ends it pops the request context then the application context.
The context is unique to each thread (or other worker type). The context is unique to each thread (or other worker type).
:data:`request` cannot be passed to another thread, the other thread :data:`request` cannot be passed to another thread, the other thread

View file

@ -21,8 +21,7 @@ that these functions are not only there for interactive shell usage, but
also for unit testing and other situations that require a faked request also for unit testing and other situations that require a faked request
context. context.
Generally it's recommended that you read the :doc:`reqcontext` Generally it's recommended that you read :doc:`reqcontext` first.
chapter of the documentation first.
Command Line Interface Command Line Interface
---------------------- ----------------------

View file

@ -22,25 +22,25 @@ cffi==1.14.6
# via cryptography # via cryptography
cfgv==3.3.1 cfgv==3.3.1
# via pre-commit # via pre-commit
charset-normalizer==2.0.4 charset-normalizer==2.0.6
# via requests # via requests
click==8.0.1 click==8.0.1
# via pip-tools # via pip-tools
cryptography==3.4.8 cryptography==35.0.0
# via -r requirements/typing.in # via -r requirements/typing.in
distlib==0.3.2 distlib==0.3.3
# via virtualenv # via virtualenv
docutils==0.16 docutils==0.16
# via # via
# sphinx # sphinx
# sphinx-tabs # sphinx-tabs
filelock==3.0.12 filelock==3.2.0
# via # via
# tox # tox
# virtualenv # virtualenv
greenlet==1.1.1 greenlet==1.1.2
# via -r requirements/tests.in # via -r requirements/tests.in
identify==2.2.13 identify==2.2.15
# via pre-commit # via pre-commit
idna==3.2 idna==3.2
# via requests # via requests
@ -68,9 +68,9 @@ pallets-sphinx-themes==2.0.1
# via -r requirements/docs.in # via -r requirements/docs.in
pep517==0.11.0 pep517==0.11.0
# via pip-tools # via pip-tools
pip-tools==6.2.0 pip-tools==6.3.0
# via -r requirements/dev.in # via -r requirements/dev.in
platformdirs==2.3.0 platformdirs==2.4.0
# via virtualenv # via virtualenv
pluggy==1.0.0 pluggy==1.0.0
# via # via
@ -106,7 +106,7 @@ six==1.16.0
# virtualenv # virtualenv
snowballstemmer==2.1.0 snowballstemmer==2.1.0
# via sphinx # via sphinx
sphinx==4.1.2 sphinx==4.2.0
# via # via
# -r requirements/docs.in # -r requirements/docs.in
# pallets-sphinx-themes # pallets-sphinx-themes
@ -139,19 +139,19 @@ toml==0.10.2
# tox # tox
tomli==1.2.1 tomli==1.2.1
# via pep517 # via pep517
tox==3.24.3 tox==3.24.4
# via -r requirements/dev.in # via -r requirements/dev.in
types-contextvars==0.1.4 types-contextvars==0.1.4
# via -r requirements/typing.in # via -r requirements/typing.in
types-dataclasses==0.1.7 types-dataclasses==0.1.7
# via -r requirements/typing.in # via -r requirements/typing.in
types-setuptools==57.0.2 types-setuptools==57.4.0
# via -r requirements/typing.in # via -r requirements/typing.in
typing-extensions==3.10.0.2 typing-extensions==3.10.0.2
# via mypy # via mypy
urllib3==1.26.6 urllib3==1.26.7
# via requests # via requests
virtualenv==20.7.2 virtualenv==20.8.1
# via # via
# pre-commit # pre-commit
# tox # tox

View file

@ -10,7 +10,7 @@ babel==2.9.1
# via sphinx # via sphinx
certifi==2021.5.30 certifi==2021.5.30
# via requests # via requests
charset-normalizer==2.0.4 charset-normalizer==2.0.6
# via requests # via requests
docutils==0.16 docutils==0.16
# via # via
@ -42,7 +42,7 @@ requests==2.26.0
# via sphinx # via sphinx
snowballstemmer==2.1.0 snowballstemmer==2.1.0
# via sphinx # via sphinx
sphinx==4.1.2 sphinx==4.2.0
# via # via
# -r requirements/docs.in # -r requirements/docs.in
# pallets-sphinx-themes # pallets-sphinx-themes
@ -67,7 +67,7 @@ sphinxcontrib-qthelp==1.0.3
# via sphinx # via sphinx
sphinxcontrib-serializinghtml==1.1.5 sphinxcontrib-serializinghtml==1.1.5
# via sphinx # via sphinx
urllib3==1.26.6 urllib3==1.26.7
# via requests # via requests
# The following packages are considered to be unsafe in a requirements file: # The following packages are considered to be unsafe in a requirements file:

View file

@ -10,7 +10,7 @@ attrs==21.2.0
# via pytest # via pytest
blinker==1.4 blinker==1.4
# via -r requirements/tests.in # via -r requirements/tests.in
greenlet==1.1.1 greenlet==1.1.2
# via -r requirements/tests.in # via -r requirements/tests.in
iniconfig==1.1.1 iniconfig==1.1.1
# via pytest # via pytest

View file

@ -6,7 +6,7 @@
# #
cffi==1.14.6 cffi==1.14.6
# via cryptography # via cryptography
cryptography==3.4.8 cryptography==35.0.0
# via -r requirements/typing.in # via -r requirements/typing.in
mypy==0.910 mypy==0.910
# via -r requirements/typing.in # via -r requirements/typing.in
@ -20,7 +20,7 @@ types-contextvars==0.1.4
# via -r requirements/typing.in # via -r requirements/typing.in
types-dataclasses==0.1.7 types-dataclasses==0.1.7
# via -r requirements/typing.in # via -r requirements/typing.in
types-setuptools==57.0.2 types-setuptools==57.4.0
# via -r requirements/typing.in # via -r requirements/typing.in
typing-extensions==3.10.0.2 typing-extensions==3.10.0.2
# via mypy # via mypy

View file

@ -43,4 +43,4 @@ from .signals import template_rendered as template_rendered
from .templating import render_template as render_template from .templating import render_template as render_template
from .templating import render_template_string as render_template_string from .templating import render_template_string as render_template_string
__version__ = "2.0.2.dev0" __version__ = "2.0.2"

View file

@ -58,17 +58,12 @@ from .signals import request_started
from .signals import request_tearing_down from .signals import request_tearing_down
from .templating import DispatchingJinjaLoader from .templating import DispatchingJinjaLoader
from .templating import Environment from .templating import Environment
from .typing import AfterRequestCallable
from .typing import BeforeFirstRequestCallable from .typing import BeforeFirstRequestCallable
from .typing import BeforeRequestCallable
from .typing import ResponseReturnValue from .typing import ResponseReturnValue
from .typing import TeardownCallable from .typing import TeardownCallable
from .typing import TemplateContextProcessorCallable
from .typing import TemplateFilterCallable from .typing import TemplateFilterCallable
from .typing import TemplateGlobalCallable from .typing import TemplateGlobalCallable
from .typing import TemplateTestCallable from .typing import TemplateTestCallable
from .typing import URLDefaultCallable
from .typing import URLValuePreprocessorCallable
from .wrappers import Request from .wrappers import Request
from .wrappers import Response from .wrappers import Response
@ -366,7 +361,8 @@ class Flask(Scaffold):
#: .. versionadded:: 1.1.0 #: .. versionadded:: 1.1.0
url_map_class = Map url_map_class = Map
#: the test client that is used with when `test_client` is used. #: The :meth:`test_client` method creates an instance of this test
#: client class. Defaults to :class:`~flask.testing.FlaskClient`.
#: #:
#: .. versionadded:: 0.7 #: .. versionadded:: 0.7
test_client_class: t.Optional[t.Type["FlaskClient"]] = None test_client_class: t.Optional[t.Type["FlaskClient"]] = None
@ -744,20 +740,21 @@ class Flask(Scaffold):
:param context: the context as a dictionary that is updated in place :param context: the context as a dictionary that is updated in place
to add extra variables. to add extra variables.
""" """
funcs: t.Iterable[ names: t.Iterable[t.Optional[str]] = (None,)
TemplateContextProcessorCallable
] = self.template_context_processors[None] # A template may be rendered outside a request context.
reqctx = _request_ctx_stack.top if request:
if reqctx is not None: names = chain(names, reversed(request.blueprints))
for bp in request.blueprints:
if bp in self.template_context_processors: # The values passed to render_template take precedence. Keep a
funcs = chain(funcs, self.template_context_processors[bp]) # copy to re-apply after all context functions.
orig_ctx = context.copy() orig_ctx = context.copy()
for func in funcs:
context.update(func()) for name in names:
# make sure the original values win. This makes it possible to if name in self.template_context_processors:
# easier add new variables in context processors without breaking for func in self.template_context_processors[name]:
# existing views. context.update(func())
context.update(orig_ctx) context.update(orig_ctx)
def make_shell_context(self) -> dict: def make_shell_context(self) -> dict:
@ -1277,9 +1274,10 @@ class Flask(Scaffold):
class, or ``None`` if a suitable handler is not found. class, or ``None`` if a suitable handler is not found.
""" """
exc_class, code = self._get_exc_class_and_code(type(e)) exc_class, code = self._get_exc_class_and_code(type(e))
names = (*request.blueprints, None)
for c in [code, None] if code is not None else [None]: for c in (code, None) if code is not None else (None,):
for name in chain(request.blueprints, [None]): for name in names:
handler_map = self.error_handler_spec[name][c] handler_map = self.error_handler_spec[name][c]
if not handler_map: if not handler_map:
@ -1621,7 +1619,7 @@ class Flask(Scaffold):
except ImportError: except ImportError:
raise RuntimeError( raise RuntimeError(
"Install Flask with the 'async' extra in order to use async views." "Install Flask with the 'async' extra in order to use async views."
) ) from None
# Check that Werkzeug isn't using its fallback ContextVar class. # Check that Werkzeug isn't using its fallback ContextVar class.
if ContextVar.__module__ == "werkzeug.local": if ContextVar.__module__ == "werkzeug.local":
@ -1727,7 +1725,7 @@ class Flask(Scaffold):
" response. The return type must be a string," " response. The return type must be a string,"
" dict, tuple, Response instance, or WSGI" " dict, tuple, Response instance, or WSGI"
f" callable, but it was a {type(rv).__name__}." f" callable, but it was a {type(rv).__name__}."
).with_traceback(sys.exc_info()[2]) ).with_traceback(sys.exc_info()[2]) from None
else: else:
raise TypeError( raise TypeError(
"The view function did not return a valid" "The view function did not return a valid"
@ -1799,17 +1797,19 @@ class Flask(Scaffold):
.. versionadded:: 0.7 .. versionadded:: 0.7
""" """
funcs: t.Iterable[URLDefaultCallable] = self.url_default_functions[None] names: t.Iterable[t.Optional[str]] = (None,)
# url_for may be called outside a request context, parse the
# passed endpoint instead of using request.blueprints.
if "." in endpoint: if "." in endpoint:
# This is called by url_for, which can be called outside a names = chain(
# request, can't use request.blueprints. names, reversed(_split_blueprint_path(endpoint.rpartition(".")[0]))
bps = _split_blueprint_path(endpoint.rpartition(".")[0]) )
bp_funcs = chain.from_iterable(self.url_default_functions[bp] for bp in bps)
funcs = chain(funcs, bp_funcs)
for func in funcs: for name in names:
func(endpoint, values) if name in self.url_default_functions:
for func in self.url_default_functions[name]:
func(endpoint, values)
def handle_url_build_error( def handle_url_build_error(
self, error: Exception, endpoint: str, values: dict self, error: Exception, endpoint: str, values: dict
@ -1844,24 +1844,20 @@ class Flask(Scaffold):
value is handled as if it was the return value from the view, and value is handled as if it was the return value from the view, and
further request handling is stopped. further request handling is stopped.
""" """
names = (None, *reversed(request.blueprints))
funcs: t.Iterable[URLValuePreprocessorCallable] = self.url_value_preprocessors[ for name in names:
None if name in self.url_value_preprocessors:
] for url_func in self.url_value_preprocessors[name]:
for bp in request.blueprints: url_func(request.endpoint, request.view_args)
if bp in self.url_value_preprocessors:
funcs = chain(funcs, self.url_value_preprocessors[bp])
for func in funcs:
func(request.endpoint, request.view_args)
funcs: t.Iterable[BeforeRequestCallable] = self.before_request_funcs[None] for name in names:
for bp in request.blueprints: if name in self.before_request_funcs:
if bp in self.before_request_funcs: for before_func in self.before_request_funcs[name]:
funcs = chain(funcs, self.before_request_funcs[bp]) rv = self.ensure_sync(before_func)()
for func in funcs:
rv = self.ensure_sync(func)() if rv is not None:
if rv is not None: return rv
return rv
return None return None
@ -1879,16 +1875,18 @@ class Flask(Scaffold):
instance of :attr:`response_class`. instance of :attr:`response_class`.
""" """
ctx = _request_ctx_stack.top ctx = _request_ctx_stack.top
funcs: t.Iterable[AfterRequestCallable] = ctx._after_request_functions
for bp in request.blueprints: for func in ctx._after_request_functions:
if bp in self.after_request_funcs: response = self.ensure_sync(func)(response)
funcs = chain(funcs, reversed(self.after_request_funcs[bp]))
if None in self.after_request_funcs: for name in chain(request.blueprints, (None,)):
funcs = chain(funcs, reversed(self.after_request_funcs[None])) if name in self.after_request_funcs:
for handler in funcs: for func in reversed(self.after_request_funcs[name]):
response = self.ensure_sync(handler)(response) response = self.ensure_sync(func)(response)
if not self.session_interface.is_null_session(ctx.session): if not self.session_interface.is_null_session(ctx.session):
self.session_interface.save_session(self, ctx.session, response) self.session_interface.save_session(self, ctx.session, response)
return response return response
def do_teardown_request( def do_teardown_request(
@ -1916,14 +1914,12 @@ class Flask(Scaffold):
""" """
if exc is _sentinel: if exc is _sentinel:
exc = sys.exc_info()[1] exc = sys.exc_info()[1]
funcs: t.Iterable[TeardownCallable] = reversed(
self.teardown_request_funcs[None] for name in chain(request.blueprints, (None,)):
) if name in self.teardown_request_funcs:
for bp in request.blueprints: for func in reversed(self.teardown_request_funcs[name]):
if bp in self.teardown_request_funcs: self.ensure_sync(func)(exc)
funcs = chain(funcs, reversed(self.teardown_request_funcs[bp]))
for func in funcs:
self.ensure_sync(func)(exc)
request_tearing_down.send(self, exc=exc) request_tearing_down.send(self, exc=exc)
def do_teardown_appcontext( def do_teardown_appcontext(
@ -1945,8 +1941,10 @@ class Flask(Scaffold):
""" """
if exc is _sentinel: if exc is _sentinel:
exc = sys.exc_info()[1] exc = sys.exc_info()[1]
for func in reversed(self.teardown_appcontext_funcs): for func in reversed(self.teardown_appcontext_funcs):
self.ensure_sync(func)(exc) self.ensure_sync(func)(exc)
appcontext_tearing_down.send(self, exc=exc) appcontext_tearing_down.send(self, exc=exc)
def app_context(self) -> AppContext: def app_context(self) -> AppContext:

View file

@ -69,15 +69,16 @@ def find_best_app(script_info, module):
if isinstance(app, Flask): if isinstance(app, Flask):
return app return app
except TypeError: except TypeError as e:
if not _called_with_wrong_args(app_factory): if not _called_with_wrong_args(app_factory):
raise raise
raise NoAppException( raise NoAppException(
f"Detected factory {attr_name!r} in module {module.__name__!r}," f"Detected factory {attr_name!r} in module {module.__name__!r},"
" but could not call it without arguments. Use" " but could not call it without arguments. Use"
f" \"FLASK_APP='{module.__name__}:{attr_name}(args)'\"" f" \"FLASK_APP='{module.__name__}:{attr_name}(args)'\""
" to specify arguments." " to specify arguments."
) ) from e
raise NoAppException( raise NoAppException(
"Failed to find Flask application or factory in module" "Failed to find Flask application or factory in module"
@ -161,7 +162,7 @@ def find_app_by_string(script_info, module, app_name):
except SyntaxError: except SyntaxError:
raise NoAppException( raise NoAppException(
f"Failed to parse {app_name!r} as an attribute name or function call." f"Failed to parse {app_name!r} as an attribute name or function call."
) ) from None
if isinstance(expr, ast.Name): if isinstance(expr, ast.Name):
name = expr.id name = expr.id
@ -184,7 +185,7 @@ def find_app_by_string(script_info, module, app_name):
# message with the full expression instead. # message with the full expression instead.
raise NoAppException( raise NoAppException(
f"Failed to parse arguments as literal values: {app_name!r}." f"Failed to parse arguments as literal values: {app_name!r}."
) ) from None
else: else:
raise NoAppException( raise NoAppException(
f"Failed to parse {app_name!r} as an attribute name or function call." f"Failed to parse {app_name!r} as an attribute name or function call."
@ -192,17 +193,17 @@ def find_app_by_string(script_info, module, app_name):
try: try:
attr = getattr(module, name) attr = getattr(module, name)
except AttributeError: except AttributeError as e:
raise NoAppException( raise NoAppException(
f"Failed to find attribute {name!r} in {module.__name__!r}." f"Failed to find attribute {name!r} in {module.__name__!r}."
) ) from e
# If the attribute is a function, call it with any args and kwargs # If the attribute is a function, call it with any args and kwargs
# to get the real application. # to get the real application.
if inspect.isfunction(attr): if inspect.isfunction(attr):
try: try:
app = call_factory(script_info, attr, args, kwargs) app = call_factory(script_info, attr, args, kwargs)
except TypeError: except TypeError as e:
if not _called_with_wrong_args(attr): if not _called_with_wrong_args(attr):
raise raise
@ -210,7 +211,7 @@ def find_app_by_string(script_info, module, app_name):
f"The factory {app_name!r} in module" f"The factory {app_name!r} in module"
f" {module.__name__!r} could not be called with the" f" {module.__name__!r} could not be called with the"
" specified arguments." " specified arguments."
) ) from e
else: else:
app = attr app = attr
@ -257,16 +258,15 @@ def locate_app(script_info, module_name, app_name, raise_if_not_found=True):
try: try:
__import__(module_name) __import__(module_name)
except ImportError: except ImportError as e:
# Reraise the ImportError if it occurred within the imported module. # Reraise the ImportError if it occurred within the imported module.
# Determine this by checking whether the trace has a depth > 1. # Determine this by checking whether the trace has a depth > 1.
if sys.exc_info()[2].tb_next: if sys.exc_info()[2].tb_next:
raise NoAppException( raise NoAppException(
f"While importing {module_name!r}, an ImportError was" f"While importing {module_name!r}, an ImportError was raised."
f" raised:\n\n{traceback.format_exc()}" ) from e
)
elif raise_if_not_found: elif raise_if_not_found:
raise NoAppException(f"Could not import {module_name!r}.") raise NoAppException(f"Could not import {module_name!r}.") from e
else: else:
return return
@ -725,7 +725,7 @@ class CertParamType(click.ParamType):
"Using ad-hoc certificates requires the cryptography library.", "Using ad-hoc certificates requires the cryptography library.",
ctx, ctx,
param, param,
) ) from None
return value return value

View file

@ -83,10 +83,11 @@ def attach_enctype_error_multidict(request):
def __getitem__(self, key): def __getitem__(self, key):
try: try:
return oldcls.__getitem__(self, key) return oldcls.__getitem__(self, key)
except KeyError: except KeyError as e:
if key not in request.form: if key not in request.form:
raise raise
raise DebugFilesKeyError(request, key)
raise DebugFilesKeyError(request, key) from e
newcls.__name__ = oldcls.__name__ newcls.__name__ = oldcls.__name__
newcls.__module__ = oldcls.__module__ newcls.__module__ = oldcls.__module__

View file

@ -715,7 +715,7 @@ class Scaffold:
f"'{code_or_exception}' is not a recognized HTTP error" f"'{code_or_exception}' is not a recognized HTTP error"
" code. Use a subclass of HTTPException with that code" " code. Use a subclass of HTTPException with that code"
" instead." " instead."
) ) from None
self.error_handler_spec[None][code][exc_class] = t.cast( self.error_handler_spec[None][code][exc_class] = t.cast(
"ErrorHandlerCallable[Exception]", f "ErrorHandlerCallable[Exception]", f

View file

@ -29,7 +29,7 @@ except ImportError:
raise RuntimeError( raise RuntimeError(
"Signalling support is unavailable because the blinker" "Signalling support is unavailable because the blinker"
" library is not installed." " library is not installed."
) ) from None
connect = connect_via = connected_to = temporarily_connected_to = _fail connect = connect_via = connected_to = temporarily_connected_to = _fail
disconnect = _fail disconnect = _fail

View file

@ -837,6 +837,86 @@ def test_nested_blueprint(app, client):
assert client.get("/parent/child/grandchild/no").data == b"Grandchild no" assert client.get("/parent/child/grandchild/no").data == b"Grandchild no"
def test_nested_callback_order(app, client):
parent = flask.Blueprint("parent", __name__)
child = flask.Blueprint("child", __name__)
@app.before_request
def app_before1():
flask.g.setdefault("seen", []).append("app_1")
@app.teardown_request
def app_teardown1(e=None):
assert flask.g.seen.pop() == "app_1"
@app.before_request
def app_before2():
flask.g.setdefault("seen", []).append("app_2")
@app.teardown_request
def app_teardown2(e=None):
assert flask.g.seen.pop() == "app_2"
@app.context_processor
def app_ctx():
return dict(key="app")
@parent.before_request
def parent_before1():
flask.g.setdefault("seen", []).append("parent_1")
@parent.teardown_request
def parent_teardown1(e=None):
assert flask.g.seen.pop() == "parent_1"
@parent.before_request
def parent_before2():
flask.g.setdefault("seen", []).append("parent_2")
@parent.teardown_request
def parent_teardown2(e=None):
assert flask.g.seen.pop() == "parent_2"
@parent.context_processor
def parent_ctx():
return dict(key="parent")
@child.before_request
def child_before1():
flask.g.setdefault("seen", []).append("child_1")
@child.teardown_request
def child_teardown1(e=None):
assert flask.g.seen.pop() == "child_1"
@child.before_request
def child_before2():
flask.g.setdefault("seen", []).append("child_2")
@child.teardown_request
def child_teardown2(e=None):
assert flask.g.seen.pop() == "child_2"
@child.context_processor
def child_ctx():
return dict(key="child")
@child.route("/a")
def a():
return ", ".join(flask.g.seen)
@child.route("/b")
def b():
return flask.render_template_string("{{ key }}")
parent.register_blueprint(child)
app.register_blueprint(parent)
assert (
client.get("/a").data == b"app_1, app_2, parent_1, parent_2, child_1, child_2"
)
assert client.get("/b").data == b"child"
@pytest.mark.parametrize( @pytest.mark.parametrize(
"parent_init, child_init, parent_registration, child_registration", "parent_init, child_init, parent_registration, child_registration",
[ [

View file

@ -1,6 +1,7 @@
# This file was part of Flask-CLI and was modified under the terms of # This file was part of Flask-CLI and was modified under the terms of
# its Revised BSD License. Copyright © 2015 CERN. # its Revised BSD License. Copyright © 2015 CERN.
import os import os
import platform
import ssl import ssl
import sys import sys
import types import types
@ -320,6 +321,7 @@ def test_scriptinfo(test_apps, monkeypatch):
assert app.name == "testapp" assert app.name == "testapp"
@pytest.mark.xfail(platform.python_implementation() == "PyPy", reason="flaky on pypy")
def test_lazy_load_error(monkeypatch): def test_lazy_load_error(monkeypatch):
"""When using lazy loading, the correct exception should be """When using lazy loading, the correct exception should be
re-raised. re-raised.
@ -334,7 +336,9 @@ def test_lazy_load_error(monkeypatch):
lazy = DispatchingApp(bad_load, use_eager_loading=False) lazy = DispatchingApp(bad_load, use_eager_loading=False)
with pytest.raises(BadExc): with pytest.raises(BadExc):
lazy._flush_bg_loading_exception() # reduce flakiness by waiting for the internal loading lock
with lazy._lock:
lazy._flush_bg_loading_exception()
def test_with_appcontext(runner): def test_with_appcontext(runner):