Skip to content

Commit

Permalink
Fix callback order for nested blueprints
Browse files Browse the repository at this point in the history
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.
  • Loading branch information
Matthias Paulsen authored and davidism committed Oct 4, 2021
1 parent 4346498 commit 166a2a6
Show file tree
Hide file tree
Showing 3 changed files with 105 additions and 26 deletions.
3 changes: 3 additions & 0 deletions CHANGES.rst
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
48 changes: 22 additions & 26 deletions src/flask/app.py
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand Down Expand Up @@ -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:
Expand Down Expand Up @@ -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:
Expand All @@ -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):
Expand Down Expand Up @@ -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)
Expand Down
80 changes: 80 additions & 0 deletions tests/test_blueprints.py
Original file line number Diff line number Diff line change
Expand Up @@ -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",
[
Expand Down

0 comments on commit 166a2a6

Please sign in to comment.