Skip to content

Commit 166a2a6

Browse files
Matthias Paulsendavidism
authored andcommitted
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.
1 parent 4346498 commit 166a2a6

File tree

3 files changed

+105
-26
lines changed

3 files changed

+105
-26
lines changed

CHANGES.rst

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,9 @@ Unreleased
2121
:issue:`4096`
2222
- The CLI loader handles ``**kwargs`` in a ``create_app`` function.
2323
:issue:`4170`
24+
- Fix the order of ``before_request`` and other callbacks that trigger
25+
before the view returns. They are called from the app down to the
26+
closest nested blueprint. :issue:`4229`
2427

2528

2629
Version 2.0.1

src/flask/app.py

Lines changed: 22 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -745,12 +745,12 @@ def update_template_context(self, context: dict) -> None:
745745
:param context: the context as a dictionary that is updated in place
746746
to add extra variables.
747747
"""
748-
funcs: t.Iterable[
749-
TemplateContextProcessorCallable
750-
] = self.template_context_processors[None]
748+
funcs: t.Iterable[TemplateContextProcessorCallable] = []
749+
if None in self.template_context_processors:
750+
funcs = chain(funcs, self.template_context_processors[None])
751751
reqctx = _request_ctx_stack.top
752752
if reqctx is not None:
753-
for bp in request.blueprints:
753+
for bp in reversed(request.blueprints):
754754
if bp in self.template_context_processors:
755755
funcs = chain(funcs, self.template_context_processors[bp])
756756
orig_ctx = context.copy()
@@ -1806,7 +1806,9 @@ def inject_url_defaults(self, endpoint: str, values: dict) -> None:
18061806
# This is called by url_for, which can be called outside a
18071807
# request, can't use request.blueprints.
18081808
bps = _split_blueprint_path(endpoint.rpartition(".")[0])
1809-
bp_funcs = chain.from_iterable(self.url_default_functions[bp] for bp in bps)
1809+
bp_funcs = chain.from_iterable(
1810+
self.url_default_functions[bp] for bp in reversed(bps)
1811+
)
18101812
funcs = chain(funcs, bp_funcs)
18111813

18121814
for func in funcs:
@@ -1846,19 +1848,17 @@ def preprocess_request(self) -> t.Optional[ResponseReturnValue]:
18461848
further request handling is stopped.
18471849
"""
18481850

1849-
funcs: t.Iterable[URLValuePreprocessorCallable] = self.url_value_preprocessors[
1850-
None
1851-
]
1852-
for bp in request.blueprints:
1853-
if bp in self.url_value_preprocessors:
1854-
funcs = chain(funcs, self.url_value_preprocessors[bp])
1851+
funcs: t.Iterable[URLValuePreprocessorCallable] = []
1852+
for name in chain([None], reversed(request.blueprints)):
1853+
if name in self.url_value_preprocessors:
1854+
funcs = chain(funcs, self.url_value_preprocessors[name])
18551855
for func in funcs:
18561856
func(request.endpoint, request.view_args)
18571857

1858-
funcs: t.Iterable[BeforeRequestCallable] = self.before_request_funcs[None]
1859-
for bp in request.blueprints:
1860-
if bp in self.before_request_funcs:
1861-
funcs = chain(funcs, self.before_request_funcs[bp])
1858+
funcs: t.Iterable[BeforeRequestCallable] = []
1859+
for name in chain([None], reversed(request.blueprints)):
1860+
if name in self.before_request_funcs:
1861+
funcs = chain(funcs, self.before_request_funcs[name])
18621862
for func in funcs:
18631863
rv = self.ensure_sync(func)()
18641864
if rv is not None:
@@ -1881,11 +1881,9 @@ def process_response(self, response: Response) -> Response:
18811881
"""
18821882
ctx = _request_ctx_stack.top
18831883
funcs: t.Iterable[AfterRequestCallable] = ctx._after_request_functions
1884-
for bp in request.blueprints:
1885-
if bp in self.after_request_funcs:
1886-
funcs = chain(funcs, reversed(self.after_request_funcs[bp]))
1887-
if None in self.after_request_funcs:
1888-
funcs = chain(funcs, reversed(self.after_request_funcs[None]))
1884+
for name in chain(request.blueprints, [None]):
1885+
if name in self.after_request_funcs:
1886+
funcs = chain(funcs, reversed(self.after_request_funcs[name]))
18891887
for handler in funcs:
18901888
response = self.ensure_sync(handler)(response)
18911889
if not self.session_interface.is_null_session(ctx.session):
@@ -1917,12 +1915,10 @@ def do_teardown_request(
19171915
"""
19181916
if exc is _sentinel:
19191917
exc = sys.exc_info()[1]
1920-
funcs: t.Iterable[TeardownCallable] = reversed(
1921-
self.teardown_request_funcs[None]
1922-
)
1923-
for bp in request.blueprints:
1924-
if bp in self.teardown_request_funcs:
1925-
funcs = chain(funcs, reversed(self.teardown_request_funcs[bp]))
1918+
funcs: t.Iterable[TeardownCallable] = []
1919+
for name in chain(request.blueprints, [None]):
1920+
if name in self.teardown_request_funcs:
1921+
funcs = chain(funcs, reversed(self.teardown_request_funcs[name]))
19261922
for func in funcs:
19271923
self.ensure_sync(func)(exc)
19281924
request_tearing_down.send(self, exc=exc)

tests/test_blueprints.py

Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -837,6 +837,86 @@ def grandchild_no():
837837
assert client.get("/parent/child/grandchild/no").data == b"Grandchild no"
838838

839839

840+
def test_nested_callback_order(app, client):
841+
parent = flask.Blueprint("parent", __name__)
842+
child = flask.Blueprint("child", __name__)
843+
844+
@app.before_request
845+
def app_before1():
846+
flask.g.setdefault("seen", []).append("app_1")
847+
848+
@app.teardown_request
849+
def app_teardown1(e=None):
850+
assert flask.g.seen.pop() == "app_1"
851+
852+
@app.before_request
853+
def app_before2():
854+
flask.g.setdefault("seen", []).append("app_2")
855+
856+
@app.teardown_request
857+
def app_teardown2(e=None):
858+
assert flask.g.seen.pop() == "app_2"
859+
860+
@app.context_processor
861+
def app_ctx():
862+
return dict(key="app")
863+
864+
@parent.before_request
865+
def parent_before1():
866+
flask.g.setdefault("seen", []).append("parent_1")
867+
868+
@parent.teardown_request
869+
def parent_teardown1(e=None):
870+
assert flask.g.seen.pop() == "parent_1"
871+
872+
@parent.before_request
873+
def parent_before2():
874+
flask.g.setdefault("seen", []).append("parent_2")
875+
876+
@parent.teardown_request
877+
def parent_teardown2(e=None):
878+
assert flask.g.seen.pop() == "parent_2"
879+
880+
@parent.context_processor
881+
def parent_ctx():
882+
return dict(key="parent")
883+
884+
@child.before_request
885+
def child_before1():
886+
flask.g.setdefault("seen", []).append("child_1")
887+
888+
@child.teardown_request
889+
def child_teardown1(e=None):
890+
assert flask.g.seen.pop() == "child_1"
891+
892+
@child.before_request
893+
def child_before2():
894+
flask.g.setdefault("seen", []).append("child_2")
895+
896+
@child.teardown_request
897+
def child_teardown2(e=None):
898+
assert flask.g.seen.pop() == "child_2"
899+
900+
@child.context_processor
901+
def child_ctx():
902+
return dict(key="child")
903+
904+
@child.route("/a")
905+
def a():
906+
return ", ".join(flask.g.seen)
907+
908+
@child.route("/b")
909+
def b():
910+
return flask.render_template_string("{{ key }}")
911+
912+
parent.register_blueprint(child)
913+
app.register_blueprint(parent)
914+
assert (
915+
client.get("/a").data == b"app_1, app_2, parent_1, parent_2, child_1, child_2"
916+
)
917+
assert client.get("/b").data == b"child"
918+
919+
840920
@pytest.mark.parametrize(
841921
"parent_init, child_init, parent_registration, child_registration",
842922
[

0 commit comments

Comments
 (0)