From 67b0b7e30d2304f628e966896748032022ac0257 Mon Sep 17 00:00:00 2001 From: David Lord Date: Thu, 20 May 2021 10:32:28 -0700 Subject: [PATCH] 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)