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
*.pyo
env/
venv/
.venv/
env*
dist/
build/

View file

@ -8,11 +8,17 @@ Unreleased
- Update Click dependency to >= 8.0.
Version 2.0.1
Version 2.0.2
-------------
Unreleased
Version 2.0.1
-------------
Released 2021-05-21
- Re-add the ``filename`` parameter in ``send_from_directory``. The
``filename`` parameter has been renamed to ``path``, the old name
is deprecated. :pr:`4019`
@ -33,6 +39,17 @@ 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`
- 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

View file

@ -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.

View file

@ -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 = {}

View file

@ -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
@ -747,7 +748,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()
@ -1018,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)
@ -1089,7 +1096,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 +1130,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 +1173,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::
@ -1261,7 +1274,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:
@ -1782,9 +1795,14 @@ 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])
# 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)
@ -1825,14 +1843,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:
@ -1857,7 +1875,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:
@ -1896,7 +1914,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:
@ -2068,9 +2086,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("."))

View file

@ -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}",
f"{self.name_prefix}.{self.name}.{endpoint}".lstrip("."),
view_func,
defaults=defaults,
**options,
@ -252,8 +254,16 @@ 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
"""
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:
@ -266,23 +276,48 @@ 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.
.. 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:
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."
)
else:
app.blueprints[self.name] = self
first_registration = True
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
state = self.make_setup_state(app, options, first_registration)
@ -298,12 +333,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 +371,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
@ -354,10 +388,12 @@ 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 + "."
bp_options["name_prefix"] = name
blueprint.register(app, bp_options)
def add_url_rule(
@ -365,6 +401,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,9 +413,19 @@ 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:
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.
@ -408,7 +455,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.
@ -442,7 +491,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.

View file

@ -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
@ -63,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
@ -821,3 +824,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

View file

@ -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:
@ -443,7 +445,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:
@ -642,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

View file

@ -6,6 +6,7 @@ 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
@ -59,23 +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.
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.
"""
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:
RequestBase._load_form_data(self)

View file

@ -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"
@ -837,48 +837,77 @@ 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"
response = client.get("/parent/child/")
assert response.status_code == 200
def test_nested_blueprint_url_prefix_only_parent_prefix(app, client):
parent = flask.Blueprint("parent", __name__)
child = flask.Blueprint("child", __name__)
def test_unique_blueprint_names(app, client) -> None:
bp = flask.Blueprint("bp", __name__)
bp2 = flask.Blueprint("bp", __name__)
@child.route("/child-endpoint")
def child_index():
return "Child"
app.register_blueprint(bp)
parent.register_blueprint(child)
app.register_blueprint(parent, url_prefix="/parent")
with pytest.warns(UserWarning):
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"