8000 Merge pull request #4230 from matipau/blueprint-fix · pallets/flask@6d637f0 · GitHub
[go: up one dir, main page]

Skip to content

Commit 6d637f0

Browse files
authored
Merge pull request #4230 from matipau/blueprint-fix
Fix callback order for nested blueprints
2 parents 4346498 + 3f6cdbd commit 6d637f0

File tree

3 files changed

+141
-61
lines changed

3 files changed

+141
-61
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: 58 additions & 61 deletions
Original file line numberDiff line numberDiff line change
@@ -58,17 +58,12 @@
5858
from .signals import request_tearing_down
5959
from .templating import DispatchingJinjaLoader
6060
from .templating import Environment
61-
from .typing import AfterRequestCallable
6261
from .typing import BeforeFirstRequestCallable
63-
from .typing import BeforeRequestCallable
6462
from .typing import ResponseReturnValue
6563
from .typing import TeardownCallable
66-
from .typing import TemplateContextProcessorCallable
6764
from .typing import TemplateFilterCallable
6865
from .typing import TemplateGlobalCallable
6966
from .typing import TemplateTestCallable
70-
from .typing import URLDefaultCallable
71-
from .typing import URLValuePreprocessorCallable
7267
from .wrappers import Request
7368
from .wrappers import Response
7469

@@ -745,20 +740,21 @@ def update_template_context(self, context: dict) -> None:
745740
:param context: the context as a dictionary that is updated in place
746741
to add extra variables.
747742
"""
748-
funcs: t.Iterable[
749-
TemplateContextProcessorCallable
750-
] = self.template_context_processors[None]
751-
reqctx = _request_ctx_stack.top
752-
if reqctx is not None:
753-
for bp in request.blueprints:
754-
if bp in self.template_context_processors:
755-
funcs = chain(funcs, self.template_context_processors[bp])
743+
names: t.Iterable[t.Optional[str]] = (None,)
744+
745+
# A template may be rendered outside a request context.
746+
if request:
747+
names = chain(names, reversed(request.blueprints))
748+
749+
# The values passed to render_template take precedence. Keep a
750+
# copy to re-apply after all context functions.
756751
orig_ctx = context.copy()
757-
for func in funcs:
758-
context.update(func())
759-
# make sure the original values win. This makes it possible to
760-
# easier add new variables in context processors without breaking
761-
# existing views.
752+
753+
for name in names:
754+
if name in self.template_context_processors:
755+
for func in self.template_context_processors[name]:
756+
context.update(func())
757+
762758
context.update(orig_ctx)
763759

764760
def make_shell_context(self) -> dict:
@@ -1278,9 +1274,10 @@ def _find_error_handler(
12781274
class, or ``None`` if a suitable handler is not found.
12791275
"""
12801276
exc_class, code = self._get_exc_class_and_code(type(e))
1277+
names = (*request.blueprints, None)
12811278

1282-
for c in [code, None] if code is not None else [None]:
1283-
for name in chain(request.blueprints, [None]):
1279+
for c in (code, None) if code is not None else (None,):
1280+
for name in names:
12841281
handler_map = self.error_handler_spec[name][c]
12851282

12861283
if not handler_map:
@@ -1800,17 +1797,19 @@ def inject_url_defaults(self, endpoint: str, values: dict) -> None:
18001797
18011798
.. versionadded:: 0.7
18021799
"""
1803-
funcs: t.Iterable[URLDefaultCallable] = self.url_default_functions[None]
1800+
names: t.Iterable[t.Optional[str]] = (None,)
18041801

1802+
# url_for may be called outside a request context, parse the
1803+
# passed endpoint instead of using request.blueprints.
18051804
if "." in endpoint:
1806-
# This is called by url_for, which can be called outside a
1807-
# request, can't use request.blueprints.
1808-
bps = _split_blueprint_path(endpoint.rpartition(".")[0])
1809-
bp_funcs = chain.from_iterable(self.url_default_functions[bp] for bp in bps)
1810-
funcs = chain(funcs, bp_funcs)
1805+
names = chain(
1806+
names, reversed(_split_blueprint_path(endpoint.rpartition(".")[0]))
1807+
)
18111808

1812-
for func in funcs:
1813-
func(endpoint, values F438 )
1809+
for name in names:
1810+
if name in self.url_default_functions:
1811+
for func in self.url_default_functions[name]:
1812+
func(endpoint, values)
18141813

18151814
def handle_url_build_error(
18161815
self, error: Exception, endpoint: str, values: dict
@@ -1845,24 +1844,20 @@ def preprocess_request(self) -> t.Optional[ResponseReturnValue]:
18451844
value is handled as if it was the return value from the view, and
18461845
further request handling is stopped.
18471846
"""
1847+
names = (None, *reversed(request.blueprints))
18481848

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])
1855-
for func in funcs:
1856-
func(request.endpoint, request.view_args)
1857-
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])
1862-
for func in funcs:
1863-
rv = self.ensure_sync(func)()
1864-
if rv is not None:
1865-
return rv
1849+
for name in names:
1850+
if name in self.url_value_preprocessors:
1851+
for url_func in self.url_value_preprocessors[name]:
1852+
url_func(request.endpoint, request.view_args)
1853+
1854+
for name in names:
1855+
if name in self.before_request_funcs:
1856+
for before_func in self.before_request_funcs[name]:
1857+
rv = self.ensure_sync(before_func)()
1858+
1859+
if rv is not None:
1860+
return rv
18661861

18671862
return None
18681863

@@ -1880,16 +1875,18 @@ def process_response(self, response: Response) -> Response:
18801875
instance of :attr:`response_class`.
18811876
"""
18821877
ctx = _request_ctx_stack.top
1883-
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]))
1889-
for handler in funcs:
1890-
response = self.ensure_sync(handler)(response)
1878+
1879+
for func in ctx._after_request_functions:
1880+
response = self.ensure_sync(func)(response)
1881+
1882+
for name in chain(request.blueprints, (None,)):
1883+
if name in self.after_request_funcs:
1884+
for func in reversed(self.after_request_funcs[name]):
1885+
response = self.ensure_sync(func)(response)
1886+
18911887
if not self.session_interface.is_null_session(ctx.session):
18921888
self.session_interface.save_session(self, ctx.session, response)
1889+
18931890
return response
18941891

18951892
def do_teardown_request(
@@ -1917,14 +1914,12 @@ def do_teardown_request(
19171914
"""
19181915
if exc is _sentinel:
19191916
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]))
1926-
for func in funcs:
1927-
self.ensure_sync(func)(exc)
1917+
1918+
for name in chain(request.blueprints, (None,)):
1919+
if name in self.teardown_request_funcs:
1920+
for func in reversed(self.teardown_request_funcs[name]):
1921+
self.ensure_sync(func)(exc)
1922+
19281923
request_tearing_down.send(self, exc=exc)
19291924

19301925
def do_teardown_appcontext(
@@ -1946,8 +1941,10 @@ def do_teardown_appcontext(
19461941
"""
19471942
if exc is _sentinel:
19481943
exc = sys.exc_info()[1]
1944+
19491945
for func in reversed(self.teardown_appcontext_funcs):
19501946
self.ensure_sync(func)(exc)
1947+
19511948
appcontext_tearing_down.send(self, exc=exc)
19521949

19531950
def app_context(self) -> AppContext:

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)
0