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
|
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
|
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
|
.. _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
|
.. _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
|
.. _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
|
.. _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 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 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 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 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/>`_
|
- `Deploying on PythonAnywhere <https://help.pythonanywhere.com/pages/Flask/>`_
|
||||||
|
|
|
||||||
|
|
@ -1,46 +1,46 @@
|
||||||
from markupsafe import escape
|
from markupsafe import escape
|
||||||
from markupsafe import Markup
|
from markupsafe import Markup
|
||||||
from werkzeug.exceptions import abort
|
from werkzeug.exceptions import abort as abort
|
||||||
from werkzeug.utils import redirect
|
from werkzeug.utils import redirect as redirect
|
||||||
|
|
||||||
from . import json
|
from . import json as json
|
||||||
from .app import Flask
|
from .app import Flask as Flask
|
||||||
from .app import Request
|
from .app import Request as Request
|
||||||
from .app import Response
|
from .app import Response as Response
|
||||||
from .blueprints import Blueprint
|
from .blueprints import Blueprint as Blueprint
|
||||||
from .config import Config
|
from .config import Config as Config
|
||||||
from .ctx import after_this_request
|
from .ctx import after_this_request as after_this_request
|
||||||
from .ctx import copy_current_request_context
|
from .ctx import copy_current_request_context as copy_current_request_context
|
||||||
from .ctx import has_app_context
|
from .ctx import has_app_context as has_app_context
|
||||||
from .ctx import has_request_context
|
from .ctx import has_request_context as has_request_context
|
||||||
from .globals import _app_ctx_stack
|
from .globals import _app_ctx_stack as _app_ctx_stack
|
||||||
from .globals import _request_ctx_stack
|
from .globals import _request_ctx_stack as _request_ctx_stack
|
||||||
from .globals import current_app
|
from .globals import current_app as current_app
|
||||||
from .globals import g
|
from .globals import g as g
|
||||||
from .globals import request
|
from .globals import request as request
|
||||||
from .globals import session
|
from .globals import session as session
|
||||||
from .helpers import flash
|
from .helpers import flash as flash
|
||||||
from .helpers import get_flashed_messages
|
from .helpers import get_flashed_messages as get_flashed_messages
|
||||||
from .helpers import get_template_attribute
|
from .helpers import get_template_attribute as get_template_attribute
|
||||||
from .helpers import make_response
|
from .helpers import make_response as make_response
|
||||||
from .helpers import safe_join
|
from .helpers import safe_join as safe_join
|
||||||
from .helpers import send_file
|
from .helpers import send_file as send_file
|
||||||
from .helpers import send_from_directory
|
from .helpers import send_from_directory as send_from_directory
|
||||||
from .helpers import stream_with_context
|
from .helpers import stream_with_context as stream_with_context
|
||||||
from .helpers import url_for
|
from .helpers import url_for as url_for
|
||||||
from .json import jsonify
|
from .json import jsonify as jsonify
|
||||||
from .signals import appcontext_popped
|
from .signals import appcontext_popped as appcontext_popped
|
||||||
from .signals import appcontext_pushed
|
from .signals import appcontext_pushed as appcontext_pushed
|
||||||
from .signals import appcontext_tearing_down
|
from .signals import appcontext_tearing_down as appcontext_tearing_down
|
||||||
from .signals import before_render_template
|
from .signals import before_render_template as before_render_template
|
||||||
from .signals import got_request_exception
|
from .signals import got_request_exception as got_request_exception
|
||||||
from .signals import message_flashed
|
from .signals import message_flashed as message_flashed
|
||||||
from .signals import request_finished
|
from .signals import request_finished as request_finished
|
||||||
from .signals import request_started
|
from .signals import request_started as request_started
|
||||||
from .signals import request_tearing_down
|
from .signals import request_tearing_down as request_tearing_down
|
||||||
from .signals import signals_available
|
from .signals import signals_available as signals_available
|
||||||
from .signals import template_rendered
|
from .signals import template_rendered as template_rendered
|
||||||
from .templating import render_template
|
from .templating import render_template as render_template
|
||||||
from .templating import render_template_string
|
from .templating import render_template_string as render_template_string
|
||||||
|
|
||||||
__version__ = "2.1.0.dev0"
|
__version__ = "2.1.0.dev0"
|
||||||
|
|
|
||||||
|
|
@ -72,6 +72,7 @@ from .wrappers import Request
|
||||||
from .wrappers import Response
|
from .wrappers import Response
|
||||||
|
|
||||||
if t.TYPE_CHECKING:
|
if t.TYPE_CHECKING:
|
||||||
|
import typing_extensions as te
|
||||||
from .blueprints import Blueprint
|
from .blueprints import Blueprint
|
||||||
from .testing import FlaskClient
|
from .testing import FlaskClient
|
||||||
from .testing import FlaskCliRunner
|
from .testing import FlaskCliRunner
|
||||||
|
|
@ -1441,7 +1442,7 @@ class Flask(Scaffold):
|
||||||
f"Exception on {request.path} [{request.method}]", exc_info=exc_info
|
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
|
"""Exceptions that are recording during routing are reraised with
|
||||||
this method. During debug we are not reraising redirect requests
|
this method. During debug we are not reraising redirect requests
|
||||||
for non ``GET``, ``HEAD``, or ``OPTIONS`` requests and we're raising
|
for non ``GET``, ``HEAD``, or ``OPTIONS`` requests and we're raising
|
||||||
|
|
|
||||||
|
|
@ -188,6 +188,10 @@ class Blueprint(Scaffold):
|
||||||
template_folder=template_folder,
|
template_folder=template_folder,
|
||||||
root_path=root_path,
|
root_path=root_path,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
if "." in name:
|
||||||
|
raise ValueError("'name' may not contain a dot '.' character.")
|
||||||
|
|
||||||
self.name = name
|
self.name = name
|
||||||
self.url_prefix = url_prefix
|
self.url_prefix = url_prefix
|
||||||
self.subdomain = subdomain
|
self.subdomain = subdomain
|
||||||
|
|
@ -256,7 +260,7 @@ class Blueprint(Scaffold):
|
||||||
"""Called by :meth:`Flask.register_blueprint` to register all
|
"""Called by :meth:`Flask.register_blueprint` to register all
|
||||||
views and callbacks registered on the blueprint with the
|
views and callbacks registered on the blueprint with the
|
||||||
application. Creates a :class:`.BlueprintSetupState` and calls
|
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
|
:param app: The application this blueprint is being registered
|
||||||
with.
|
with.
|
||||||
|
|
@ -340,13 +344,17 @@ class Blueprint(Scaffold):
|
||||||
app.cli.add_command(self.cli)
|
app.cli.add_command(self.cli)
|
||||||
|
|
||||||
for blueprint, bp_options in self._blueprints:
|
for blueprint, bp_options in self._blueprints:
|
||||||
url_prefix = options.get("url_prefix", "")
|
bp_options = bp_options.copy()
|
||||||
if "url_prefix" in bp_options:
|
bp_url_prefix = bp_options.get("url_prefix")
|
||||||
url_prefix = (
|
|
||||||
url_prefix.rstrip("/") + "/" + bp_options["url_prefix"].lstrip("/")
|
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 + "."
|
bp_options["name_prefix"] = options.get("name_prefix", "") + self.name + "."
|
||||||
blueprint.register(app, bp_options)
|
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
|
"""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.
|
the :func:`url_for` function is prefixed with the name of the blueprint.
|
||||||
"""
|
"""
|
||||||
if endpoint:
|
if endpoint and "." in endpoint:
|
||||||
assert "." not in endpoint, "Blueprint endpoints should not contain dots"
|
raise ValueError("'endpoint' may not contain a dot '.' character.")
|
||||||
if view_func and hasattr(view_func, "__name__"):
|
|
||||||
assert (
|
if view_func and hasattr(view_func, "__name__") and "." in view_func.__name__:
|
||||||
"." not in view_func.__name__
|
raise ValueError("'view_func' name may not contain a dot '.' character.")
|
||||||
), "Blueprint view function name should not contain dots"
|
|
||||||
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, **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:
|
||||||
|
|
|
||||||
|
|
@ -41,6 +41,24 @@ class _AppCtxGlobals:
|
||||||
.. versionadded:: 0.10
|
.. 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:
|
def get(self, name: str, default: t.Optional[t.Any] = None) -> t.Any:
|
||||||
"""Get an attribute by name, or a default value. Like
|
"""Get an attribute by name, or a default value. Like
|
||||||
:meth:`dict.get`.
|
:meth:`dict.get`.
|
||||||
|
|
@ -78,10 +96,10 @@ class _AppCtxGlobals:
|
||||||
"""
|
"""
|
||||||
return self.__dict__.setdefault(name, default)
|
return self.__dict__.setdefault(name, default)
|
||||||
|
|
||||||
def __contains__(self, item: t.Any) -> bool:
|
def __contains__(self, item: str) -> bool:
|
||||||
return item in self.__dict__
|
return item in self.__dict__
|
||||||
|
|
||||||
def __iter__(self) -> t.Iterator:
|
def __iter__(self) -> t.Iterator[str]:
|
||||||
return iter(self.__dict__)
|
return iter(self.__dict__)
|
||||||
|
|
||||||
def __repr__(self) -> str:
|
def __repr__(self) -> str:
|
||||||
|
|
|
||||||
|
|
@ -6,7 +6,7 @@ from werkzeug.local import LocalStack
|
||||||
|
|
||||||
if t.TYPE_CHECKING:
|
if t.TYPE_CHECKING:
|
||||||
from .app import Flask
|
from .app import Flask
|
||||||
from .ctx import AppContext
|
from .ctx import _AppCtxGlobals
|
||||||
from .sessions import SessionMixin
|
from .sessions import SessionMixin
|
||||||
from .wrappers import Request
|
from .wrappers import Request
|
||||||
|
|
||||||
|
|
@ -53,5 +53,7 @@ _request_ctx_stack = LocalStack()
|
||||||
_app_ctx_stack = LocalStack()
|
_app_ctx_stack = LocalStack()
|
||||||
current_app: "Flask" = LocalProxy(_find_app) # type: ignore
|
current_app: "Flask" = LocalProxy(_find_app) # type: ignore
|
||||||
request: "Request" = LocalProxy(partial(_lookup_req_object, "request")) # type: ignore
|
request: "Request" = LocalProxy(partial(_lookup_req_object, "request")) # type: ignore
|
||||||
session: "SessionMixin" = LocalProxy(partial(_lookup_req_object, "session")) # type: ignore # noqa: B950
|
session: "SessionMixin" = LocalProxy( # type: ignore
|
||||||
g: "AppContext" = LocalProxy(partial(_lookup_app_object, "g")) # 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 sys
|
||||||
import typing as t
|
import typing as t
|
||||||
import warnings
|
import warnings
|
||||||
|
from datetime import datetime
|
||||||
from datetime import timedelta
|
from datetime import timedelta
|
||||||
from functools import update_wrapper
|
from functools import update_wrapper
|
||||||
from threading import RLock
|
from threading import RLock
|
||||||
|
|
@ -436,14 +437,16 @@ def get_flashed_messages(
|
||||||
|
|
||||||
|
|
||||||
def _prepare_send_file_kwargs(
|
def _prepare_send_file_kwargs(
|
||||||
download_name=None,
|
download_name: t.Optional[str] = None,
|
||||||
attachment_filename=None,
|
attachment_filename: t.Optional[str] = None,
|
||||||
etag=None,
|
etag: t.Optional[t.Union[bool, str]] = None,
|
||||||
add_etags=None,
|
add_etags: t.Optional[t.Union[bool]] = None,
|
||||||
max_age=None,
|
max_age: t.Optional[
|
||||||
cache_timeout=None,
|
t.Union[int, t.Callable[[t.Optional[str]], t.Optional[int]]]
|
||||||
**kwargs,
|
] = None,
|
||||||
):
|
cache_timeout: t.Optional[int] = None,
|
||||||
|
**kwargs: t.Any,
|
||||||
|
) -> t.Dict[str, t.Any]:
|
||||||
if attachment_filename is not None:
|
if attachment_filename is not None:
|
||||||
warnings.warn(
|
warnings.warn(
|
||||||
"The 'attachment_filename' parameter has been renamed to"
|
"The 'attachment_filename' parameter has been renamed to"
|
||||||
|
|
@ -482,23 +485,25 @@ def _prepare_send_file_kwargs(
|
||||||
max_age=max_age,
|
max_age=max_age,
|
||||||
use_x_sendfile=current_app.use_x_sendfile,
|
use_x_sendfile=current_app.use_x_sendfile,
|
||||||
response_class=current_app.response_class,
|
response_class=current_app.response_class,
|
||||||
_root_path=current_app.root_path,
|
_root_path=current_app.root_path, # type: ignore
|
||||||
)
|
)
|
||||||
return kwargs
|
return kwargs
|
||||||
|
|
||||||
|
|
||||||
def send_file(
|
def send_file(
|
||||||
path_or_file,
|
path_or_file: t.Union[os.PathLike, str, t.BinaryIO],
|
||||||
mimetype=None,
|
mimetype: t.Optional[str] = None,
|
||||||
as_attachment=False,
|
as_attachment: bool = False,
|
||||||
download_name=None,
|
download_name: t.Optional[str] = None,
|
||||||
attachment_filename=None,
|
attachment_filename: t.Optional[str] = None,
|
||||||
conditional=True,
|
conditional: bool = True,
|
||||||
etag=True,
|
etag: t.Union[bool, str] = True,
|
||||||
add_etags=None,
|
add_etags: t.Optional[bool] = None,
|
||||||
last_modified=None,
|
last_modified: t.Optional[t.Union[datetime, int, float]] = None,
|
||||||
max_age=None,
|
max_age: t.Optional[
|
||||||
cache_timeout=None,
|
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.
|
"""Send the contents of a file to the client.
|
||||||
|
|
||||||
|
|
@ -642,7 +647,12 @@ def safe_join(directory: str, *pathnames: str) -> str:
|
||||||
return path
|
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`.
|
"""Send a file from within a directory using :func:`send_file`.
|
||||||
|
|
||||||
.. code-block:: python
|
.. code-block:: python
|
||||||
|
|
@ -666,12 +676,24 @@ def send_from_directory(directory: str, path: str, **kwargs: t.Any) -> "Response
|
||||||
``directory``.
|
``directory``.
|
||||||
:param kwargs: Arguments to pass to :func:`send_file`.
|
:param kwargs: Arguments to pass to :func:`send_file`.
|
||||||
|
|
||||||
|
.. versionchanged:: 2.0
|
||||||
|
``path`` replaces the ``filename`` parameter.
|
||||||
|
|
||||||
.. versionadded:: 2.0
|
.. versionadded:: 2.0
|
||||||
Moved the implementation to Werkzeug. This is now a wrapper to
|
Moved the implementation to Werkzeug. This is now a wrapper to
|
||||||
pass some Flask-specific arguments.
|
pass some Flask-specific arguments.
|
||||||
|
|
||||||
.. versionadded:: 0.5
|
.. 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
|
return werkzeug.utils.send_from_directory( # type: ignore
|
||||||
directory, path, **_prepare_send_file_kwargs(**kwargs)
|
directory, path, **_prepare_send_file_kwargs(**kwargs)
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -288,7 +288,7 @@ class Scaffold:
|
||||||
|
|
||||||
self._static_url_path = value
|
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
|
"""Used by :func:`send_file` to determine the ``max_age`` cache
|
||||||
value for a given file path if it wasn't passed.
|
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
|
from .json.tag import TaggedJSONSerializer
|
||||||
|
|
||||||
if t.TYPE_CHECKING:
|
if t.TYPE_CHECKING:
|
||||||
|
import typing_extensions as te
|
||||||
from .app import Flask
|
from .app import Flask
|
||||||
from .wrappers import Request, Response
|
from .wrappers import Request, Response
|
||||||
|
|
||||||
|
|
@ -92,7 +93,7 @@ class NullSession(SecureCookieSession):
|
||||||
but fail on setting.
|
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(
|
raise RuntimeError(
|
||||||
"The session is unavailable because no secret "
|
"The session is unavailable because no secret "
|
||||||
"key was set. Set the secret_key on the "
|
"key was set. Set the secret_key on the "
|
||||||
|
|
|
||||||
|
|
@ -8,6 +8,7 @@ from . import json
|
||||||
from .globals import current_app
|
from .globals import current_app
|
||||||
|
|
||||||
if t.TYPE_CHECKING:
|
if t.TYPE_CHECKING:
|
||||||
|
import typing_extensions as te
|
||||||
from werkzeug.routing import Rule
|
from werkzeug.routing import Rule
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -91,7 +92,7 @@ class Request(RequestBase):
|
||||||
|
|
||||||
attach_enctype_error_multidict(self)
|
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:
|
if current_app and current_app.debug:
|
||||||
raise BadRequest(f"Failed to decode JSON object: {e}")
|
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):
|
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
|
@bp.url_defaults
|
||||||
def bp_defaults(endpoint, values):
|
def bp_defaults(endpoint, values):
|
||||||
|
|
@ -1644,12 +1644,12 @@ def test_inject_blueprint_url_defaults(app):
|
||||||
app.register_blueprint(bp)
|
app.register_blueprint(bp)
|
||||||
|
|
||||||
values = dict()
|
values = dict()
|
||||||
app.inject_url_defaults("foo.bar.baz.view", values)
|
app.inject_url_defaults("foo.view", values)
|
||||||
expected = dict(page="login")
|
expected = dict(page="login")
|
||||||
assert values == expected
|
assert values == expected
|
||||||
|
|
||||||
with app.test_request_context("/somepage"):
|
with app.test_request_context("/somepage"):
|
||||||
url = flask.url_for("foo.bar.baz.view")
|
url = flask.url_for("foo.view")
|
||||||
expected = "/login"
|
expected = "/login"
|
||||||
assert url == expected
|
assert url == expected
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,3 @@
|
||||||
import functools
|
|
||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
from jinja2 import TemplateNotFound
|
from jinja2 import TemplateNotFound
|
||||||
from werkzeug.http import parse_cache_control_header
|
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"]
|
assert templates == ["admin/index.html", "frontend/index.html"]
|
||||||
|
|
||||||
|
|
||||||
def test_dotted_names(app, client):
|
def test_dotted_name_not_allowed(app, client):
|
||||||
frontend = flask.Blueprint("myapp.frontend", __name__)
|
with pytest.raises(ValueError):
|
||||||
backend = flask.Blueprint("myapp.backend", __name__)
|
flask.Blueprint("app.ui", __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_names_from_app(app, client):
|
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):
|
def test_route_decorator_custom_endpoint_with_dots(app, client):
|
||||||
bp = flask.Blueprint("bp", __name__)
|
bp = flask.Blueprint("bp", __name__)
|
||||||
|
|
||||||
@bp.route("/foo")
|
with pytest.raises(ValueError):
|
||||||
def foo():
|
bp.route("/", endpoint="a.b")(lambda: "")
|
||||||
return flask.request.endpoint
|
|
||||||
|
|
||||||
try:
|
with pytest.raises(ValueError):
|
||||||
|
bp.add_url_rule("/", endpoint="a.b")
|
||||||
|
|
||||||
@bp.route("/bar", endpoint="bar.bar")
|
def view():
|
||||||
def foo_bar():
|
return ""
|
||||||
return flask.request.endpoint
|
|
||||||
|
|
||||||
except AssertionError:
|
view.__name__ = "a.b"
|
||||||
pass
|
|
||||||
else:
|
|
||||||
raise AssertionError("expected AssertionError not raised")
|
|
||||||
|
|
||||||
try:
|
with pytest.raises(ValueError):
|
||||||
|
bp.add_url_rule("/", view_func=view)
|
||||||
@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
|
|
||||||
|
|
||||||
|
|
||||||
def test_endpoint_decorator(app, client):
|
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/no").data == b"Parent no"
|
||||||
assert client.get("/parent/child/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"
|
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