From 0d594b8c0f13c70507aa61a7666c844c5e2aeda0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Oleksis=20Fraga=20Men=C3=A9ndez?= <44526468+oleksis@users.noreply.github.com> Date: Fri, 21 May 2021 00:33:09 -0400 Subject: [PATCH 01/18] Add update pip and setuptools section (#4061) * Add update pip and setuptools section * Simplify the command to upgrade pip Co-authored-by: Grey Li --- CONTRIBUTING.rst | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/CONTRIBUTING.rst b/CONTRIBUTING.rst index 64c8e197..3a9177a4 100644 --- a/CONTRIBUTING.rst +++ b/CONTRIBUTING.rst @@ -112,6 +112,12 @@ First time setup > py -3 -m venv env > env\Scripts\activate +- Upgrade pip and setuptools. + + .. code-block:: text + + $ python -m pip install --upgrade pip setuptools + - Install the development dependencies, then install Flask in editable mode. From bf982718cf30615f2dd17fa71fe17d565ebb1d3e Mon Sep 17 00:00:00 2001 From: Alex Hedges Date: Sat, 15 May 2021 17:38:25 -0400 Subject: [PATCH 02/18] Make add_url_rule() signature consistent This caused a mypy error when I was making another typing improvement, so I am fixing it before committing my other changes. --- src/flask/blueprints.py | 11 ++++++++++- src/flask/scaffold.py | 2 +- 2 files changed, 11 insertions(+), 2 deletions(-) diff --git a/src/flask/blueprints.py b/src/flask/blueprints.py index 39396ce7..098eca8c 100644 --- a/src/flask/blueprints.py +++ b/src/flask/blueprints.py @@ -365,6 +365,7 @@ class Blueprint(Scaffold): rule: str, endpoint: t.Optional[str] = None, view_func: t.Optional[t.Callable] = None, + provide_automatic_options: t.Optional[bool] = None, **options: t.Any, ) -> None: """Like :meth:`Flask.add_url_rule` but for a blueprint. The endpoint for @@ -376,7 +377,15 @@ class Blueprint(Scaffold): 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)) + self.record( + lambda s: s.add_url_rule( + rule, + endpoint, + view_func, + provide_automatic_options=provide_automatic_options, + **options, + ) + ) def app_template_filter(self, name: t.Optional[str] = None) -> t.Callable: """Register a custom template filter, available application wide. Like diff --git a/src/flask/scaffold.py b/src/flask/scaffold.py index f50c9b1b..20654b6b 100644 --- a/src/flask/scaffold.py +++ b/src/flask/scaffold.py @@ -443,7 +443,7 @@ class Scaffold: view_func: t.Optional[t.Callable] = None, provide_automatic_options: t.Optional[bool] = None, **options: t.Any, - ) -> t.Callable: + ) -> None: """Register a rule for routing incoming requests and building URLs. The :meth:`route` decorator is a shortcut to call this with the ``view_func`` argument. These are equivalent: From 8796b2a784bc45fcc6865cd80b2cba0601448727 Mon Sep 17 00:00:00 2001 From: Alex Hedges Date: Mon, 17 May 2021 14:02:27 -0400 Subject: [PATCH 03/18] Use TypeVar for setupmethod() TypeVar is needed to preserve function signatures. The type cast for update_wrapper is needed because wapper_func can not use the full signature that f does. --- src/flask/scaffold.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/flask/scaffold.py b/src/flask/scaffold.py index 20654b6b..f8f94af1 100644 --- a/src/flask/scaffold.py +++ b/src/flask/scaffold.py @@ -33,8 +33,10 @@ if t.TYPE_CHECKING: # a singleton sentinel value for parameter defaults _sentinel = object() +F = t.TypeVar("F", bound=t.Callable[..., t.Any]) -def setupmethod(f: t.Callable) -> t.Callable: + +def setupmethod(f: F) -> F: """Wraps a method so that it performs a check in debug mode if the first request was already handled. """ @@ -53,7 +55,7 @@ def setupmethod(f: t.Callable) -> t.Callable: ) return f(self, *args, **kwargs) - return update_wrapper(wrapper_func, f) + return t.cast(F, update_wrapper(wrapper_func, f)) class Scaffold: From 10a36cb60e10594aa5dbf3c5aba6abbd5f6fcd70 Mon Sep 17 00:00:00 2001 From: Alex Hedges Date: Mon, 17 May 2021 15:30:05 -0400 Subject: [PATCH 04/18] Improve decorator factory type signatures These changes are required to preserve the type signatures of the created decorators. --- src/flask/app.py | 12 +++++++++--- src/flask/blueprints.py | 12 +++++++++--- src/flask/scaffold.py | 2 +- 3 files changed, 19 insertions(+), 7 deletions(-) diff --git a/src/flask/app.py b/src/flask/app.py index f0f31486..cd1c42ad 100644 --- a/src/flask/app.py +++ b/src/flask/app.py @@ -1089,7 +1089,9 @@ class Flask(Scaffold): self.view_functions[endpoint] = view_func @setupmethod - def template_filter(self, name: t.Optional[str] = None) -> t.Callable: + def template_filter( + self, name: t.Optional[str] = None + ) -> t.Callable[[TemplateFilterCallable], TemplateFilterCallable]: """A decorator that is used to register custom template filter. You can specify a name for the filter, otherwise the function name will be used. Example:: @@ -1121,7 +1123,9 @@ class Flask(Scaffold): self.jinja_env.filters[name or f.__name__] = f @setupmethod - def template_test(self, name: t.Optional[str] = None) -> t.Callable: + def template_test( + self, name: t.Optional[str] = None + ) -> t.Callable[[TemplateTestCallable], TemplateTestCallable]: """A decorator that is used to register custom template test. You can specify a name for the test, otherwise the function name will be used. Example:: @@ -1162,7 +1166,9 @@ class Flask(Scaffold): self.jinja_env.tests[name or f.__name__] = f @setupmethod - def template_global(self, name: t.Optional[str] = None) -> t.Callable: + def template_global( + self, name: t.Optional[str] = None + ) -> t.Callable[[TemplateGlobalCallable], TemplateGlobalCallable]: """A decorator that is used to register a custom template global function. You can specify a name for the global function, otherwise the function name will be used. Example:: diff --git a/src/flask/blueprints.py b/src/flask/blueprints.py index 098eca8c..88883ba7 100644 --- a/src/flask/blueprints.py +++ b/src/flask/blueprints.py @@ -387,7 +387,9 @@ class Blueprint(Scaffold): ) ) - def app_template_filter(self, name: t.Optional[str] = None) -> t.Callable: + def app_template_filter( + self, name: t.Optional[str] = None + ) -> t.Callable[[TemplateFilterCallable], TemplateFilterCallable]: """Register a custom template filter, available application wide. Like :meth:`Flask.template_filter` but for a blueprint. @@ -417,7 +419,9 @@ class Blueprint(Scaffold): self.record_once(register_template) - def app_template_test(self, name: t.Optional[str] = None) -> t.Callable: + def app_template_test( + self, name: t.Optional[str] = None + ) -> t.Callable[[TemplateTestCallable], TemplateTestCallable]: """Register a custom template test, available application wide. Like :meth:`Flask.template_test` but for a blueprint. @@ -451,7 +455,9 @@ class Blueprint(Scaffold): self.record_once(register_template) - def app_template_global(self, name: t.Optional[str] = None) -> t.Callable: + def app_template_global( + self, name: t.Optional[str] = None + ) -> t.Callable[[TemplateGlobalCallable], TemplateGlobalCallable]: """Register a custom template global, available application wide. Like :meth:`Flask.template_global` but for a blueprint. diff --git a/src/flask/scaffold.py b/src/flask/scaffold.py index f8f94af1..239bc46a 100644 --- a/src/flask/scaffold.py +++ b/src/flask/scaffold.py @@ -644,7 +644,7 @@ class Scaffold: @setupmethod def errorhandler( self, code_or_exception: t.Union[t.Type[Exception], int] - ) -> t.Callable: + ) -> t.Callable[[ErrorHandlerCallable], ErrorHandlerCallable]: """Register a function to handle errors by code or exception class. A decorator that is used to register a function given an From a82cc31af847de7250cd489f6080802ce3f5236e Mon Sep 17 00:00:00 2001 From: Alex Hedges Date: Mon, 17 May 2021 17:14:47 -0400 Subject: [PATCH 05/18] Update CHANGES.rst --- CHANGES.rst | 2 ++ 1 file changed, 2 insertions(+) diff --git a/CHANGES.rst b/CHANGES.rst index 0b5a939d..57dbf3dc 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -25,6 +25,8 @@ Unreleased available in custom URL converters. :issue:`4053` - Re-add deprecated ``Config.from_json``, which was accidentally removed early. :issue:`4078` +- Improve typing for some functions using ``Callable`` in their type + signatures, focusing on decorator factories. :issue:`4060` Version 2.0.0 From 36872e7bd45162dc78e789f45e026932a4687434 Mon Sep 17 00:00:00 2001 From: Grey Li Date: Fri, 21 May 2021 16:58:58 +0800 Subject: [PATCH 06/18] Add venv and .venv to .gitignore --- .gitignore | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.gitignore b/.gitignore index 71dafa39..e50a290e 100644 --- a/.gitignore +++ b/.gitignore @@ -4,6 +4,8 @@ *.pyc *.pyo env/ +venv/ +.venv/ env* dist/ build/ From a541c2ac8b05c2b23e11bd8540088fce1abc2373 Mon Sep 17 00:00:00 2001 From: Grey Li Date: Fri, 21 May 2021 17:46:31 +0800 Subject: [PATCH 07/18] Fix view decorators docs --- docs/patterns/viewdecorators.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/patterns/viewdecorators.rst b/docs/patterns/viewdecorators.rst index b0960b2d..0b0479ef 100644 --- a/docs/patterns/viewdecorators.rst +++ b/docs/patterns/viewdecorators.rst @@ -142,7 +142,7 @@ Here is the code for that decorator:: def decorated_function(*args, **kwargs): template_name = template if template_name is None: - template_name = f"'{request.endpoint.replace('.', '/')}.html'" + template_name = f"{request.endpoint.replace('.', '/')}.html" ctx = f(*args, **kwargs) if ctx is None: ctx = {} From 99afbb277d25d3b052e00b9a8da216054d51d62a Mon Sep 17 00:00:00 2001 From: pgjones Date: Tue, 18 May 2021 13:06:21 +0100 Subject: [PATCH 08/18] 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 09/18] 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 10/18] 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 11/18] 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 12/18] 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 13/18] 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 14/18] 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 15/18] 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" From 7ab934f6bc2380a322c77d92c0b6cfd10b7de991 Mon Sep 17 00:00:00 2001 From: David Lord Date: Fri, 21 May 2021 08:42:44 -0700 Subject: [PATCH 16/18] improve typing for `stream_with_context` --- CHANGES.rst | 1 + src/flask/helpers.py | 6 ++++-- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/CHANGES.rst b/CHANGES.rst index 2698276e..9e987892 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -35,6 +35,7 @@ Unreleased same blueprint to be registered multiple times with unique names for ``url_for``. Registering the same blueprint with the same name multiple times is deprecated. :issue:`1091` +- Improve typing for ``stream_with_context``. :issue:`4052` Version 2.0.0 diff --git a/src/flask/helpers.py b/src/flask/helpers.py index 57ec9ebf..7b8b0870 100644 --- a/src/flask/helpers.py +++ b/src/flask/helpers.py @@ -64,8 +64,10 @@ def get_load_dotenv(default: bool = True) -> bool: def stream_with_context( - generator_or_function: t.Union[t.Generator, t.Callable] -) -> t.Generator: + generator_or_function: t.Union[ + t.Iterator[t.AnyStr], t.Callable[..., t.Iterator[t.AnyStr]] + ] +) -> t.Iterator[t.AnyStr]: """Request contexts disappear when the response is started on the server. This is done for efficiency reasons and to make it less likely to encounter memory leaks with badly written WSGI middlewares. The downside is that if From fe2d744b530283094dd0a2ffa4a9d86f1029b2cd Mon Sep 17 00:00:00 2001 From: David Lord Date: Fri, 21 May 2021 08:50:31 -0700 Subject: [PATCH 17/18] release version 2.0.1 --- CHANGES.rst | 2 +- src/flask/__init__.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/CHANGES.rst b/CHANGES.rst index 9e987892..d7fe44b9 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -3,7 +3,7 @@ Version 2.0.1 ------------- -Unreleased +Released 2021-05-21 - Re-add the ``filename`` parameter in ``send_from_directory``. The ``filename`` parameter has been renamed to ``path``, the old name diff --git a/src/flask/__init__.py b/src/flask/__init__.py index 008e1a81..c5da045e 100644 --- a/src/flask/__init__.py +++ b/src/flask/__init__.py @@ -43,4 +43,4 @@ from .signals import template_rendered as template_rendered from .templating import render_template as render_template from .templating import render_template_string as render_template_string -__version__ = "2.0.1.dev0" +__version__ = "2.0.1" From e22021d531499af87f65d75f1e37e6329e7385ed Mon Sep 17 00:00:00 2001 From: David Lord Date: Fri, 21 May 2021 08:55:31 -0700 Subject: [PATCH 18/18] start version 2.0.2.dev0 --- CHANGES.rst | 6 ++++++ src/flask/__init__.py | 2 +- 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/CHANGES.rst b/CHANGES.rst index d7fe44b9..006c0a1f 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -1,5 +1,11 @@ .. currentmodule:: flask +Version 2.0.2 +------------- + +Unreleased + + Version 2.0.1 ------------- diff --git a/src/flask/__init__.py b/src/flask/__init__.py index c5da045e..471ba7a6 100644 --- a/src/flask/__init__.py +++ b/src/flask/__init__.py @@ -43,4 +43,4 @@ from .signals import template_rendered as template_rendered from .templating import render_template as render_template from .templating import render_template_string as render_template_string -__version__ = "2.0.1" +__version__ = "2.0.2.dev0"