diff --git a/CHANGES.rst b/CHANGES.rst index d546dcab63..c2a42443d6 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -15,6 +15,9 @@ Unreleased - Fix some types that weren't available in Python 3.6.0. :issue:`4040` - Improve typing for ``send_file``, ``send_from_directory``, and ``get_send_file_max_age``. :issue:`4044`, :pr:`4026` +- Show an error when a blueprint name contains a dot. The ``.`` has + special meaning, it is used to separate (nested) blueprint names and + the endpoint name. :issue:`4041` Version 2.0.0 diff --git a/src/flask/blueprints.py b/src/flask/blueprints.py index 5fb84d86a6..823995f675 100644 --- a/src/flask/blueprints.py +++ b/src/flask/blueprints.py @@ -188,6 +188,10 @@ def __init__( template_folder=template_folder, root_path=root_path, ) + + if "." in name: + raise ValueError("'name' may not contain a dot '.' character.") + self.name = name self.url_prefix = url_prefix self.subdomain = subdomain @@ -360,12 +364,12 @@ def add_url_rule( """Like :meth:`Flask.add_url_rule` but for a blueprint. The endpoint for the :func:`url_for` function is prefixed with the name of the blueprint. """ - if endpoint: - assert "." not in endpoint, "Blueprint endpoints should not contain dots" - if view_func and hasattr(view_func, "__name__"): - assert ( - "." not in view_func.__name__ - ), "Blueprint view function name should not contain dots" + if endpoint and "." in endpoint: + raise ValueError("'endpoint' may not contain a dot '.' character.") + + if view_func and hasattr(view_func, "__name__") and "." in view_func.__name__: + raise ValueError("'view_func' name may not contain a dot '.' character.") + self.record(lambda s: s.add_url_rule(rule, endpoint, view_func, **options)) def app_template_filter(self, name: t.Optional[str] = None) -> t.Callable: diff --git a/tests/test_basic.py b/tests/test_basic.py index d6ec3fe43f..7bdeba1edf 100644 --- a/tests/test_basic.py +++ b/tests/test_basic.py @@ -1631,7 +1631,7 @@ def something_else(): def test_inject_blueprint_url_defaults(app): - bp = flask.Blueprint("foo.bar.baz", __name__, template_folder="template") + bp = flask.Blueprint("foo", __name__, template_folder="template") @bp.url_defaults def bp_defaults(endpoint, values): @@ -1644,12 +1644,12 @@ def view(page): app.register_blueprint(bp) values = dict() - app.inject_url_defaults("foo.bar.baz.view", values) + app.inject_url_defaults("foo.view", values) expected = dict(page="login") assert values == expected with app.test_request_context("/somepage"): - url = flask.url_for("foo.bar.baz.view") + url = flask.url_for("foo.view") expected = "/login" assert url == expected diff --git a/tests/test_blueprints.py b/tests/test_blueprints.py index b986ca022d..cfcb43b39a 100644 --- a/tests/test_blueprints.py +++ b/tests/test_blueprints.py @@ -1,5 +1,3 @@ -import functools - import pytest from jinja2 import TemplateNotFound from werkzeug.http import parse_cache_control_header @@ -253,28 +251,9 @@ def test_templates_list(test_apps): 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_name_not_allowed(app, client): + with pytest.raises(ValueError): + flask.Blueprint("app.ui", __name__) def test_dotted_names_from_app(app, client): @@ -343,62 +322,19 @@ def index(): 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" + with pytest.raises(ValueError): + bp.route("/", endpoint="a.b")(lambda: "") - pytest.raises( - AssertionError, lambda: bp.add_url_rule("/bar/123", view_func=foo_foo_foo) - ) + with pytest.raises(ValueError): + bp.add_url_rule("/", endpoint="a.b") - bp.add_url_rule( - "/bar/456", endpoint="foofoofoo", view_func=functools.partial(foo_foo_foo) - ) + def view(): + return "" - app.register_blueprint(bp, url_prefix="/py") + view.__name__ = "a.b" - 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 + with pytest.raises(ValueError): + bp.add_url_rule("/", view_func=view) def test_endpoint_decorator(app, client):