From 99afbb277d25d3b052e00b9a8da216054d51d62a Mon Sep 17 00:00:00 2001 From: pgjones Date: Tue, 18 May 2021 13:06:21 +0100 Subject: [PATCH 1/8] Fix blueprint nested url_prefix This ensures that the url_prefix is correctly applied, no matter if set during the registration override or when constructing the blueprint. --- src/flask/blueprints.py | 4 ++- tests/test_blueprints.py | 66 +++++++++++++++------------------------- 2 files changed, 28 insertions(+), 42 deletions(-) diff --git a/src/flask/blueprints.py b/src/flask/blueprints.py index 88883ba7..85870a90 100644 --- a/src/flask/blueprints.py +++ b/src/flask/blueprints.py @@ -354,7 +354,9 @@ class Blueprint(Scaffold): bp_options["url_prefix"] = ( state.url_prefix.rstrip("/") + "/" + bp_url_prefix.lstrip("/") ) - else: + elif bp_url_prefix is not None: + bp_options["url_prefix"] = bp_url_prefix + elif state.url_prefix is not None: bp_options["url_prefix"] = state.url_prefix bp_options["name_prefix"] = options.get("name_prefix", "") + self.name + "." diff --git a/tests/test_blueprints.py b/tests/test_blueprints.py index 0bae5333..0f9e9db9 100644 --- a/tests/test_blueprints.py +++ b/tests/test_blueprints.py @@ -837,48 +837,32 @@ def test_nested_blueprint(app, client): assert client.get("/parent/child/grandchild/no").data == b"Grandchild no" -def test_nested_blueprint_url_prefix(app, client): - parent = flask.Blueprint("parent", __name__, url_prefix="/parent") - child = flask.Blueprint("child", __name__, url_prefix="/child") - grandchild = flask.Blueprint("grandchild", __name__, url_prefix="/grandchild") - apple = flask.Blueprint("apple", __name__, url_prefix="/apple") - - @parent.route("/") - def parent_index(): - return "Parent" +@pytest.mark.parametrize( + "parent_init, child_init, parent_registration, child_registration", + [ + ("/parent", "/child", None, None), + ("/parent", None, None, "/child"), + (None, None, "/parent", "/child"), + ("/other", "/something", "/parent", "/child"), + ], +) +def test_nesting_url_prefixes( + parent_init, + child_init, + parent_registration, + child_registration, + app, + client, +) -> None: + parent = flask.Blueprint("parent", __name__, url_prefix=parent_init) + child = flask.Blueprint("child", __name__, url_prefix=child_init) @child.route("/") - def child_index(): - return "Child" + def index(): + return "index" - @grandchild.route("/") - def grandchild_index(): - return "Grandchild" + parent.register_blueprint(child, url_prefix=child_registration) + app.register_blueprint(parent, url_prefix=parent_registration) - @apple.route("/") - def apple_index(): - return "Apple" - - child.register_blueprint(grandchild) - child.register_blueprint(apple, url_prefix="/orange") # test overwrite - parent.register_blueprint(child) - app.register_blueprint(parent) - - assert client.get("/parent/").data == b"Parent" - assert client.get("/parent/child/").data == b"Child" - assert client.get("/parent/child/grandchild/").data == b"Grandchild" - assert client.get("/parent/child/orange/").data == b"Apple" - - -def test_nested_blueprint_url_prefix_only_parent_prefix(app, client): - parent = flask.Blueprint("parent", __name__) - child = flask.Blueprint("child", __name__) - - @child.route("/child-endpoint") - def child_index(): - return "Child" - - parent.register_blueprint(child) - app.register_blueprint(parent, url_prefix="/parent") - - assert client.get("/parent/child-endpoint").data == b"Child" + response = client.get("/parent/child/") + assert response.status_code == 200 From 141fde1d8ec8663b4be98777750d2f58c6fe44ad Mon Sep 17 00:00:00 2001 From: pgjones Date: Tue, 18 May 2021 13:33:45 +0100 Subject: [PATCH 2/8] Bugfix blueprint naming Following discussions for Flask we've decided to name blueprints based on how they are registered. This allows for two different blueprints to have the same self-name as long as they are registered in different nested positions. This helps users choose better blueprint names. --- src/flask/app.py | 29 +++++++++++++++-------------- src/flask/blueprints.py | 38 +++++++++++++++++++------------------- src/flask/wrappers.py | 16 ++++++++++++++++ 3 files changed, 50 insertions(+), 33 deletions(-) diff --git a/src/flask/app.py b/src/flask/app.py index cd1c42ad..3bf92ceb 100644 --- a/src/flask/app.py +++ b/src/flask/app.py @@ -747,7 +747,7 @@ class Flask(Scaffold): ] = self.template_context_processors[None] reqctx = _request_ctx_stack.top if reqctx is not None: - for bp in self._request_blueprints(): + for bp in request.blueprints: if bp in self.template_context_processors: funcs = chain(funcs, self.template_context_processors[bp]) orig_ctx = context.copy() @@ -1267,7 +1267,7 @@ class Flask(Scaffold): exc_class, code = self._get_exc_class_and_code(type(e)) for c in [code, None]: - for name in chain(self._request_blueprints(), [None]): + for name in chain(request.blueprints, [None]): handler_map = self.error_handler_spec[name][c] if not handler_map: @@ -1788,9 +1788,16 @@ class Flask(Scaffold): .. versionadded:: 0.7 """ funcs: t.Iterable[URLDefaultCallable] = self.url_default_functions[None] + if "." in endpoint: - bp = endpoint.rsplit(".", 1)[0] - funcs = chain(funcs, self.url_default_functions[bp]) + bps: t.List[str] = [endpoint.rsplit(".", 1)[0]] + + while "." in bps[-1]: + bps.append(bps[-1].rpartition(".")[0]) + + for bp in bps: + funcs = chain(funcs, self.url_default_functions[bp]) + for func in funcs: func(endpoint, values) @@ -1831,14 +1838,14 @@ class Flask(Scaffold): funcs: t.Iterable[URLValuePreprocessorCallable] = self.url_value_preprocessors[ None ] - for bp in self._request_blueprints(): + for bp in request.blueprints: if bp in self.url_value_preprocessors: funcs = chain(funcs, self.url_value_preprocessors[bp]) for func in funcs: func(request.endpoint, request.view_args) funcs: t.Iterable[BeforeRequestCallable] = self.before_request_funcs[None] - for bp in self._request_blueprints(): + for bp in request.blueprints: if bp in self.before_request_funcs: funcs = chain(funcs, self.before_request_funcs[bp]) for func in funcs: @@ -1863,7 +1870,7 @@ class Flask(Scaffold): """ ctx = _request_ctx_stack.top funcs: t.Iterable[AfterRequestCallable] = ctx._after_request_functions - for bp in self._request_blueprints(): + 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: @@ -1902,7 +1909,7 @@ class Flask(Scaffold): funcs: t.Iterable[TeardownCallable] = reversed( self.teardown_request_funcs[None] ) - for bp in self._request_blueprints(): + for bp in request.blueprints: if bp in self.teardown_request_funcs: funcs = chain(funcs, reversed(self.teardown_request_funcs[bp])) for func in funcs: @@ -2074,9 +2081,3 @@ class Flask(Scaffold): wrapped to apply middleware. """ return self.wsgi_app(environ, start_response) - - def _request_blueprints(self) -> t.Iterable[str]: - if _request_ctx_stack.top.request.blueprint is None: - return [] - else: - return reversed(_request_ctx_stack.top.request.blueprint.split(".")) diff --git a/src/flask/blueprints.py b/src/flask/blueprints.py index 85870a90..8fe7d9e9 100644 --- a/src/flask/blueprints.py +++ b/src/flask/blueprints.py @@ -98,7 +98,7 @@ class BlueprintSetupState: defaults = dict(defaults, **options.pop("defaults")) self.app.add_url_rule( rule, - f"{self.name_prefix}{self.blueprint.name}.{endpoint}", + f"{self.name_prefix}.{self.blueprint.name}.{endpoint}".lstrip("."), view_func, defaults=defaults, **options, @@ -266,23 +266,24 @@ class Blueprint(Scaffold): with. :param options: Keyword arguments forwarded from :meth:`~Flask.register_blueprint`. - :param first_registration: Whether this is the first time this - blueprint has been registered on the application. """ - first_registration = False + first_registration = True - if self.name in app.blueprints: - assert app.blueprints[self.name] is self, ( - "A name collision occurred between blueprints" - f" {self!r} and {app.blueprints[self.name]!r}." - f" Both share the same name {self.name!r}." - f" Blueprints that are created on the fly need unique" - f" names." + for blueprint in app.blueprints.values(): + if blueprint is self: + first_registration = False + + name_prefix = options.get("name_prefix", "") + name = f"{name_prefix}.{self.name}".lstrip(".") + + if name in app.blueprints and app.blueprints[name] is not self: + raise ValueError( + f"Blueprint name '{self.name}' " + f"is already registered by {app.blueprints[self.name]}. " + "Blueprints must have unique names." ) - else: - app.blueprints[self.name] = self - first_registration = True + app.blueprints[name] = self self._got_registered_once = True state = self.make_setup_state(app, options, first_registration) @@ -298,12 +299,11 @@ class Blueprint(Scaffold): def extend(bp_dict, parent_dict): for key, values in bp_dict.items(): - key = self.name if key is None else f"{self.name}.{key}" - + key = name if key is None else f"{name}.{key}" parent_dict[key].extend(values) for key, value in self.error_handler_spec.items(): - key = self.name if key is None else f"{self.name}.{key}" + key = name if key is None else f"{name}.{key}" value = defaultdict( dict, { @@ -337,7 +337,7 @@ class Blueprint(Scaffold): if cli_resolved_group is None: app.cli.commands.update(self.cli.commands) elif cli_resolved_group is _sentinel: - self.cli.name = self.name + self.cli.name = name app.cli.add_command(self.cli) else: self.cli.name = cli_resolved_group @@ -359,7 +359,7 @@ class Blueprint(Scaffold): elif state.url_prefix is not None: bp_options["url_prefix"] = state.url_prefix - bp_options["name_prefix"] = options.get("name_prefix", "") + self.name + "." + bp_options["name_prefix"] = name blueprint.register(app, bp_options) def add_url_rule( diff --git a/src/flask/wrappers.py b/src/flask/wrappers.py index bfa9d7ce..547e68d6 100644 --- a/src/flask/wrappers.py +++ b/src/flask/wrappers.py @@ -1,6 +1,7 @@ import typing as t from werkzeug.exceptions import BadRequest +from werkzeug.utils import cached_property from werkzeug.wrappers import Request as RequestBase from werkzeug.wrappers import Response as ResponseBase @@ -77,6 +78,21 @@ class Request(RequestBase): else: return None + @cached_property + def blueprints(self) -> t.List[str]: + """The names of the current blueprint upwards through parent + blueprints. + """ + if self.blueprint is None: + return [] + + bps: t.List[str] = [self.blueprint] + + while "." in bps[-1]: + bps.append(bps[-1].rpartition(".")[0]) + + return bps + def _load_form_data(self) -> None: RequestBase._load_form_data(self) From c2920e2bd98cdb3dcccc2868c25b695d4780c620 Mon Sep 17 00:00:00 2001 From: pgjones Date: Tue, 18 May 2021 13:37:11 +0100 Subject: [PATCH 3/8] Bugfix allow blueprints to be registered with a different name This allows the same blueprint to be registered multiple times at the same level, but with differing url_prefixes and names. --- src/flask/blueprints.py | 3 ++- tests/test_blueprints.py | 13 +++++++++++++ 2 files changed, 15 insertions(+), 1 deletion(-) diff --git a/src/flask/blueprints.py b/src/flask/blueprints.py index 8fe7d9e9..61b0ac69 100644 --- a/src/flask/blueprints.py +++ b/src/flask/blueprints.py @@ -274,7 +274,8 @@ class Blueprint(Scaffold): first_registration = False name_prefix = options.get("name_prefix", "") - name = f"{name_prefix}.{self.name}".lstrip(".") + self_name = options.get("name", self.name) + name = f"{name_prefix}.{self_name}".lstrip(".") if name in app.blueprints and app.blueprints[name] is not self: raise ValueError( diff --git a/tests/test_blueprints.py b/tests/test_blueprints.py index 0f9e9db9..2a93e8fa 100644 --- a/tests/test_blueprints.py +++ b/tests/test_blueprints.py @@ -866,3 +866,16 @@ def test_nesting_url_prefixes( response = client.get("/parent/child/") assert response.status_code == 200 + + +def test_unique_blueprint_names(app, client) -> None: + bp = flask.Blueprint("bp", __name__) + bp2 = flask.Blueprint("bp", __name__) + + app.register_blueprint(bp) + app.register_blueprint(bp) # same name, same object, no error + + with pytest.raises(ValueError): + app.register_blueprint(bp2) # same name, different object + + app.register_blueprint(bp2, name="alt") # different name From 67b0b7e30d2304f628e966896748032022ac0257 Mon Sep 17 00:00:00 2001 From: David Lord Date: Thu, 20 May 2021 10:32:28 -0700 Subject: [PATCH 4/8] cache blueprint path calculation --- src/flask/app.py | 13 +++++---- src/flask/helpers.py | 11 ++++++++ src/flask/wrappers.py | 62 +++++++++++++++++++++++++++---------------- 3 files changed, 56 insertions(+), 30 deletions(-) diff --git a/src/flask/app.py b/src/flask/app.py index 3bf92ceb..315633b5 100644 --- a/src/flask/app.py +++ b/src/flask/app.py @@ -36,6 +36,7 @@ from .globals import _request_ctx_stack from .globals import g from .globals import request from .globals import session +from .helpers import _split_blueprint_path from .helpers import get_debug_flag from .helpers import get_env from .helpers import get_flashed_messages @@ -1790,13 +1791,11 @@ class Flask(Scaffold): funcs: t.Iterable[URLDefaultCallable] = self.url_default_functions[None] if "." in endpoint: - bps: t.List[str] = [endpoint.rsplit(".", 1)[0]] - - while "." in bps[-1]: - bps.append(bps[-1].rpartition(".")[0]) - - for bp in bps: - funcs = chain(funcs, self.url_default_functions[bp]) + # 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) + funcs = chain(funcs, bp_funcs) for func in funcs: func(endpoint, values) diff --git a/src/flask/helpers.py b/src/flask/helpers.py index 585b4dea..57ec9ebf 100644 --- a/src/flask/helpers.py +++ b/src/flask/helpers.py @@ -6,6 +6,7 @@ import typing as t import warnings from datetime import datetime from datetime import timedelta +from functools import lru_cache from functools import update_wrapper from threading import RLock @@ -821,3 +822,13 @@ def is_ip(value: str) -> bool: return True return False + + +@lru_cache(maxsize=None) +def _split_blueprint_path(name: str) -> t.List[str]: + out: t.List[str] = [name] + + if "." in name: + out.extend(_split_blueprint_path(name.rpartition(".")[0])) + + return out diff --git a/src/flask/wrappers.py b/src/flask/wrappers.py index 547e68d6..47dbe5c8 100644 --- a/src/flask/wrappers.py +++ b/src/flask/wrappers.py @@ -1,12 +1,12 @@ import typing as t from werkzeug.exceptions import BadRequest -from werkzeug.utils import cached_property from werkzeug.wrappers import Request as RequestBase from werkzeug.wrappers import Response as ResponseBase from . import json from .globals import current_app +from .helpers import _split_blueprint_path if t.TYPE_CHECKING: import typing_extensions as te @@ -60,38 +60,54 @@ class Request(RequestBase): @property def endpoint(self) -> t.Optional[str]: - """The endpoint that matched the request. This in combination with - :attr:`view_args` can be used to reconstruct the same or a - modified URL. If an exception happened when matching, this will - be ``None``. + """The endpoint that matched the request URL. + + This will be ``None`` if matching failed or has not been + performed yet. + + This in combination with :attr:`view_args` can be used to + reconstruct the same URL or a modified URL. """ if self.url_rule is not None: return self.url_rule.endpoint - else: - return None + + return None @property def blueprint(self) -> t.Optional[str]: - """The name of the current blueprint""" - if self.url_rule and "." in self.url_rule.endpoint: - return self.url_rule.endpoint.rsplit(".", 1)[0] - else: - return None + """The registered name of the current blueprint. - @cached_property - def blueprints(self) -> t.List[str]: - """The names of the current blueprint upwards through parent - blueprints. + This will be ``None`` if the endpoint is not part of a + blueprint, or if URL matching failed or has not been performed + yet. + + This does not necessarily match the name the blueprint was + created with. It may have been nested, or registered with a + different name. """ - if self.blueprint is None: + endpoint = self.endpoint + + if endpoint is not None and "." in endpoint: + return endpoint.rpartition(".")[0] + + return None + + @property + def blueprints(self) -> t.List[str]: + """The registered names of the current blueprint upwards through + parent blueprints. + + This will be an empty list if there is no current blueprint, or + if URL matching failed. + + .. versionadded:: 2.0.1 + """ + name = self.blueprint + + if name is None: return [] - bps: t.List[str] = [self.blueprint] - - while "." in bps[-1]: - bps.append(bps[-1].rpartition(".")[0]) - - return bps + return _split_blueprint_path(name) def _load_form_data(self) -> None: RequestBase._load_form_data(self) From 63b306743fccdd52079034e82cbbeee182a4361d Mon Sep 17 00:00:00 2001 From: David Lord Date: Thu, 20 May 2021 11:05:36 -0700 Subject: [PATCH 5/8] changelog for blueprint registered name --- CHANGES.rst | 7 +++++++ src/flask/app.py | 6 ++++++ src/flask/blueprints.py | 17 +++++++++++++++++ 3 files changed, 30 insertions(+) diff --git a/CHANGES.rst b/CHANGES.rst index 57dbf3dc..f50a7ad9 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -27,6 +27,13 @@ Unreleased removed early. :issue:`4078` - Improve typing for some functions using ``Callable`` in their type signatures, focusing on decorator factories. :issue:`4060` +- Nested blueprints are registered with their dotted name. This allows + different blueprints with the same name to be nested at different + locations. :issue:`4069` +- ``register_blueprint`` takes a ``name`` option to change the + (pre-dotted) name the blueprint is registered with. This allows the + same blueprint to be registered multiple times with unique names for + ``url_for``. :issue:`1091` Version 2.0.0 diff --git a/src/flask/app.py b/src/flask/app.py index 315633b5..3abce3ce 100644 --- a/src/flask/app.py +++ b/src/flask/app.py @@ -1019,6 +1019,12 @@ class Flask(Scaffold): :class:`~flask.blueprints.BlueprintSetupState`. They can be accessed in :meth:`~flask.Blueprint.record` callbacks. + .. versionchanged:: 2.0.1 + The ``name`` option can be used to change the (pre-dotted) + name the blueprint is registered with. This allows the same + blueprint to be registered multiple times with unique names + for ``url_for``. + .. versionadded:: 0.7 """ blueprint.register(self, options) diff --git a/src/flask/blueprints.py b/src/flask/blueprints.py index 61b0ac69..6b4e2714 100644 --- a/src/flask/blueprints.py +++ b/src/flask/blueprints.py @@ -252,6 +252,12 @@ class Blueprint(Scaffold): arguments passed to this method will override the defaults set on the blueprint. + .. versionchanged:: 2.0.1 + The ``name`` option can be used to change the (pre-dotted) + name the blueprint is registered with. This allows the same + blueprint to be registered multiple times with unique names + for ``url_for``. + .. versionadded:: 2.0 """ self._blueprints.append((blueprint, options)) @@ -266,6 +272,17 @@ class Blueprint(Scaffold): with. :param options: Keyword arguments forwarded from :meth:`~Flask.register_blueprint`. + + .. versionchanged:: 2.0.1 + Nested blueprints are registered with their dotted name. + This allows different blueprints with the same name to be + nested at different locations. + + .. versionchanged:: 2.0.1 + The ``name`` option can be used to change the (pre-dotted) + name the blueprint is registered with. This allows the same + blueprint to be registered multiple times with unique names + for ``url_for``. """ first_registration = True From 9409be6e34e153bfdb0aac2c9eb7f60110109172 Mon Sep 17 00:00:00 2001 From: David Lord Date: Thu, 20 May 2021 13:08:28 -0700 Subject: [PATCH 6/8] warn when registering same blueprint with same name --- CHANGES.rst | 3 ++- src/flask/blueprints.py | 36 ++++++++++++++++++++++++------------ tests/test_blueprints.py | 12 ++++++++---- 3 files changed, 34 insertions(+), 17 deletions(-) diff --git a/CHANGES.rst b/CHANGES.rst index f50a7ad9..2698276e 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -33,7 +33,8 @@ Unreleased - ``register_blueprint`` takes a ``name`` option to change the (pre-dotted) name the blueprint is registered with. This allows the same blueprint to be registered multiple times with unique names for - ``url_for``. :issue:`1091` + ``url_for``. Registering the same blueprint with the same name + multiple times is deprecated. :issue:`1091` Version 2.0.0 diff --git a/src/flask/blueprints.py b/src/flask/blueprints.py index 6b4e2714..fc999c75 100644 --- a/src/flask/blueprints.py +++ b/src/flask/blueprints.py @@ -283,23 +283,35 @@ class Blueprint(Scaffold): name the blueprint is registered with. This allows the same blueprint to be registered multiple times with unique names for ``url_for``. + + .. versionchanged:: 2.0.1 + Registering the same blueprint with the same name multiple + times is deprecated and will become an error in Flask 2.1. """ - first_registration = True - - for blueprint in app.blueprints.values(): - if blueprint is self: - first_registration = False - + first_registration = not any(bp is self for bp in app.blueprints.values()) name_prefix = options.get("name_prefix", "") self_name = options.get("name", self.name) name = f"{name_prefix}.{self_name}".lstrip(".") - if name in app.blueprints and app.blueprints[name] is not self: - raise ValueError( - f"Blueprint name '{self.name}' " - f"is already registered by {app.blueprints[self.name]}. " - "Blueprints must have unique names." - ) + if name in app.blueprints: + existing_at = f" '{name}'" if self_name != name else "" + + if app.blueprints[name] is not self: + raise ValueError( + f"The name '{self_name}' is already registered for" + f" a different blueprint{existing_at}. Use 'name='" + " to provide a unique name." + ) + else: + import warnings + + warnings.warn( + f"The name '{self_name}' is already registered for" + f" this blueprint{existing_at}. Use 'name=' to" + " provide a unique name. This will become an error" + " in Flask 2.1.", + stacklevel=4, + ) app.blueprints[name] = self self._got_registered_once = True diff --git a/tests/test_blueprints.py b/tests/test_blueprints.py index 2a93e8fa..2977f69c 100644 --- a/tests/test_blueprints.py +++ b/tests/test_blueprints.py @@ -140,7 +140,7 @@ def test_blueprint_url_defaults(app, client): 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}) + app.register_blueprint(bp, name="test2", 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" @@ -873,9 +873,13 @@ def test_unique_blueprint_names(app, client) -> None: bp2 = flask.Blueprint("bp", __name__) app.register_blueprint(bp) - app.register_blueprint(bp) # same name, same object, no error + + with pytest.warns(UserWarning): + app.register_blueprint(bp) # same bp, same name, warning + + app.register_blueprint(bp, name="again") # same bp, different name, ok with pytest.raises(ValueError): - app.register_blueprint(bp2) # same name, different object + app.register_blueprint(bp2) # different bp, same name, error - app.register_blueprint(bp2, name="alt") # different name + app.register_blueprint(bp2, name="alt") # different bp, different name, ok From 714b0a467acab54091d8aeb73403d808abfbcf5b Mon Sep 17 00:00:00 2001 From: pgjones Date: Fri, 21 May 2021 15:01:32 +0100 Subject: [PATCH 7/8] Fix blueprint self registration By raising a ValueError if attempted. I don't see a use case that makes this worth supporting. --- src/flask/blueprints.py | 2 ++ tests/test_blueprints.py | 6 ++++++ 2 files changed, 8 insertions(+) diff --git a/src/flask/blueprints.py b/src/flask/blueprints.py index fc999c75..2da4d12b 100644 --- a/src/flask/blueprints.py +++ b/src/flask/blueprints.py @@ -260,6 +260,8 @@ class Blueprint(Scaffold): .. versionadded:: 2.0 """ + if blueprint is self: + raise ValueError("Cannot register a blueprint on itself") self._blueprints.append((blueprint, options)) def register(self, app: "Flask", options: dict) -> None: diff --git a/tests/test_blueprints.py b/tests/test_blueprints.py index 2977f69c..73c94ade 100644 --- a/tests/test_blueprints.py +++ b/tests/test_blueprints.py @@ -883,3 +883,9 @@ def test_unique_blueprint_names(app, client) -> None: app.register_blueprint(bp2) # different bp, same name, error app.register_blueprint(bp2, name="alt") # different bp, different name, ok + + +def test_self_registration(app, client) -> None: + bp = flask.Blueprint("bp", __name__) + with pytest.raises(ValueError): + bp.register_blueprint(bp) From 3257b7574e7f010686a617197e0fb4596986f7f7 Mon Sep 17 00:00:00 2001 From: pgjones Date: Fri, 21 May 2021 15:02:05 +0100 Subject: [PATCH 8/8] Fix blueprint renaming This ensures that if a blueprint is renamed at the time of registration that name is used when constructing endpoints, as expected. --- src/flask/blueprints.py | 4 +++- tests/test_blueprints.py | 22 ++++++++++++++++++++++ 2 files changed, 25 insertions(+), 1 deletion(-) diff --git a/src/flask/blueprints.py b/src/flask/blueprints.py index 2da4d12b..f3913b30 100644 --- a/src/flask/blueprints.py +++ b/src/flask/blueprints.py @@ -67,6 +67,7 @@ class BlueprintSetupState: #: blueprint. self.url_prefix = url_prefix + self.name = self.options.get("name", blueprint.name) self.name_prefix = self.options.get("name_prefix", "") #: A dictionary with URL defaults that is added to each and every @@ -96,9 +97,10 @@ class BlueprintSetupState: defaults = self.url_defaults if "defaults" in options: defaults = dict(defaults, **options.pop("defaults")) + self.app.add_url_rule( rule, - f"{self.name_prefix}.{self.blueprint.name}.{endpoint}".lstrip("."), + f"{self.name_prefix}.{self.name}.{endpoint}".lstrip("."), view_func, defaults=defaults, **options, diff --git a/tests/test_blueprints.py b/tests/test_blueprints.py index 73c94ade..088ad779 100644 --- a/tests/test_blueprints.py +++ b/tests/test_blueprints.py @@ -889,3 +889,25 @@ def test_self_registration(app, client) -> None: bp = flask.Blueprint("bp", __name__) with pytest.raises(ValueError): bp.register_blueprint(bp) + + +def test_blueprint_renaming(app, client) -> None: + bp = flask.Blueprint("bp", __name__) + bp2 = flask.Blueprint("bp2", __name__) + + @bp.get("/") + def index(): + return flask.request.endpoint + + @bp2.get("/") + def index2(): + return flask.request.endpoint + + bp.register_blueprint(bp2, url_prefix="/a", name="sub") + app.register_blueprint(bp, url_prefix="/a") + app.register_blueprint(bp, url_prefix="/b", name="alt") + + assert client.get("/a/").data == b"bp.index" + assert client.get("/b/").data == b"alt.index" + assert client.get("/a/a/").data == b"bp.sub.index2" + assert client.get("/b/a/").data == b"alt.sub.index2"