From 166a2a6207027bff07fdfc5590ce04f9b37e9e8f Mon Sep 17 00:00:00 2001 From: Matthias Paulsen Date: Tue, 10 Aug 2021 00:23:57 +0200 Subject: [PATCH] Fix callback order for nested blueprints Handlers registered via url_value_preprocessor, before_request, context_processor, and url_defaults are called in downward order: First on the app and last on the current blueprint. Handlers registered via after_request and teardown_request are called in upward order: First on the current blueprint and last on the app. --- CHANGES.rst | 3 ++ src/flask/app.py | 48 +++++++++++------------- tests/test_blueprints.py | 80 ++++++++++++++++++++++++++++++++++++++++ 3 files changed, 105 insertions(+), 26 deletions(-) diff --git a/CHANGES.rst b/CHANGES.rst index eeba61ab00..1b4551abf7 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -21,6 +21,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/src/flask/app.py b/src/flask/app.py index 5179305a24..a9bdba653c 100644 --- a/src/flask/app.py +++ b/src/flask/app.py @@ -745,12 +745,12 @@ def update_template_context(self, context: dict) -> None: :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] + funcs: t.Iterable[TemplateContextProcessorCallable] = [] + if None in self.template_context_processors: + funcs = chain(funcs, self.template_context_processors[None]) reqctx = _request_ctx_stack.top if reqctx is not None: - for bp in request.blueprints: + for bp in reversed(request.blueprints): if bp in self.template_context_processors: funcs = chain(funcs, self.template_context_processors[bp]) orig_ctx = context.copy() @@ -1806,7 +1806,9 @@ def inject_url_defaults(self, endpoint: str, values: dict) -> None: # 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) + bp_funcs = chain.from_iterable( + self.url_default_functions[bp] for bp in reversed(bps) + ) funcs = chain(funcs, bp_funcs) for func in funcs: @@ -1846,19 +1848,17 @@ def preprocess_request(self) -> t.Optional[ResponseReturnValue]: further request handling is stopped. """ - 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]) + funcs: t.Iterable[URLValuePreprocessorCallable] = [] + for name in chain([None], reversed(request.blueprints)): + if name in self.url_value_preprocessors: + funcs = chain(funcs, self.url_value_preprocessors[name]) for func in funcs: 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]) + funcs: t.Iterable[BeforeRequestCallable] = [] + for name in chain([None], reversed(request.blueprints)): + if name in self.before_request_funcs: + funcs = chain(funcs, self.before_request_funcs[name]) for func in funcs: rv = self.ensure_sync(func)() if rv is not None: @@ -1881,11 +1881,9 @@ def process_response(self, response: Response) -> Response: """ 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 name in chain(request.blueprints, [None]): + if name in self.after_request_funcs: + funcs = chain(funcs, reversed(self.after_request_funcs[name])) for handler in funcs: response = self.ensure_sync(handler)(response) if not self.session_interface.is_null_session(ctx.session): @@ -1917,12 +1915,10 @@ def do_teardown_request( """ 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])) + funcs: t.Iterable[TeardownCallable] = [] + for name in chain(request.blueprints, [None]): + if name in self.teardown_request_funcs: + funcs = chain(funcs, reversed(self.teardown_request_funcs[name])) for func in funcs: self.ensure_sync(func)(exc) request_tearing_down.send(self, exc=exc) diff --git a/tests/test_blueprints.py b/tests/test_blueprints.py index a124c612e7..e02cd4be3d 100644 --- a/tests/test_blueprints.py +++ b/tests/test_blueprints.py @@ -837,6 +837,86 @@ def 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( "parent_init, child_init, parent_registration, child_registration", [