forked from orbit-oss/flask
Merge remote-tracking branch 'origin/2.0.x'
This commit is contained in:
commit
4240ace597
14 changed files with 202 additions and 163 deletions
16
CHANGES.rst
16
CHANGES.rst
|
|
@ -13,6 +13,22 @@ Version 2.0.1
|
|||
|
||||
Unreleased
|
||||
|
||||
- Re-add the ``filename`` parameter in ``send_from_directory``. The
|
||||
``filename`` parameter has been renamed to ``path``, the old name
|
||||
is deprecated. :pr:`4019`
|
||||
- Mark top-level names as exported so type checking understands
|
||||
imports in user projects. :issue:`4024`
|
||||
- Fix type annotation for ``g`` and inform mypy that it is a namespace
|
||||
object that has arbitrary attributes. :issue:`4020`
|
||||
- Fix some types that weren't available in Python 3.6.0. :issue:`4040`
|
||||
- Improve typing for ``send_file``, ``send_from_directory``, and
|
||||
``get_send_file_max_age``. :issue:`4044`, :pr:`4026`
|
||||
- Show an error when a blueprint name contains a dot. The ``.`` has
|
||||
special meaning, it is used to separate (nested) blueprint names and
|
||||
the endpoint name. :issue:`4041`
|
||||
- Combine URL prefixes when nesting blueprints that were created with
|
||||
a ``url_prefix`` value. :issue:`4037`
|
||||
|
||||
|
||||
Version 2.0.0
|
||||
-------------
|
||||
|
|
|
|||
|
|
@ -129,7 +129,7 @@ First time setup
|
|||
.. _username: https://docs.github.com/en/github/using-git/setting-your-username-in-git
|
||||
.. _email: https://docs.github.com/en/github/setting-up-and-managing-your-github-user-account/setting-your-commit-email-address
|
||||
.. _GitHub account: https://github.com/join
|
||||
.. _Fork: https://github.com/pallets/jinja/fork
|
||||
.. _Fork: https://github.com/pallets/flask/fork
|
||||
.. _Clone: https://docs.github.com/en/github/getting-started-with-github/fork-a-repo#step-2-create-a-local-clone-of-your-fork
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -16,6 +16,7 @@ Hosted options
|
|||
|
||||
- `Deploying Flask on Heroku <https://devcenter.heroku.com/articles/getting-started-with-python>`_
|
||||
- `Deploying Flask on Google App Engine <https://cloud.google.com/appengine/docs/standard/python3/runtime>`_
|
||||
- `Deploying Flask on Google Cloud Run <https://cloud.google.com/run/docs/quickstarts/build-and-deploy/python>`_
|
||||
- `Deploying Flask on AWS Elastic Beanstalk <https://docs.aws.amazon.com/elasticbeanstalk/latest/dg/create-deploy-python-flask.html>`_
|
||||
- `Deploying on Azure (IIS) <https://docs.microsoft.com/en-us/azure/app-service/containers/how-to-configure-python>`_
|
||||
- `Deploying on PythonAnywhere <https://help.pythonanywhere.com/pages/Flask/>`_
|
||||
|
|
|
|||
|
|
@ -1,46 +1,46 @@
|
|||
from markupsafe import escape
|
||||
from markupsafe import Markup
|
||||
from werkzeug.exceptions import abort
|
||||
from werkzeug.utils import redirect
|
||||
from werkzeug.exceptions import abort as abort
|
||||
from werkzeug.utils import redirect as redirect
|
||||
|
||||
from . import json
|
||||
from .app import Flask
|
||||
from .app import Request
|
||||
from .app import Response
|
||||
from .blueprints import Blueprint
|
||||
from .config import Config
|
||||
from .ctx import after_this_request
|
||||
from .ctx import copy_current_request_context
|
||||
from .ctx import has_app_context
|
||||
from .ctx import has_request_context
|
||||
from .globals import _app_ctx_stack
|
||||
from .globals import _request_ctx_stack
|
||||
from .globals import current_app
|
||||
from .globals import g
|
||||
from .globals import request
|
||||
from .globals import session
|
||||
from .helpers import flash
|
||||
from .helpers import get_flashed_messages
|
||||
from .helpers import get_template_attribute
|
||||
from .helpers import make_response
|
||||
from .helpers import safe_join
|
||||
from .helpers import send_file
|
||||
from .helpers import send_from_directory
|
||||
from .helpers import stream_with_context
|
||||
from .helpers import url_for
|
||||
from .json import jsonify
|
||||
from .signals import appcontext_popped
|
||||
from .signals import appcontext_pushed
|
||||
from .signals import appcontext_tearing_down
|
||||
from .signals import before_render_template
|
||||
from .signals import got_request_exception
|
||||
from .signals import message_flashed
|
||||
from .signals import request_finished
|
||||
from .signals import request_started
|
||||
from .signals import request_tearing_down
|
||||
from .signals import signals_available
|
||||
from .signals import template_rendered
|
||||
from .templating import render_template
|
||||
from .templating import render_template_string
|
||||
from . import json as json
|
||||
from .app import Flask as Flask
|
||||
from .app import Request as Request
|
||||
from .app import Response as Response
|
||||
from .blueprints import Blueprint as Blueprint
|
||||
from .config import Config as Config
|
||||
from .ctx import after_this_request as after_this_request
|
||||
from .ctx import copy_current_request_context as copy_current_request_context
|
||||
from .ctx import has_app_context as has_app_context
|
||||
from .ctx import has_request_context as has_request_context
|
||||
from .globals import _app_ctx_stack as _app_ctx_stack
|
||||
from .globals import _request_ctx_stack as _request_ctx_stack
|
||||
from .globals import current_app as current_app
|
||||
from .globals import g as g
|
||||
from .globals import request as request
|
||||
from .globals import session as session
|
||||
from .helpers import flash as flash
|
||||
from .helpers import get_flashed_messages as get_flashed_messages
|
||||
from .helpers import get_template_attribute as get_template_attribute
|
||||
from .helpers import make_response as make_response
|
||||
from .helpers import safe_join as safe_join
|
||||
from .helpers import send_file as send_file
|
||||
from .helpers import send_from_directory as send_from_directory
|
||||
from .helpers import stream_with_context as stream_with_context
|
||||
from .helpers import url_for as url_for
|
||||
from .json import jsonify as jsonify
|
||||
from .signals import appcontext_popped as appcontext_popped
|
||||
from .signals import appcontext_pushed as appcontext_pushed
|
||||
from .signals import appcontext_tearing_down as appcontext_tearing_down
|
||||
from .signals import before_render_template as before_render_template
|
||||
from .signals import got_request_exception as got_request_exception
|
||||
from .signals import message_flashed as message_flashed
|
||||
from .signals import request_finished as request_finished
|
||||
from .signals import request_started as request_started
|
||||
from .signals import request_tearing_down as request_tearing_down
|
||||
from .signals import signals_available as signals_available
|
||||
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.1.0.dev0"
|
||||
|
|
|
|||
|
|
@ -72,6 +72,7 @@ from .wrappers import Request
|
|||
from .wrappers import Response
|
||||
|
||||
if t.TYPE_CHECKING:
|
||||
import typing_extensions as te
|
||||
from .blueprints import Blueprint
|
||||
from .testing import FlaskClient
|
||||
from .testing import FlaskCliRunner
|
||||
|
|
@ -1441,7 +1442,7 @@ class Flask(Scaffold):
|
|||
f"Exception on {request.path} [{request.method}]", exc_info=exc_info
|
||||
)
|
||||
|
||||
def raise_routing_exception(self, request: Request) -> t.NoReturn:
|
||||
def raise_routing_exception(self, request: Request) -> "te.NoReturn":
|
||||
"""Exceptions that are recording during routing are reraised with
|
||||
this method. During debug we are not reraising redirect requests
|
||||
for non ``GET``, ``HEAD``, or ``OPTIONS`` requests and we're raising
|
||||
|
|
|
|||
|
|
@ -188,6 +188,10 @@ class Blueprint(Scaffold):
|
|||
template_folder=template_folder,
|
||||
root_path=root_path,
|
||||
)
|
||||
|
||||
if "." in name:
|
||||
raise ValueError("'name' may not contain a dot '.' character.")
|
||||
|
||||
self.name = name
|
||||
self.url_prefix = url_prefix
|
||||
self.subdomain = subdomain
|
||||
|
|
@ -256,7 +260,7 @@ class Blueprint(Scaffold):
|
|||
"""Called by :meth:`Flask.register_blueprint` to register all
|
||||
views and callbacks registered on the blueprint with the
|
||||
application. Creates a :class:`.BlueprintSetupState` and calls
|
||||
each :meth:`record` callbackwith it.
|
||||
each :meth:`record` callback with it.
|
||||
|
||||
:param app: The application this blueprint is being registered
|
||||
with.
|
||||
|
|
@ -340,13 +344,17 @@ class Blueprint(Scaffold):
|
|||
app.cli.add_command(self.cli)
|
||||
|
||||
for blueprint, bp_options in self._blueprints:
|
||||
url_prefix = options.get("url_prefix", "")
|
||||
if "url_prefix" in bp_options:
|
||||
url_prefix = (
|
||||
url_prefix.rstrip("/") + "/" + bp_options["url_prefix"].lstrip("/")
|
||||
bp_options = bp_options.copy()
|
||||
bp_url_prefix = bp_options.get("url_prefix")
|
||||
|
||||
if bp_url_prefix is None:
|
||||
bp_url_prefix = blueprint.url_prefix
|
||||
|
||||
if state.url_prefix is not None and bp_url_prefix is not None:
|
||||
bp_options["url_prefix"] = (
|
||||
state.url_prefix.rstrip("/") + "/" + bp_url_prefix.lstrip("/")
|
||||
)
|
||||
|
||||
bp_options["url_prefix"] = url_prefix
|
||||
bp_options["name_prefix"] = options.get("name_prefix", "") + self.name + "."
|
||||
blueprint.register(app, bp_options)
|
||||
|
||||
|
|
@ -360,12 +368,12 @@ class Blueprint(Scaffold):
|
|||
"""Like :meth:`Flask.add_url_rule` but for a blueprint. The endpoint for
|
||||
the :func:`url_for` function is prefixed with the name of the blueprint.
|
||||
"""
|
||||
if endpoint:
|
||||
assert "." not in endpoint, "Blueprint endpoints should not contain dots"
|
||||
if view_func and hasattr(view_func, "__name__"):
|
||||
assert (
|
||||
"." not in view_func.__name__
|
||||
), "Blueprint view function name should not contain dots"
|
||||
if endpoint and "." in endpoint:
|
||||
raise ValueError("'endpoint' may not contain a dot '.' character.")
|
||||
|
||||
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))
|
||||
|
||||
def app_template_filter(self, name: t.Optional[str] = None) -> t.Callable:
|
||||
|
|
|
|||
|
|
@ -41,6 +41,24 @@ class _AppCtxGlobals:
|
|||
.. versionadded:: 0.10
|
||||
"""
|
||||
|
||||
# Define attr methods to let mypy know this is a namespace object
|
||||
# that has arbitrary attributes.
|
||||
|
||||
def __getattr__(self, name: str) -> t.Any:
|
||||
try:
|
||||
return self.__dict__[name]
|
||||
except KeyError:
|
||||
raise AttributeError(name) from None
|
||||
|
||||
def __setattr__(self, name: str, value: t.Any) -> None:
|
||||
self.__dict__[name] = value
|
||||
|
||||
def __delattr__(self, name: str) -> None:
|
||||
try:
|
||||
del self.__dict__[name]
|
||||
except KeyError:
|
||||
raise AttributeError(name) from None
|
||||
|
||||
def get(self, name: str, default: t.Optional[t.Any] = None) -> t.Any:
|
||||
"""Get an attribute by name, or a default value. Like
|
||||
:meth:`dict.get`.
|
||||
|
|
@ -78,10 +96,10 @@ class _AppCtxGlobals:
|
|||
"""
|
||||
return self.__dict__.setdefault(name, default)
|
||||
|
||||
def __contains__(self, item: t.Any) -> bool:
|
||||
def __contains__(self, item: str) -> bool:
|
||||
return item in self.__dict__
|
||||
|
||||
def __iter__(self) -> t.Iterator:
|
||||
def __iter__(self) -> t.Iterator[str]:
|
||||
return iter(self.__dict__)
|
||||
|
||||
def __repr__(self) -> str:
|
||||
|
|
|
|||
|
|
@ -6,7 +6,7 @@ from werkzeug.local import LocalStack
|
|||
|
||||
if t.TYPE_CHECKING:
|
||||
from .app import Flask
|
||||
from .ctx import AppContext
|
||||
from .ctx import _AppCtxGlobals
|
||||
from .sessions import SessionMixin
|
||||
from .wrappers import Request
|
||||
|
||||
|
|
@ -53,5 +53,7 @@ _request_ctx_stack = LocalStack()
|
|||
_app_ctx_stack = LocalStack()
|
||||
current_app: "Flask" = LocalProxy(_find_app) # type: ignore
|
||||
request: "Request" = LocalProxy(partial(_lookup_req_object, "request")) # type: ignore
|
||||
session: "SessionMixin" = LocalProxy(partial(_lookup_req_object, "session")) # type: ignore # noqa: B950
|
||||
g: "AppContext" = LocalProxy(partial(_lookup_app_object, "g")) # type: ignore
|
||||
session: "SessionMixin" = LocalProxy( # type: ignore
|
||||
partial(_lookup_req_object, "session")
|
||||
)
|
||||
g: "_AppCtxGlobals" = LocalProxy(partial(_lookup_app_object, "g")) # type: ignore
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@ import socket
|
|||
import sys
|
||||
import typing as t
|
||||
import warnings
|
||||
from datetime import datetime
|
||||
from datetime import timedelta
|
||||
from functools import update_wrapper
|
||||
from threading import RLock
|
||||
|
|
@ -436,14 +437,16 @@ def get_flashed_messages(
|
|||
|
||||
|
||||
def _prepare_send_file_kwargs(
|
||||
download_name=None,
|
||||
attachment_filename=None,
|
||||
etag=None,
|
||||
add_etags=None,
|
||||
max_age=None,
|
||||
cache_timeout=None,
|
||||
**kwargs,
|
||||
):
|
||||
download_name: t.Optional[str] = None,
|
||||
attachment_filename: t.Optional[str] = None,
|
||||
etag: t.Optional[t.Union[bool, str]] = None,
|
||||
add_etags: t.Optional[t.Union[bool]] = None,
|
||||
max_age: t.Optional[
|
||||
t.Union[int, t.Callable[[t.Optional[str]], t.Optional[int]]]
|
||||
] = None,
|
||||
cache_timeout: t.Optional[int] = None,
|
||||
**kwargs: t.Any,
|
||||
) -> t.Dict[str, t.Any]:
|
||||
if attachment_filename is not None:
|
||||
warnings.warn(
|
||||
"The 'attachment_filename' parameter has been renamed to"
|
||||
|
|
@ -482,23 +485,25 @@ def _prepare_send_file_kwargs(
|
|||
max_age=max_age,
|
||||
use_x_sendfile=current_app.use_x_sendfile,
|
||||
response_class=current_app.response_class,
|
||||
_root_path=current_app.root_path,
|
||||
_root_path=current_app.root_path, # type: ignore
|
||||
)
|
||||
return kwargs
|
||||
|
||||
|
||||
def send_file(
|
||||
path_or_file,
|
||||
mimetype=None,
|
||||
as_attachment=False,
|
||||
download_name=None,
|
||||
attachment_filename=None,
|
||||
conditional=True,
|
||||
etag=True,
|
||||
add_etags=None,
|
||||
last_modified=None,
|
||||
max_age=None,
|
||||
cache_timeout=None,
|
||||
path_or_file: t.Union[os.PathLike, str, t.BinaryIO],
|
||||
mimetype: t.Optional[str] = None,
|
||||
as_attachment: bool = False,
|
||||
download_name: t.Optional[str] = None,
|
||||
attachment_filename: t.Optional[str] = None,
|
||||
conditional: bool = True,
|
||||
etag: t.Union[bool, str] = True,
|
||||
add_etags: t.Optional[bool] = None,
|
||||
last_modified: t.Optional[t.Union[datetime, int, float]] = None,
|
||||
max_age: t.Optional[
|
||||
t.Union[int, t.Callable[[t.Optional[str]], t.Optional[int]]]
|
||||
] = None,
|
||||
cache_timeout: t.Optional[int] = None,
|
||||
):
|
||||
"""Send the contents of a file to the client.
|
||||
|
||||
|
|
@ -642,7 +647,12 @@ def safe_join(directory: str, *pathnames: str) -> str:
|
|||
return path
|
||||
|
||||
|
||||
def send_from_directory(directory: str, path: str, **kwargs: t.Any) -> "Response":
|
||||
def send_from_directory(
|
||||
directory: t.Union[os.PathLike, str],
|
||||
path: t.Union[os.PathLike, str],
|
||||
filename: t.Optional[str] = None,
|
||||
**kwargs: t.Any,
|
||||
) -> "Response":
|
||||
"""Send a file from within a directory using :func:`send_file`.
|
||||
|
||||
.. code-block:: python
|
||||
|
|
@ -666,12 +676,24 @@ def send_from_directory(directory: str, path: str, **kwargs: t.Any) -> "Response
|
|||
``directory``.
|
||||
:param kwargs: Arguments to pass to :func:`send_file`.
|
||||
|
||||
.. versionchanged:: 2.0
|
||||
``path`` replaces the ``filename`` parameter.
|
||||
|
||||
.. versionadded:: 2.0
|
||||
Moved the implementation to Werkzeug. This is now a wrapper to
|
||||
pass some Flask-specific arguments.
|
||||
|
||||
.. versionadded:: 0.5
|
||||
"""
|
||||
if filename is not None:
|
||||
warnings.warn(
|
||||
"The 'filename' parameter has been renamed to 'path'. The"
|
||||
" old name will be removed in Flask 2.1.",
|
||||
DeprecationWarning,
|
||||
stacklevel=2,
|
||||
)
|
||||
path = filename
|
||||
|
||||
return werkzeug.utils.send_from_directory( # type: ignore
|
||||
directory, path, **_prepare_send_file_kwargs(**kwargs)
|
||||
)
|
||||
|
|
|
|||
|
|
@ -288,7 +288,7 @@ class Scaffold:
|
|||
|
||||
self._static_url_path = value
|
||||
|
||||
def get_send_file_max_age(self, filename: str) -> t.Optional[int]:
|
||||
def get_send_file_max_age(self, filename: t.Optional[str]) -> t.Optional[int]:
|
||||
"""Used by :func:`send_file` to determine the ``max_age`` cache
|
||||
value for a given file path if it wasn't passed.
|
||||
|
||||
|
|
|
|||
|
|
@ -12,6 +12,7 @@ from .helpers import is_ip
|
|||
from .json.tag import TaggedJSONSerializer
|
||||
|
||||
if t.TYPE_CHECKING:
|
||||
import typing_extensions as te
|
||||
from .app import Flask
|
||||
from .wrappers import Request, Response
|
||||
|
||||
|
|
@ -92,7 +93,7 @@ class NullSession(SecureCookieSession):
|
|||
but fail on setting.
|
||||
"""
|
||||
|
||||
def _fail(self, *args: t.Any, **kwargs: t.Any) -> t.NoReturn:
|
||||
def _fail(self, *args: t.Any, **kwargs: t.Any) -> "te.NoReturn":
|
||||
raise RuntimeError(
|
||||
"The session is unavailable because no secret "
|
||||
"key was set. Set the secret_key on the "
|
||||
|
|
|
|||
|
|
@ -8,6 +8,7 @@ from . import json
|
|||
from .globals import current_app
|
||||
|
||||
if t.TYPE_CHECKING:
|
||||
import typing_extensions as te
|
||||
from werkzeug.routing import Rule
|
||||
|
||||
|
||||
|
|
@ -91,7 +92,7 @@ class Request(RequestBase):
|
|||
|
||||
attach_enctype_error_multidict(self)
|
||||
|
||||
def on_json_loading_failed(self, e: Exception) -> t.NoReturn:
|
||||
def on_json_loading_failed(self, e: Exception) -> "te.NoReturn":
|
||||
if current_app and current_app.debug:
|
||||
raise BadRequest(f"Failed to decode JSON object: {e}")
|
||||
|
||||
|
|
|
|||
|
|
@ -1631,7 +1631,7 @@ def test_url_processors(app, client):
|
|||
|
||||
|
||||
def test_inject_blueprint_url_defaults(app):
|
||||
bp = flask.Blueprint("foo.bar.baz", __name__, template_folder="template")
|
||||
bp = flask.Blueprint("foo", __name__, template_folder="template")
|
||||
|
||||
@bp.url_defaults
|
||||
def bp_defaults(endpoint, values):
|
||||
|
|
@ -1644,12 +1644,12 @@ def test_inject_blueprint_url_defaults(app):
|
|||
app.register_blueprint(bp)
|
||||
|
||||
values = dict()
|
||||
app.inject_url_defaults("foo.bar.baz.view", values)
|
||||
app.inject_url_defaults("foo.view", values)
|
||||
expected = dict(page="login")
|
||||
assert values == expected
|
||||
|
||||
with app.test_request_context("/somepage"):
|
||||
url = flask.url_for("foo.bar.baz.view")
|
||||
url = flask.url_for("foo.view")
|
||||
expected = "/login"
|
||||
assert url == expected
|
||||
|
||||
|
|
|
|||
|
|
@ -1,5 +1,3 @@
|
|||
import functools
|
||||
|
||||
import pytest
|
||||
from jinja2 import TemplateNotFound
|
||||
from werkzeug.http import parse_cache_control_header
|
||||
|
|
@ -253,28 +251,9 @@ def test_templates_list(test_apps):
|
|||
assert templates == ["admin/index.html", "frontend/index.html"]
|
||||
|
||||
|
||||
def test_dotted_names(app, client):
|
||||
frontend = flask.Blueprint("myapp.frontend", __name__)
|
||||
backend = flask.Blueprint("myapp.backend", __name__)
|
||||
|
||||
@frontend.route("/fe")
|
||||
def frontend_index():
|
||||
return flask.url_for("myapp.backend.backend_index")
|
||||
|
||||
@frontend.route("/fe2")
|
||||
def frontend_page2():
|
||||
return flask.url_for(".frontend_index")
|
||||
|
||||
@backend.route("/be")
|
||||
def backend_index():
|
||||
return flask.url_for("myapp.frontend.frontend_index")
|
||||
|
||||
app.register_blueprint(frontend)
|
||||
app.register_blueprint(backend)
|
||||
|
||||
assert client.get("/fe").data.strip() == b"/be"
|
||||
assert client.get("/fe2").data.strip() == b"/fe"
|
||||
assert client.get("/be").data.strip() == b"/fe"
|
||||
def test_dotted_name_not_allowed(app, client):
|
||||
with pytest.raises(ValueError):
|
||||
flask.Blueprint("app.ui", __name__)
|
||||
|
||||
|
||||
def test_dotted_names_from_app(app, client):
|
||||
|
|
@ -343,62 +322,19 @@ def test_route_decorator_custom_endpoint(app, client):
|
|||
def test_route_decorator_custom_endpoint_with_dots(app, client):
|
||||
bp = flask.Blueprint("bp", __name__)
|
||||
|
||||
@bp.route("/foo")
|
||||
def foo():
|
||||
return flask.request.endpoint
|
||||
with pytest.raises(ValueError):
|
||||
bp.route("/", endpoint="a.b")(lambda: "")
|
||||
|
||||
try:
|
||||
with pytest.raises(ValueError):
|
||||
bp.add_url_rule("/", endpoint="a.b")
|
||||
|
||||
@bp.route("/bar", endpoint="bar.bar")
|
||||
def foo_bar():
|
||||
return flask.request.endpoint
|
||||
def view():
|
||||
return ""
|
||||
|
||||
except AssertionError:
|
||||
pass
|
||||
else:
|
||||
raise AssertionError("expected AssertionError not raised")
|
||||
view.__name__ = "a.b"
|
||||
|
||||
try:
|
||||
|
||||
@bp.route("/bar/123", endpoint="bar.123")
|
||||
def foo_bar_foo():
|
||||
return flask.request.endpoint
|
||||
|
||||
except AssertionError:
|
||||
pass
|
||||
else:
|
||||
raise AssertionError("expected AssertionError not raised")
|
||||
|
||||
def foo_foo_foo():
|
||||
pass
|
||||
|
||||
pytest.raises(
|
||||
AssertionError,
|
||||
lambda: bp.add_url_rule("/bar/123", endpoint="bar.123", view_func=foo_foo_foo),
|
||||
)
|
||||
|
||||
pytest.raises(
|
||||
AssertionError, bp.route("/bar/123", endpoint="bar.123"), lambda: None
|
||||
)
|
||||
|
||||
foo_foo_foo.__name__ = "bar.123"
|
||||
|
||||
pytest.raises(
|
||||
AssertionError, lambda: bp.add_url_rule("/bar/123", view_func=foo_foo_foo)
|
||||
)
|
||||
|
||||
bp.add_url_rule(
|
||||
"/bar/456", endpoint="foofoofoo", view_func=functools.partial(foo_foo_foo)
|
||||
)
|
||||
|
||||
app.register_blueprint(bp, url_prefix="/py")
|
||||
|
||||
assert client.get("/py/foo").data == b"bp.foo"
|
||||
# The rule's didn't actually made it through
|
||||
rv = client.get("/py/bar")
|
||||
assert rv.status_code == 404
|
||||
rv = client.get("/py/bar/123")
|
||||
assert rv.status_code == 404
|
||||
with pytest.raises(ValueError):
|
||||
bp.add_url_rule("/", view_func=view)
|
||||
|
||||
|
||||
def test_endpoint_decorator(app, client):
|
||||
|
|
@ -899,3 +835,36 @@ def test_nested_blueprint(app, client):
|
|||
assert client.get("/parent/no").data == b"Parent no"
|
||||
assert client.get("/parent/child/no").data == b"Parent no"
|
||||
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"
|
||||
|
||||
@child.route("/")
|
||||
def child_index():
|
||||
return "Child"
|
||||
|
||||
@grandchild.route("/")
|
||||
def grandchild_index():
|
||||
return "Grandchild"
|
||||
|
||||
@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"
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue