Merge branch '2.0.x'

This commit is contained in:
David Lord 2021-05-21 08:56:18 -07:00
commit 7161776824
No known key found for this signature in database
GPG key ID: 7A1C87E3F5BC42A8
10 changed files with 262 additions and 98 deletions

2
.gitignore vendored
View file

@ -4,6 +4,8 @@
*.pyc *.pyc
*.pyo *.pyo
env/ env/
venv/
.venv/
env* env*
dist/ dist/
build/ build/

View file

@ -8,11 +8,17 @@ Unreleased
- Update Click dependency to >= 8.0. - Update Click dependency to >= 8.0.
Version 2.0.1 Version 2.0.2
------------- -------------
Unreleased Unreleased
Version 2.0.1
-------------
Released 2021-05-21
- Re-add the ``filename`` parameter in ``send_from_directory``. The - Re-add the ``filename`` parameter in ``send_from_directory``. The
``filename`` parameter has been renamed to ``path``, the old name ``filename`` parameter has been renamed to ``path``, the old name
is deprecated. :pr:`4019` is deprecated. :pr:`4019`
@ -33,6 +39,17 @@ Unreleased
available in custom URL converters. :issue:`4053` available in custom URL converters. :issue:`4053`
- Re-add deprecated ``Config.from_json``, which was accidentally - Re-add deprecated ``Config.from_json``, which was accidentally
removed early. :issue:`4078` 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``. 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 Version 2.0.0

View file

@ -112,6 +112,12 @@ First time setup
> py -3 -m venv env > py -3 -m venv env
> env\Scripts\activate > 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 - Install the development dependencies, then install Flask in editable
mode. mode.

View file

@ -142,7 +142,7 @@ Here is the code for that decorator::
def decorated_function(*args, **kwargs): def decorated_function(*args, **kwargs):
template_name = template template_name = template
if template_name is None: if template_name is None:
template_name = f"'{request.endpoint.replace('.', '/')}.html'" template_name = f"{request.endpoint.replace('.', '/')}.html"
ctx = f(*args, **kwargs) ctx = f(*args, **kwargs)
if ctx is None: if ctx is None:
ctx = {} ctx = {}

View file

@ -36,6 +36,7 @@ from .globals import _request_ctx_stack
from .globals import g from .globals import g
from .globals import request from .globals import request
from .globals import session from .globals import session
from .helpers import _split_blueprint_path
from .helpers import get_debug_flag from .helpers import get_debug_flag
from .helpers import get_env from .helpers import get_env
from .helpers import get_flashed_messages from .helpers import get_flashed_messages
@ -747,7 +748,7 @@ class Flask(Scaffold):
] = self.template_context_processors[None] ] = self.template_context_processors[None]
reqctx = _request_ctx_stack.top reqctx = _request_ctx_stack.top
if reqctx is not None: if reqctx is not None:
for bp in self._request_blueprints(): for bp in request.blueprints:
if bp in self.template_context_processors: if bp in self.template_context_processors:
funcs = chain(funcs, self.template_context_processors[bp]) funcs = chain(funcs, self.template_context_processors[bp])
orig_ctx = context.copy() orig_ctx = context.copy()
@ -1018,6 +1019,12 @@ class Flask(Scaffold):
:class:`~flask.blueprints.BlueprintSetupState`. They can be :class:`~flask.blueprints.BlueprintSetupState`. They can be
accessed in :meth:`~flask.Blueprint.record` callbacks. 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 .. versionadded:: 0.7
""" """
blueprint.register(self, options) blueprint.register(self, options)
@ -1089,7 +1096,9 @@ class Flask(Scaffold):
self.view_functions[endpoint] = view_func self.view_functions[endpoint] = view_func
@setupmethod @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. """A decorator that is used to register custom template filter.
You can specify a name for the filter, otherwise the function You can specify a name for the filter, otherwise the function
name will be used. Example:: name will be used. Example::
@ -1121,7 +1130,9 @@ class Flask(Scaffold):
self.jinja_env.filters[name or f.__name__] = f self.jinja_env.filters[name or f.__name__] = f
@setupmethod @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. """A decorator that is used to register custom template test.
You can specify a name for the test, otherwise the function You can specify a name for the test, otherwise the function
name will be used. Example:: name will be used. Example::
@ -1162,7 +1173,9 @@ class Flask(Scaffold):
self.jinja_env.tests[name or f.__name__] = f self.jinja_env.tests[name or f.__name__] = f
@setupmethod @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. """A decorator that is used to register a custom template global function.
You can specify a name for the global function, otherwise the function You can specify a name for the global function, otherwise the function
name will be used. Example:: name will be used. Example::
@ -1261,7 +1274,7 @@ class Flask(Scaffold):
exc_class, code = self._get_exc_class_and_code(type(e)) exc_class, code = self._get_exc_class_and_code(type(e))
for c in [code, None]: 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] handler_map = self.error_handler_spec[name][c]
if not handler_map: if not handler_map:
@ -1782,9 +1795,14 @@ class Flask(Scaffold):
.. versionadded:: 0.7 .. versionadded:: 0.7
""" """
funcs: t.Iterable[URLDefaultCallable] = self.url_default_functions[None] funcs: t.Iterable[URLDefaultCallable] = self.url_default_functions[None]
if "." in endpoint: if "." in endpoint:
bp = endpoint.rsplit(".", 1)[0] # This is called by url_for, which can be called outside a
funcs = chain(funcs, self.url_default_functions[bp]) # 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: for func in funcs:
func(endpoint, values) func(endpoint, values)
@ -1825,14 +1843,14 @@ class Flask(Scaffold):
funcs: t.Iterable[URLValuePreprocessorCallable] = self.url_value_preprocessors[ funcs: t.Iterable[URLValuePreprocessorCallable] = self.url_value_preprocessors[
None None
] ]
for bp in self._request_blueprints(): for bp in request.blueprints:
if bp in self.url_value_preprocessors: if bp in self.url_value_preprocessors:
funcs = chain(funcs, self.url_value_preprocessors[bp]) funcs = chain(funcs, self.url_value_preprocessors[bp])
for func in funcs: for func in funcs:
func(request.endpoint, request.view_args) func(request.endpoint, request.view_args)
funcs: t.Iterable[BeforeRequestCallable] = self.before_request_funcs[None] 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: if bp in self.before_request_funcs:
funcs = chain(funcs, self.before_request_funcs[bp]) funcs = chain(funcs, self.before_request_funcs[bp])
for func in funcs: for func in funcs:
@ -1857,7 +1875,7 @@ class Flask(Scaffold):
""" """
ctx = _request_ctx_stack.top ctx = _request_ctx_stack.top
funcs: t.Iterable[AfterRequestCallable] = ctx._after_request_functions 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: if bp in self.after_request_funcs:
funcs = chain(funcs, reversed(self.after_request_funcs[bp])) funcs = chain(funcs, reversed(self.after_request_funcs[bp]))
if None in self.after_request_funcs: if None in self.after_request_funcs:
@ -1896,7 +1914,7 @@ class Flask(Scaffold):
funcs: t.Iterable[TeardownCallable] = reversed( funcs: t.Iterable[TeardownCallable] = reversed(
self.teardown_request_funcs[None] self.teardown_request_funcs[None]
) )
for bp in self._request_blueprints(): for bp in request.blueprints:
if bp in self.teardown_request_funcs: if bp in self.teardown_request_funcs:
funcs = chain(funcs, reversed(self.teardown_request_funcs[bp])) funcs = chain(funcs, reversed(self.teardown_request_funcs[bp]))
for func in funcs: for func in funcs:
@ -2068,9 +2086,3 @@ class Flask(Scaffold):
wrapped to apply middleware. wrapped to apply middleware.
""" """
return self.wsgi_app(environ, start_response) 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("."))

View file

@ -67,6 +67,7 @@ class BlueprintSetupState:
#: blueprint. #: blueprint.
self.url_prefix = url_prefix self.url_prefix = url_prefix
self.name = self.options.get("name", blueprint.name)
self.name_prefix = self.options.get("name_prefix", "") self.name_prefix = self.options.get("name_prefix", "")
#: A dictionary with URL defaults that is added to each and every #: A dictionary with URL defaults that is added to each and every
@ -96,9 +97,10 @@ class BlueprintSetupState:
defaults = self.url_defaults defaults = self.url_defaults
if "defaults" in options: if "defaults" in options:
defaults = dict(defaults, **options.pop("defaults")) defaults = dict(defaults, **options.pop("defaults"))
self.app.add_url_rule( self.app.add_url_rule(
rule, rule,
f"{self.name_prefix}{self.blueprint.name}.{endpoint}", f"{self.name_prefix}.{self.name}.{endpoint}".lstrip("."),
view_func, view_func,
defaults=defaults, defaults=defaults,
**options, **options,
@ -252,8 +254,16 @@ class Blueprint(Scaffold):
arguments passed to this method will override the defaults set arguments passed to this method will override the defaults set
on the blueprint. 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 .. versionadded:: 2.0
""" """
if blueprint is self:
raise ValueError("Cannot register a blueprint on itself")
self._blueprints.append((blueprint, options)) self._blueprints.append((blueprint, options))
def register(self, app: "Flask", options: dict) -> None: def register(self, app: "Flask", options: dict) -> None:
@ -266,23 +276,48 @@ class Blueprint(Scaffold):
with. with.
:param options: Keyword arguments forwarded from :param options: Keyword arguments forwarded from
:meth:`~Flask.register_blueprint`. :meth:`~Flask.register_blueprint`.
:param first_registration: Whether this is the first time this
blueprint has been registered on the application. .. 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``.
.. 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 = 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 self.name in app.blueprints: if name in app.blueprints:
assert app.blueprints[self.name] is self, ( existing_at = f" '{name}'" if self_name != name else ""
"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."
)
else:
app.blueprints[self.name] = self
first_registration = True
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 self._got_registered_once = True
state = self.make_setup_state(app, options, first_registration) state = self.make_setup_state(app, options, first_registration)
@ -298,12 +333,11 @@ class Blueprint(Scaffold):
def extend(bp_dict, parent_dict): def extend(bp_dict, parent_dict):
for key, values in bp_dict.items(): 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) parent_dict[key].extend(values)
for key, value in self.error_handler_spec.items(): 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( value = defaultdict(
dict, dict,
{ {
@ -337,7 +371,7 @@ class Blueprint(Scaffold):
if cli_resolved_group is None: if cli_resolved_group is None:
app.cli.commands.update(self.cli.commands) app.cli.commands.update(self.cli.commands)
elif cli_resolved_group is _sentinel: elif cli_resolved_group is _sentinel:
self.cli.name = self.name self.cli.name = name
app.cli.add_command(self.cli) app.cli.add_command(self.cli)
else: else:
self.cli.name = cli_resolved_group self.cli.name = cli_resolved_group
@ -354,10 +388,12 @@ class Blueprint(Scaffold):
bp_options["url_prefix"] = ( bp_options["url_prefix"] = (
state.url_prefix.rstrip("/") + "/" + bp_url_prefix.lstrip("/") 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["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) blueprint.register(app, bp_options)
def add_url_rule( def add_url_rule(
@ -365,6 +401,7 @@ class Blueprint(Scaffold):
rule: str, rule: str,
endpoint: t.Optional[str] = None, endpoint: t.Optional[str] = None,
view_func: t.Optional[t.Callable] = None, view_func: t.Optional[t.Callable] = None,
provide_automatic_options: t.Optional[bool] = None,
**options: t.Any, **options: t.Any,
) -> None: ) -> None:
"""Like :meth:`Flask.add_url_rule` but for a blueprint. The endpoint for """Like :meth:`Flask.add_url_rule` but for a blueprint. The endpoint for
@ -376,9 +413,19 @@ class Blueprint(Scaffold):
if view_func and hasattr(view_func, "__name__") and "." in view_func.__name__: if view_func and hasattr(view_func, "__name__") and "." in view_func.__name__:
raise ValueError("'view_func' name may not contain a dot '.' character.") 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: def app_template_filter(
self, name: t.Optional[str] = None
) -> t.Callable[[TemplateFilterCallable], TemplateFilterCallable]:
"""Register a custom template filter, available application wide. Like """Register a custom template filter, available application wide. Like
:meth:`Flask.template_filter` but for a blueprint. :meth:`Flask.template_filter` but for a blueprint.
@ -408,7 +455,9 @@ class Blueprint(Scaffold):
self.record_once(register_template) 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 """Register a custom template test, available application wide. Like
:meth:`Flask.template_test` but for a blueprint. :meth:`Flask.template_test` but for a blueprint.
@ -442,7 +491,9 @@ class Blueprint(Scaffold):
self.record_once(register_template) 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 """Register a custom template global, available application wide. Like
:meth:`Flask.template_global` but for a blueprint. :meth:`Flask.template_global` but for a blueprint.

View file

@ -6,6 +6,7 @@ import typing as t
import warnings import warnings
from datetime import datetime from datetime import datetime
from datetime import timedelta from datetime import timedelta
from functools import lru_cache
from functools import update_wrapper from functools import update_wrapper
from threading import RLock from threading import RLock
@ -63,8 +64,10 @@ def get_load_dotenv(default: bool = True) -> bool:
def stream_with_context( def stream_with_context(
generator_or_function: t.Union[t.Generator, t.Callable] generator_or_function: t.Union[
) -> t.Generator: 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. """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 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 memory leaks with badly written WSGI middlewares. The downside is that if
@ -821,3 +824,13 @@ def is_ip(value: str) -> bool:
return True return True
return False 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

View file

@ -33,8 +33,10 @@ if t.TYPE_CHECKING:
# a singleton sentinel value for parameter defaults # a singleton sentinel value for parameter defaults
_sentinel = object() _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 """Wraps a method so that it performs a check in debug mode if the
first request was already handled. first request was already handled.
""" """
@ -53,7 +55,7 @@ def setupmethod(f: t.Callable) -> t.Callable:
) )
return f(self, *args, **kwargs) return f(self, *args, **kwargs)
return update_wrapper(wrapper_func, f) return t.cast(F, update_wrapper(wrapper_func, f))
class Scaffold: class Scaffold:
@ -443,7 +445,7 @@ class Scaffold:
view_func: t.Optional[t.Callable] = None, view_func: t.Optional[t.Callable] = None,
provide_automatic_options: t.Optional[bool] = None, provide_automatic_options: t.Optional[bool] = None,
**options: t.Any, **options: t.Any,
) -> t.Callable: ) -> None:
"""Register a rule for routing incoming requests and building """Register a rule for routing incoming requests and building
URLs. The :meth:`route` decorator is a shortcut to call this URLs. The :meth:`route` decorator is a shortcut to call this
with the ``view_func`` argument. These are equivalent: with the ``view_func`` argument. These are equivalent:
@ -642,7 +644,7 @@ class Scaffold:
@setupmethod @setupmethod
def errorhandler( def errorhandler(
self, code_or_exception: t.Union[t.Type[Exception], int] 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. """Register a function to handle errors by code or exception class.
A decorator that is used to register a function given an A decorator that is used to register a function given an

View file

@ -6,6 +6,7 @@ from werkzeug.wrappers import Response as ResponseBase
from . import json from . import json
from .globals import current_app from .globals import current_app
from .helpers import _split_blueprint_path
if t.TYPE_CHECKING: if t.TYPE_CHECKING:
import typing_extensions as te import typing_extensions as te
@ -59,23 +60,54 @@ class Request(RequestBase):
@property @property
def endpoint(self) -> t.Optional[str]: def endpoint(self) -> t.Optional[str]:
"""The endpoint that matched the request. This in combination with """The endpoint that matched the request URL.
:attr:`view_args` can be used to reconstruct the same or a
modified URL. If an exception happened when matching, this will This will be ``None`` if matching failed or has not been
be ``None``. 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: if self.url_rule is not None:
return self.url_rule.endpoint return self.url_rule.endpoint
else:
return None return None
@property @property
def blueprint(self) -> t.Optional[str]: def blueprint(self) -> t.Optional[str]:
"""The name of the current blueprint""" """The registered name of the current blueprint.
if self.url_rule and "." in self.url_rule.endpoint:
return self.url_rule.endpoint.rsplit(".", 1)[0] This will be ``None`` if the endpoint is not part of a
else: blueprint, or if URL matching failed or has not been performed
return None yet.
This does not necessarily match the name the blueprint was
created with. It may have been nested, or registered with a
different name.
"""
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 []
return _split_blueprint_path(name)
def _load_form_data(self) -> None: def _load_form_data(self) -> None:
RequestBase._load_form_data(self) RequestBase._load_form_data(self)

View file

@ -140,7 +140,7 @@ def test_blueprint_url_defaults(app, client):
return str(bar) return str(bar)
app.register_blueprint(bp, url_prefix="/1", url_defaults={"bar": 23}) 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("/1/foo").data == b"23/42"
assert client.get("/2/foo").data == b"19/42" assert client.get("/2/foo").data == b"19/42"
@ -837,48 +837,77 @@ def test_nested_blueprint(app, client):
assert client.get("/parent/child/grandchild/no").data == b"Grandchild no" assert client.get("/parent/child/grandchild/no").data == b"Grandchild no"
def test_nested_blueprint_url_prefix(app, client): @pytest.mark.parametrize(
parent = flask.Blueprint("parent", __name__, url_prefix="/parent") "parent_init, child_init, parent_registration, child_registration",
child = flask.Blueprint("child", __name__, url_prefix="/child") [
grandchild = flask.Blueprint("grandchild", __name__, url_prefix="/grandchild") ("/parent", "/child", None, None),
apple = flask.Blueprint("apple", __name__, url_prefix="/apple") ("/parent", None, None, "/child"),
(None, None, "/parent", "/child"),
@parent.route("/") ("/other", "/something", "/parent", "/child"),
def parent_index(): ],
return "Parent" )
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("/") @child.route("/")
def child_index(): def index():
return "Child" return "index"
@grandchild.route("/") parent.register_blueprint(child, url_prefix=child_registration)
def grandchild_index(): app.register_blueprint(parent, url_prefix=parent_registration)
return "Grandchild"
@apple.route("/") response = client.get("/parent/child/")
def apple_index(): assert response.status_code == 200
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): def test_unique_blueprint_names(app, client) -> None:
parent = flask.Blueprint("parent", __name__) bp = flask.Blueprint("bp", __name__)
child = flask.Blueprint("child", __name__) bp2 = flask.Blueprint("bp", __name__)
@child.route("/child-endpoint") app.register_blueprint(bp)
def child_index():
return "Child"
parent.register_blueprint(child) with pytest.warns(UserWarning):
app.register_blueprint(parent, url_prefix="/parent") app.register_blueprint(bp) # same bp, same name, warning
assert client.get("/parent/child-endpoint").data == b"Child" app.register_blueprint(bp, name="again") # same bp, different name, ok
with pytest.raises(ValueError):
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)
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"