diff --git a/CHANGES.rst b/CHANGES.rst index 461284c0..e241e1a1 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -29,6 +29,9 @@ Unreleased :issue:`4096` - The CLI loader handles ``**kwargs`` in a ``create_app`` function. :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` Version 2.0.1 diff --git a/artwork/LICENSE.rst b/artwork/LICENSE.rst index 605e41cb..99c58a21 100644 --- a/artwork/LICENSE.rst +++ b/artwork/LICENSE.rst @@ -10,7 +10,7 @@ following conditions are met: 1. Redistributions of source code must retain the above copyright 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 this software without specific prior written permission. diff --git a/docs/appcontext.rst b/docs/appcontext.rst index 68176494..b214f254 100644 --- a/docs/appcontext.rst +++ b/docs/appcontext.rst @@ -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 :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 is pushed when a request context is pushed. diff --git a/docs/patterns/wtforms.rst b/docs/patterns/wtforms.rst index e5fd500d..3d626f50 100644 --- a/docs/patterns/wtforms.rst +++ b/docs/patterns/wtforms.rst @@ -19,7 +19,7 @@ forms. fun. You can get it from `PyPI `_. -.. _Flask-WTF: https://flask-wtf.readthedocs.io/en/stable/ +.. _Flask-WTF: https://flask-wtf.readthedocs.io/ The Forms --------- diff --git a/docs/reqcontext.rst b/docs/reqcontext.rst index 31a83cff..b67745ed 100644 --- a/docs/reqcontext.rst +++ b/docs/reqcontext.rst @@ -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 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 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 -context, which also pushes an :doc:`/appcontext`. When the request ends -it pops the request context then the application context. +context, which also pushes an :doc:`app context `. When the +request ends it pops the request context then the application context. The context is unique to each thread (or other worker type). :data:`request` cannot be passed to another thread, the other thread diff --git a/docs/shell.rst b/docs/shell.rst index 47efba37..7e42e285 100644 --- a/docs/shell.rst +++ b/docs/shell.rst @@ -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 context. -Generally it's recommended that you read the :doc:`reqcontext` -chapter of the documentation first. +Generally it's recommended that you read :doc:`reqcontext` first. Command Line Interface ---------------------- diff --git a/src/flask/app.py b/src/flask/app.py index 9097c464..23b99e2c 100644 --- a/src/flask/app.py +++ b/src/flask/app.py @@ -58,17 +58,12 @@ from .signals import request_started from .signals import request_tearing_down from .templating import DispatchingJinjaLoader from .templating import Environment -from .typing import AfterRequestCallable from .typing import BeforeFirstRequestCallable -from .typing import BeforeRequestCallable from .typing import ResponseReturnValue from .typing import TeardownCallable -from .typing import TemplateContextProcessorCallable from .typing import TemplateFilterCallable from .typing import TemplateGlobalCallable from .typing import TemplateTestCallable -from .typing import URLDefaultCallable -from .typing import URLValuePreprocessorCallable from .wrappers import Request from .wrappers import Response @@ -366,7 +361,8 @@ class Flask(Scaffold): #: .. versionadded:: 1.1.0 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 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 to add extra variables. """ - funcs: t.Iterable[ - TemplateContextProcessorCallable - ] = self.template_context_processors[None] - reqctx = _request_ctx_stack.top - if reqctx is not None: - for bp in request.blueprints: - if bp in self.template_context_processors: - funcs = chain(funcs, self.template_context_processors[bp]) + names: t.Iterable[t.Optional[str]] = (None,) + + # A template may be rendered outside a request context. + if request: + names = chain(names, reversed(request.blueprints)) + + # The values passed to render_template take precedence. Keep a + # copy to re-apply after all context functions. orig_ctx = context.copy() - for func in funcs: - context.update(func()) - # make sure the original values win. This makes it possible to - # easier add new variables in context processors without breaking - # existing views. + + for name in names: + if name in self.template_context_processors: + for func in self.template_context_processors[name]: + context.update(func()) + context.update(orig_ctx) def make_shell_context(self) -> dict: @@ -1277,9 +1274,10 @@ class Flask(Scaffold): class, or ``None`` if a suitable handler is not found. """ 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 name in chain(request.blueprints, [None]): + for c in (code, None) if code is not None else (None,): + for name in names: handler_map = self.error_handler_spec[name][c] if not handler_map: @@ -1799,17 +1797,19 @@ class Flask(Scaffold): .. 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: - # This is called by url_for, which can be called outside a - # request, can't use request.blueprints. - 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) + names = chain( + names, reversed(_split_blueprint_path(endpoint.rpartition(".")[0])) + ) - for func in funcs: - func(endpoint, values) + for name in names: + if name in self.url_default_functions: + for func in self.url_default_functions[name]: + func(endpoint, values) def handle_url_build_error( 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 further request handling is stopped. """ + names = (None, *reversed(request.blueprints)) - funcs: t.Iterable[URLValuePreprocessorCallable] = self.url_value_preprocessors[ - None - ] - for bp in request.blueprints: - 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) + for name in names: + if name in self.url_value_preprocessors: + for url_func in self.url_value_preprocessors[name]: + url_func(request.endpoint, request.view_args) - funcs: t.Iterable[BeforeRequestCallable] = self.before_request_funcs[None] - for bp in request.blueprints: - if bp in self.before_request_funcs: - funcs = chain(funcs, self.before_request_funcs[bp]) - for func in funcs: - rv = self.ensure_sync(func)() - if rv is not None: - return rv + for name in names: + if name in self.before_request_funcs: + for before_func in self.before_request_funcs[name]: + rv = self.ensure_sync(before_func)() + + if rv is not None: + return rv return None @@ -1879,16 +1875,18 @@ class Flask(Scaffold): instance of :attr:`response_class`. """ ctx = _request_ctx_stack.top - funcs: t.Iterable[AfterRequestCallable] = ctx._after_request_functions - for bp in request.blueprints: - if bp in self.after_request_funcs: - funcs = chain(funcs, reversed(self.after_request_funcs[bp])) - if None in self.after_request_funcs: - funcs = chain(funcs, reversed(self.after_request_funcs[None])) - for handler in funcs: - response = self.ensure_sync(handler)(response) + + for func in ctx._after_request_functions: + response = self.ensure_sync(func)(response) + + for name in chain(request.blueprints, (None,)): + if name in self.after_request_funcs: + for func in reversed(self.after_request_funcs[name]): + response = self.ensure_sync(func)(response) + if not self.session_interface.is_null_session(ctx.session): self.session_interface.save_session(self, ctx.session, response) + return response def do_teardown_request( @@ -1916,14 +1914,12 @@ class Flask(Scaffold): """ if exc is _sentinel: exc = sys.exc_info()[1] - funcs: t.Iterable[TeardownCallable] = reversed( - self.teardown_request_funcs[None] - ) - for bp in request.blueprints: - if bp in self.teardown_request_funcs: - funcs = chain(funcs, reversed(self.teardown_request_funcs[bp])) - for func in funcs: - self.ensure_sync(func)(exc) + + for name in chain(request.blueprints, (None,)): + if name in self.teardown_request_funcs: + for func in reversed(self.teardown_request_funcs[name]): + self.ensure_sync(func)(exc) + request_tearing_down.send(self, exc=exc) def do_teardown_appcontext( @@ -1945,8 +1941,10 @@ class Flask(Scaffold): """ if exc is _sentinel: exc = sys.exc_info()[1] + for func in reversed(self.teardown_appcontext_funcs): self.ensure_sync(func)(exc) + appcontext_tearing_down.send(self, exc=exc) def app_context(self) -> AppContext: diff --git a/tests/test_blueprints.py b/tests/test_blueprints.py index a124c612..e02cd4be 100644 --- a/tests/test_blueprints.py +++ b/tests/test_blueprints.py @@ -837,6 +837,86 @@ def test_nested_blueprint(app, client): 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( "parent_init, child_init, parent_registration, child_registration", [