flask/tests/test_blueprints.py
pgjones f92e820b4b
Nested blueprints
This allows blueprints to be nested within blueprints via a new
Blueprint.register_blueprint method. This should provide a use case
that has been desired for the past ~10 years.

This works by setting the endpoint name to be the blueprint names,
from parent to child delimeted by "." and then iterating over the
blueprint names in reverse order in the app (from most specific to
most general). This means that the expectation of nesting a blueprint
within a nested blueprint is met.
2021-04-14 09:25:42 -07:00

901 lines
24 KiB
Python

import functools
import pytest
from jinja2 import TemplateNotFound
from werkzeug.http import parse_cache_control_header
import flask
def test_blueprint_specific_error_handling(app, client):
frontend = flask.Blueprint("frontend", __name__)
backend = flask.Blueprint("backend", __name__)
sideend = flask.Blueprint("sideend", __name__)
@frontend.errorhandler(403)
def frontend_forbidden(e):
return "frontend says no", 403
@frontend.route("/frontend-no")
def frontend_no():
flask.abort(403)
@backend.errorhandler(403)
def backend_forbidden(e):
return "backend says no", 403
@backend.route("/backend-no")
def backend_no():
flask.abort(403)
@sideend.route("/what-is-a-sideend")
def sideend_no():
flask.abort(403)
app.register_blueprint(frontend)
app.register_blueprint(backend)
app.register_blueprint(sideend)
@app.errorhandler(403)
def app_forbidden(e):
return "application itself says no", 403
assert client.get("/frontend-no").data == b"frontend says no"
assert client.get("/backend-no").data == b"backend says no"
assert client.get("/what-is-a-sideend").data == b"application itself says no"
def test_blueprint_specific_user_error_handling(app, client):
class MyDecoratorException(Exception):
pass
class MyFunctionException(Exception):
pass
blue = flask.Blueprint("blue", __name__)
@blue.errorhandler(MyDecoratorException)
def my_decorator_exception_handler(e):
assert isinstance(e, MyDecoratorException)
return "boom"
def my_function_exception_handler(e):
assert isinstance(e, MyFunctionException)
return "bam"
blue.register_error_handler(MyFunctionException, my_function_exception_handler)
@blue.route("/decorator")
def blue_deco_test():
raise MyDecoratorException()
@blue.route("/function")
def blue_func_test():
raise MyFunctionException()
app.register_blueprint(blue)
assert client.get("/decorator").data == b"boom"
assert client.get("/function").data == b"bam"
def test_blueprint_app_error_handling(app, client):
errors = flask.Blueprint("errors", __name__)
@errors.app_errorhandler(403)
def forbidden_handler(e):
return "you shall not pass", 403
@app.route("/forbidden")
def app_forbidden():
flask.abort(403)
forbidden_bp = flask.Blueprint("forbidden_bp", __name__)
@forbidden_bp.route("/nope")
def bp_forbidden():
flask.abort(403)
app.register_blueprint(errors)
app.register_blueprint(forbidden_bp)
assert client.get("/forbidden").data == b"you shall not pass"
assert client.get("/nope").data == b"you shall not pass"
@pytest.mark.parametrize(
("prefix", "rule", "url"),
(
("", "/", "/"),
("/", "", "/"),
("/", "/", "/"),
("/foo", "", "/foo"),
("/foo/", "", "/foo/"),
("", "/bar", "/bar"),
("/foo/", "/bar", "/foo/bar"),
("/foo/", "bar", "/foo/bar"),
("/foo", "/bar", "/foo/bar"),
("/foo/", "//bar", "/foo/bar"),
("/foo//", "/bar", "/foo/bar"),
),
)
def test_blueprint_prefix_slash(app, client, prefix, rule, url):
bp = flask.Blueprint("test", __name__, url_prefix=prefix)
@bp.route(rule)
def index():
return "", 204
app.register_blueprint(bp)
assert client.get(url).status_code == 204
def test_blueprint_url_defaults(app, client):
bp = flask.Blueprint("test", __name__)
@bp.route("/foo", defaults={"baz": 42})
def foo(bar, baz):
return f"{bar}/{baz:d}"
@bp.route("/bar")
def bar(bar):
return str(bar)
app.register_blueprint(bp, url_prefix="/1", url_defaults={"bar": 23})
app.register_blueprint(bp, url_prefix="/2", url_defaults={"bar": 19})
assert client.get("/1/foo").data == b"23/42"
assert client.get("/2/foo").data == b"19/42"
assert client.get("/1/bar").data == b"23"
assert client.get("/2/bar").data == b"19"
def test_blueprint_url_processors(app, client):
bp = flask.Blueprint("frontend", __name__, url_prefix="/<lang_code>")
@bp.url_defaults
def add_language_code(endpoint, values):
values.setdefault("lang_code", flask.g.lang_code)
@bp.url_value_preprocessor
def pull_lang_code(endpoint, values):
flask.g.lang_code = values.pop("lang_code")
@bp.route("/")
def index():
return flask.url_for(".about")
@bp.route("/about")
def about():
return flask.url_for(".index")
app.register_blueprint(bp)
assert client.get("/de/").data == b"/de/about"
assert client.get("/de/about").data == b"/de/"
def test_templates_and_static(test_apps):
from blueprintapp import app
client = app.test_client()
rv = client.get("/")
assert rv.data == b"Hello from the Frontend"
rv = client.get("/admin/")
assert rv.data == b"Hello from the Admin"
rv = client.get("/admin/index2")
assert rv.data == b"Hello from the Admin"
rv = client.get("/admin/static/test.txt")
assert rv.data.strip() == b"Admin File"
rv.close()
rv = client.get("/admin/static/css/test.css")
assert rv.data.strip() == b"/* nested file */"
rv.close()
# try/finally, in case other tests use this app for Blueprint tests.
max_age_default = app.config["SEND_FILE_MAX_AGE_DEFAULT"]
try:
expected_max_age = 3600
if app.config["SEND_FILE_MAX_AGE_DEFAULT"] == expected_max_age:
expected_max_age = 7200
app.config["SEND_FILE_MAX_AGE_DEFAULT"] = expected_max_age
rv = client.get("/admin/static/css/test.css")
cc = parse_cache_control_header(rv.headers["Cache-Control"])
assert cc.max_age == expected_max_age
rv.close()
finally:
app.config["SEND_FILE_MAX_AGE_DEFAULT"] = max_age_default
with app.test_request_context():
assert (
flask.url_for("admin.static", filename="test.txt")
== "/admin/static/test.txt"
)
with app.test_request_context():
with pytest.raises(TemplateNotFound) as e:
flask.render_template("missing.html")
assert e.value.name == "missing.html"
with flask.Flask(__name__).test_request_context():
assert flask.render_template("nested/nested.txt") == "I'm nested"
def test_default_static_max_age(app):
class MyBlueprint(flask.Blueprint):
def get_send_file_max_age(self, filename):
return 100
blueprint = MyBlueprint("blueprint", __name__, static_folder="static")
app.register_blueprint(blueprint)
# try/finally, in case other tests use this app for Blueprint tests.
max_age_default = app.config["SEND_FILE_MAX_AGE_DEFAULT"]
try:
with app.test_request_context():
unexpected_max_age = 3600
if app.config["SEND_FILE_MAX_AGE_DEFAULT"] == unexpected_max_age:
unexpected_max_age = 7200
app.config["SEND_FILE_MAX_AGE_DEFAULT"] = unexpected_max_age
rv = blueprint.send_static_file("index.html")
cc = parse_cache_control_header(rv.headers["Cache-Control"])
assert cc.max_age == 100
rv.close()
finally:
app.config["SEND_FILE_MAX_AGE_DEFAULT"] = max_age_default
def test_templates_list(test_apps):
from blueprintapp import app
templates = sorted(app.jinja_env.list_templates())
assert templates == ["admin/index.html", "frontend/index.html"]
def test_dotted_names(app, client):
frontend = flask.Blueprint("myapp.frontend", __name__)
backend = flask.Blueprint("myapp.backend", __name__)
@frontend.route("/fe")
def frontend_index():
return flask.url_for("myapp.backend.backend_index")
@frontend.route("/fe2")
def frontend_page2():
return flask.url_for(".frontend_index")
@backend.route("/be")
def backend_index():
return flask.url_for("myapp.frontend.frontend_index")
app.register_blueprint(frontend)
app.register_blueprint(backend)
assert client.get("/fe").data.strip() == b"/be"
assert client.get("/fe2").data.strip() == b"/fe"
assert client.get("/be").data.strip() == b"/fe"
def test_dotted_names_from_app(app, client):
test = flask.Blueprint("test", __name__)
@app.route("/")
def app_index():
return flask.url_for("test.index")
@test.route("/test/")
def index():
return flask.url_for("app_index")
app.register_blueprint(test)
rv = client.get("/")
assert rv.data == b"/test/"
def test_empty_url_defaults(app, client):
bp = flask.Blueprint("bp", __name__)
@bp.route("/", defaults={"page": 1})
@bp.route("/page/<int:page>")
def something(page):
return str(page)
app.register_blueprint(bp)
assert client.get("/").data == b"1"
assert client.get("/page/2").data == b"2"
def test_route_decorator_custom_endpoint(app, client):
bp = flask.Blueprint("bp", __name__)
@bp.route("/foo")
def foo():
return flask.request.endpoint
@bp.route("/bar", endpoint="bar")
def foo_bar():
return flask.request.endpoint
@bp.route("/bar/123", endpoint="123")
def foo_bar_foo():
return flask.request.endpoint
@bp.route("/bar/foo")
def bar_foo():
return flask.request.endpoint
app.register_blueprint(bp, url_prefix="/py")
@app.route("/")
def index():
return flask.request.endpoint
assert client.get("/").data == b"index"
assert client.get("/py/foo").data == b"bp.foo"
assert client.get("/py/bar").data == b"bp.bar"
assert client.get("/py/bar/123").data == b"bp.123"
assert client.get("/py/bar/foo").data == b"bp.bar_foo"
def test_route_decorator_custom_endpoint_with_dots(app, client):
bp = flask.Blueprint("bp", __name__)
@bp.route("/foo")
def foo():
return flask.request.endpoint
try:
@bp.route("/bar", endpoint="bar.bar")
def foo_bar():
return flask.request.endpoint
except AssertionError:
pass
else:
raise AssertionError("expected AssertionError not raised")
try:
@bp.route("/bar/123", endpoint="bar.123")
def foo_bar_foo():
return flask.request.endpoint
except AssertionError:
pass
else:
raise AssertionError("expected AssertionError not raised")
def foo_foo_foo():
pass
pytest.raises(
AssertionError,
lambda: bp.add_url_rule("/bar/123", endpoint="bar.123", view_func=foo_foo_foo),
)
pytest.raises(
AssertionError, bp.route("/bar/123", endpoint="bar.123"), lambda: None
)
foo_foo_foo.__name__ = "bar.123"
pytest.raises(
AssertionError, lambda: bp.add_url_rule("/bar/123", view_func=foo_foo_foo)
)
bp.add_url_rule(
"/bar/456", endpoint="foofoofoo", view_func=functools.partial(foo_foo_foo)
)
app.register_blueprint(bp, url_prefix="/py")
assert client.get("/py/foo").data == b"bp.foo"
# The rule's didn't actually made it through
rv = client.get("/py/bar")
assert rv.status_code == 404
rv = client.get("/py/bar/123")
assert rv.status_code == 404
def test_endpoint_decorator(app, client):
from werkzeug.routing import Rule
app.url_map.add(Rule("/foo", endpoint="bar"))
bp = flask.Blueprint("bp", __name__)
@bp.endpoint("bar")
def foobar():
return flask.request.endpoint
app.register_blueprint(bp, url_prefix="/bp_prefix")
assert client.get("/foo").data == b"bar"
assert client.get("/bp_prefix/bar").status_code == 404
def test_template_filter(app):
bp = flask.Blueprint("bp", __name__)
@bp.app_template_filter()
def my_reverse(s):
return s[::-1]
app.register_blueprint(bp, url_prefix="/py")
assert "my_reverse" in app.jinja_env.filters.keys()
assert app.jinja_env.filters["my_reverse"] == my_reverse
assert app.jinja_env.filters["my_reverse"]("abcd") == "dcba"
def test_add_template_filter(app):
bp = flask.Blueprint("bp", __name__)
def my_reverse(s):
return s[::-1]
bp.add_app_template_filter(my_reverse)
app.register_blueprint(bp, url_prefix="/py")
assert "my_reverse" in app.jinja_env.filters.keys()
assert app.jinja_env.filters["my_reverse"] == my_reverse
assert app.jinja_env.filters["my_reverse"]("abcd") == "dcba"
def test_template_filter_with_name(app):
bp = flask.Blueprint("bp", __name__)
@bp.app_template_filter("strrev")
def my_reverse(s):
return s[::-1]
app.register_blueprint(bp, url_prefix="/py")
assert "strrev" in app.jinja_env.filters.keys()
assert app.jinja_env.filters["strrev"] == my_reverse
assert app.jinja_env.filters["strrev"]("abcd") == "dcba"
def test_add_template_filter_with_name(app):
bp = flask.Blueprint("bp", __name__)
def my_reverse(s):
return s[::-1]
bp.add_app_template_filter(my_reverse, "strrev")
app.register_blueprint(bp, url_prefix="/py")
assert "strrev" in app.jinja_env.filters.keys()
assert app.jinja_env.filters["strrev"] == my_reverse
assert app.jinja_env.filters["strrev"]("abcd") == "dcba"
def test_template_filter_with_template(app, client):
bp = flask.Blueprint("bp", __name__)
@bp.app_template_filter()
def super_reverse(s):
return s[::-1]
app.register_blueprint(bp, url_prefix="/py")
@app.route("/")
def index():
return flask.render_template("template_filter.html", value="abcd")
rv = client.get("/")
assert rv.data == b"dcba"
def test_template_filter_after_route_with_template(app, client):
@app.route("/")
def index():
return flask.render_template("template_filter.html", value="abcd")
bp = flask.Blueprint("bp", __name__)
@bp.app_template_filter()
def super_reverse(s):
return s[::-1]
app.register_blueprint(bp, url_prefix="/py")
rv = client.get("/")
assert rv.data == b"dcba"
def test_add_template_filter_with_template(app, client):
bp = flask.Blueprint("bp", __name__)
def super_reverse(s):
return s[::-1]
bp.add_app_template_filter(super_reverse)
app.register_blueprint(bp, url_prefix="/py")
@app.route("/")
def index():
return flask.render_template("template_filter.html", value="abcd")
rv = client.get("/")
assert rv.data == b"dcba"
def test_template_filter_with_name_and_template(app, client):
bp = flask.Blueprint("bp", __name__)
@bp.app_template_filter("super_reverse")
def my_reverse(s):
return s[::-1]
app.register_blueprint(bp, url_prefix="/py")
@app.route("/")
def index():
return flask.render_template("template_filter.html", value="abcd")
rv = client.get("/")
assert rv.data == b"dcba"
def test_add_template_filter_with_name_and_template(app, client):
bp = flask.Blueprint("bp", __name__)
def my_reverse(s):
return s[::-1]
bp.add_app_template_filter(my_reverse, "super_reverse")
app.register_blueprint(bp, url_prefix="/py")
@app.route("/")
def index():
return flask.render_template("template_filter.html", value="abcd")
rv = client.get("/")
assert rv.data == b"dcba"
def test_template_test(app):
bp = flask.Blueprint("bp", __name__)
@bp.app_template_test()
def is_boolean(value):
return isinstance(value, bool)
app.register_blueprint(bp, url_prefix="/py")
assert "is_boolean" in app.jinja_env.tests.keys()
assert app.jinja_env.tests["is_boolean"] == is_boolean
assert app.jinja_env.tests["is_boolean"](False)
def test_add_template_test(app):
bp = flask.Blueprint("bp", __name__)
def is_boolean(value):
return isinstance(value, bool)
bp.add_app_template_test(is_boolean)
app.register_blueprint(bp, url_prefix="/py")
assert "is_boolean" in app.jinja_env.tests.keys()
assert app.jinja_env.tests["is_boolean"] == is_boolean
assert app.jinja_env.tests["is_boolean"](False)
def test_template_test_with_name(app):
bp = flask.Blueprint("bp", __name__)
@bp.app_template_test("boolean")
def is_boolean(value):
return isinstance(value, bool)
app.register_blueprint(bp, url_prefix="/py")
assert "boolean" in app.jinja_env.tests.keys()
assert app.jinja_env.tests["boolean"] == is_boolean
assert app.jinja_env.tests["boolean"](False)
def test_add_template_test_with_name(app):
bp = flask.Blueprint("bp", __name__)
def is_boolean(value):
return isinstance(value, bool)
bp.add_app_template_test(is_boolean, "boolean")
app.register_blueprint(bp, url_prefix="/py")
assert "boolean" in app.jinja_env.tests.keys()
assert app.jinja_env.tests["boolean"] == is_boolean
assert app.jinja_env.tests["boolean"](False)
def test_template_test_with_template(app, client):
bp = flask.Blueprint("bp", __name__)
@bp.app_template_test()
def boolean(value):
return isinstance(value, bool)
app.register_blueprint(bp, url_prefix="/py")
@app.route("/")
def index():
return flask.render_template("template_test.html", value=False)
rv = client.get("/")
assert b"Success!" in rv.data
def test_template_test_after_route_with_template(app, client):
@app.route("/")
def index():
return flask.render_template("template_test.html", value=False)
bp = flask.Blueprint("bp", __name__)
@bp.app_template_test()
def boolean(value):
return isinstance(value, bool)
app.register_blueprint(bp, url_prefix="/py")
rv = client.get("/")
assert b"Success!" in rv.data
def test_add_template_test_with_template(app, client):
bp = flask.Blueprint("bp", __name__)
def boolean(value):
return isinstance(value, bool)
bp.add_app_template_test(boolean)
app.register_blueprint(bp, url_prefix="/py")
@app.route("/")
def index():
return flask.render_template("template_test.html", value=False)
rv = client.get("/")
assert b"Success!" in rv.data
def test_template_test_with_name_and_template(app, client):
bp = flask.Blueprint("bp", __name__)
@bp.app_template_test("boolean")
def is_boolean(value):
return isinstance(value, bool)
app.register_blueprint(bp, url_prefix="/py")
@app.route("/")
def index():
return flask.render_template("template_test.html", value=False)
rv = client.get("/")
assert b"Success!" in rv.data
def test_add_template_test_with_name_and_template(app, client):
bp = flask.Blueprint("bp", __name__)
def is_boolean(value):
return isinstance(value, bool)
bp.add_app_template_test(is_boolean, "boolean")
app.register_blueprint(bp, url_prefix="/py")
@app.route("/")
def index():
return flask.render_template("template_test.html", value=False)
rv = client.get("/")
assert b"Success!" in rv.data
def test_context_processing(app, client):
answer_bp = flask.Blueprint("answer_bp", __name__)
template_string = lambda: flask.render_template_string( # noqa: E731
"{% if notanswer %}{{ notanswer }} is not the answer. {% endif %}"
"{% if answer %}{{ answer }} is the answer.{% endif %}"
)
# App global context processor
@answer_bp.app_context_processor
def not_answer_context_processor():
return {"notanswer": 43}
# Blueprint local context processor
@answer_bp.context_processor
def answer_context_processor():
return {"answer": 42}
# Setup endpoints for testing
@answer_bp.route("/bp")
def bp_page():
return template_string()
@app.route("/")
def app_page():
return template_string()
# Register the blueprint
app.register_blueprint(answer_bp)
app_page_bytes = client.get("/").data
answer_page_bytes = client.get("/bp").data
assert b"43" in app_page_bytes
assert b"42" not in app_page_bytes
assert b"42" in answer_page_bytes
assert b"43" in answer_page_bytes
def test_template_global(app):
bp = flask.Blueprint("bp", __name__)
@bp.app_template_global()
def get_answer():
return 42
# Make sure the function is not in the jinja_env already
assert "get_answer" not in app.jinja_env.globals.keys()
app.register_blueprint(bp)
# Tests
assert "get_answer" in app.jinja_env.globals.keys()
assert app.jinja_env.globals["get_answer"] is get_answer
assert app.jinja_env.globals["get_answer"]() == 42
with app.app_context():
rv = flask.render_template_string("{{ get_answer() }}")
assert rv == "42"
def test_request_processing(app, client):
bp = flask.Blueprint("bp", __name__)
evts = []
@bp.before_request
def before_bp():
evts.append("before")
@bp.after_request
def after_bp(response):
response.data += b"|after"
evts.append("after")
return response
@bp.teardown_request
def teardown_bp(exc):
evts.append("teardown")
# Setup routes for testing
@bp.route("/bp")
def bp_endpoint():
return "request"
app.register_blueprint(bp)
assert evts == []
rv = client.get("/bp")
assert rv.data == b"request|after"
assert evts == ["before", "after", "teardown"]
def test_app_request_processing(app, client):
bp = flask.Blueprint("bp", __name__)
evts = []
@bp.before_app_first_request
def before_first_request():
evts.append("first")
@bp.before_app_request
def before_app():
evts.append("before")
@bp.after_app_request
def after_app(response):
response.data += b"|after"
evts.append("after")
return response
@bp.teardown_app_request
def teardown_app(exc):
evts.append("teardown")
app.register_blueprint(bp)
# Setup routes for testing
@app.route("/")
def bp_endpoint():
return "request"
# before first request
assert evts == []
# first request
resp = client.get("/").data
assert resp == b"request|after"
assert evts == ["first", "before", "after", "teardown"]
# second request
resp = client.get("/").data
assert resp == b"request|after"
assert evts == ["first"] + ["before", "after", "teardown"] * 2
def test_app_url_processors(app, client):
bp = flask.Blueprint("bp", __name__)
# Register app-wide url defaults and preprocessor on blueprint
@bp.app_url_defaults
def add_language_code(endpoint, values):
values.setdefault("lang_code", flask.g.lang_code)
@bp.app_url_value_preprocessor
def pull_lang_code(endpoint, values):
flask.g.lang_code = values.pop("lang_code")
# Register route rules at the app level
@app.route("/<lang_code>/")
def index():
return flask.url_for("about")
@app.route("/<lang_code>/about")
def about():
return flask.url_for("index")
app.register_blueprint(bp)
assert client.get("/de/").data == b"/de/about"
assert client.get("/de/about").data == b"/de/"
def test_nested_blueprint(app, client):
parent = flask.Blueprint("parent", __name__)
child = flask.Blueprint("child", __name__)
grandchild = flask.Blueprint("grandchild", __name__)
@parent.errorhandler(403)
def forbidden(e):
return "Parent no", 403
@parent.route("/")
def parent_index():
return "Parent yes"
@parent.route("/no")
def parent_no():
flask.abort(403)
@child.route("/")
def child_index():
return "Child yes"
@child.route("/no")
def child_no():
flask.abort(403)
@grandchild.errorhandler(403)
def grandchild_forbidden(e):
return "Grandchild no", 403
@grandchild.route("/")
def grandchild_index():
return "Grandchild yes"
@grandchild.route("/no")
def grandchild_no():
flask.abort(403)
child.register_blueprint(grandchild, url_prefix="/grandchild")
parent.register_blueprint(child, url_prefix="/child")
app.register_blueprint(parent, url_prefix="/parent")
assert client.get("/parent/").data == b"Parent yes"
assert client.get("/parent/child/").data == b"Child yes"
assert client.get("/parent/child/grandchild/").data == b"Grandchild yes"
assert client.get("/parent/no").data == b"Parent no"
assert client.get("/parent/child/no").data == b"Parent no"
assert client.get("/parent/child/grandchild/no").data == b"Grandchild no"