forked from orbit-oss/flask
Merge pull request #4580 from ionesu/move_url_for_to_the_flask_app_object
Move url_for to the Flask app object
This commit is contained in:
commit
00e2aac937
3 changed files with 200 additions and 158 deletions
156
src/flask/app.py
156
src/flask/app.py
|
|
@ -23,6 +23,7 @@ from werkzeug.routing import MapAdapter
|
||||||
from werkzeug.routing import RequestRedirect
|
from werkzeug.routing import RequestRedirect
|
||||||
from werkzeug.routing import RoutingException
|
from werkzeug.routing import RoutingException
|
||||||
from werkzeug.routing import Rule
|
from werkzeug.routing import Rule
|
||||||
|
from werkzeug.urls import url_quote
|
||||||
from werkzeug.utils import redirect as _wz_redirect
|
from werkzeug.utils import redirect as _wz_redirect
|
||||||
from werkzeug.wrappers import Response as BaseResponse
|
from werkzeug.wrappers import Response as BaseResponse
|
||||||
|
|
||||||
|
|
@ -33,6 +34,7 @@ from .config import ConfigAttribute
|
||||||
from .ctx import _AppCtxGlobals
|
from .ctx import _AppCtxGlobals
|
||||||
from .ctx import AppContext
|
from .ctx import AppContext
|
||||||
from .ctx import RequestContext
|
from .ctx import RequestContext
|
||||||
|
from .globals import _app_ctx_stack
|
||||||
from .globals import _request_ctx_stack
|
from .globals import _request_ctx_stack
|
||||||
from .globals import g
|
from .globals import g
|
||||||
from .globals import request
|
from .globals import request
|
||||||
|
|
@ -439,15 +441,16 @@ class Flask(Scaffold):
|
||||||
#: .. versionadded:: 2.2
|
#: .. versionadded:: 2.2
|
||||||
self.aborter = self.make_aborter()
|
self.aborter = self.make_aborter()
|
||||||
|
|
||||||
#: A list of functions that are called when :meth:`url_for` raises a
|
#: A list of functions that are called by
|
||||||
#: :exc:`~werkzeug.routing.BuildError`. Each function registered here
|
#: :meth:`handle_url_build_error` when :meth:`.url_for` raises a
|
||||||
#: is called with `error`, `endpoint` and `values`. If a function
|
#: :exc:`~werkzeug.routing.BuildError`. Each function is called
|
||||||
#: returns ``None`` or raises a :exc:`BuildError` the next function is
|
#: with ``error``, ``endpoint`` and ``values``. If a function
|
||||||
#: tried.
|
#: returns ``None`` or raises a ``BuildError``, it is skipped.
|
||||||
|
#: Otherwise, its return value is returned by ``url_for``.
|
||||||
#:
|
#:
|
||||||
#: .. versionadded:: 0.9
|
#: .. versionadded:: 0.9
|
||||||
self.url_build_error_handlers: t.List[
|
self.url_build_error_handlers: t.List[
|
||||||
t.Callable[[Exception, str, dict], str]
|
t.Callable[[Exception, str, t.Dict[str, t.Any]], str]
|
||||||
] = []
|
] = []
|
||||||
|
|
||||||
#: A list of functions that will be called at the beginning of the
|
#: A list of functions that will be called at the beginning of the
|
||||||
|
|
@ -1661,6 +1664,130 @@ class Flask(Scaffold):
|
||||||
|
|
||||||
return asgiref_async_to_sync(func)
|
return asgiref_async_to_sync(func)
|
||||||
|
|
||||||
|
def url_for(
|
||||||
|
self,
|
||||||
|
endpoint: str,
|
||||||
|
*,
|
||||||
|
_anchor: t.Optional[str] = None,
|
||||||
|
_method: t.Optional[str] = None,
|
||||||
|
_scheme: t.Optional[str] = None,
|
||||||
|
_external: t.Optional[bool] = None,
|
||||||
|
**values: t.Any,
|
||||||
|
) -> str:
|
||||||
|
"""Generate a URL to the given endpoint with the given values.
|
||||||
|
|
||||||
|
This is called by :func:`flask.url_for`, and can be called
|
||||||
|
directly as well.
|
||||||
|
|
||||||
|
An *endpoint* is the name of a URL rule, usually added with
|
||||||
|
:meth:`@app.route() <route>`, and usually the same name as the
|
||||||
|
view function. A route defined in a :class:`~flask.Blueprint`
|
||||||
|
will prepend the blueprint's name separated by a ``.`` to the
|
||||||
|
endpoint.
|
||||||
|
|
||||||
|
In some cases, such as email messages, you want URLs to include
|
||||||
|
the scheme and domain, like ``https://example.com/hello``. When
|
||||||
|
not in an active request, URLs will be external by default, but
|
||||||
|
this requires setting :data:`SERVER_NAME` so Flask knows what
|
||||||
|
domain to use. :data:`APPLICATION_ROOT` and
|
||||||
|
:data:`PREFERRED_URL_SCHEME` should also be configured as
|
||||||
|
needed. This config is only used when not in an active request.
|
||||||
|
|
||||||
|
Functions can be decorated with :meth:`url_defaults` to modify
|
||||||
|
keyword arguments before the URL is built.
|
||||||
|
|
||||||
|
If building fails for some reason, such as an unknown endpoint
|
||||||
|
or incorrect values, the app's :meth:`handle_url_build_error`
|
||||||
|
method is called. If that returns a string, that is returned,
|
||||||
|
otherwise a :exc:`~werkzeug.routing.BuildError` is raised.
|
||||||
|
|
||||||
|
:param endpoint: The endpoint name associated with the URL to
|
||||||
|
generate. If this starts with a ``.``, the current blueprint
|
||||||
|
name (if any) will be used.
|
||||||
|
:param _anchor: If given, append this as ``#anchor`` to the URL.
|
||||||
|
:param _method: If given, generate the URL associated with this
|
||||||
|
method for the endpoint.
|
||||||
|
:param _scheme: If given, the URL will have this scheme if it
|
||||||
|
is external.
|
||||||
|
:param _external: If given, prefer the URL to be internal
|
||||||
|
(False) or require it to be external (True). External URLs
|
||||||
|
include the scheme and domain. When not in an active
|
||||||
|
request, URLs are external by default.
|
||||||
|
:param values: Values to use for the variable parts of the URL
|
||||||
|
rule. Unknown keys are appended as query string arguments,
|
||||||
|
like ``?a=b&c=d``.
|
||||||
|
|
||||||
|
.. versionadded:: 2.2
|
||||||
|
Moved from ``flask.url_for``, which calls this method.
|
||||||
|
"""
|
||||||
|
req_ctx = _request_ctx_stack.top
|
||||||
|
|
||||||
|
if req_ctx is not None:
|
||||||
|
url_adapter = req_ctx.url_adapter
|
||||||
|
blueprint_name = req_ctx.request.blueprint
|
||||||
|
|
||||||
|
# If the endpoint starts with "." and the request matches a
|
||||||
|
# blueprint, the endpoint is relative to the blueprint.
|
||||||
|
if endpoint[:1] == ".":
|
||||||
|
if blueprint_name is not None:
|
||||||
|
endpoint = f"{blueprint_name}{endpoint}"
|
||||||
|
else:
|
||||||
|
endpoint = endpoint[1:]
|
||||||
|
|
||||||
|
# When in a request, generate a URL without scheme and
|
||||||
|
# domain by default, unless a scheme is given.
|
||||||
|
if _external is None:
|
||||||
|
_external = _scheme is not None
|
||||||
|
else:
|
||||||
|
app_ctx = _app_ctx_stack.top
|
||||||
|
|
||||||
|
# If called by helpers.url_for, an app context is active,
|
||||||
|
# use its url_adapter. Otherwise, app.url_for was called
|
||||||
|
# directly, build an adapter.
|
||||||
|
if app_ctx is not None:
|
||||||
|
url_adapter = app_ctx.url_adapter
|
||||||
|
else:
|
||||||
|
url_adapter = self.create_url_adapter(None)
|
||||||
|
|
||||||
|
if url_adapter is None:
|
||||||
|
raise RuntimeError(
|
||||||
|
"Unable to build URLs outside an active request"
|
||||||
|
" without 'SERVER_NAME' configured. Also configure"
|
||||||
|
" 'APPLICATION_ROOT' and 'PREFERRED_URL_SCHEME' as"
|
||||||
|
" needed."
|
||||||
|
)
|
||||||
|
|
||||||
|
# When outside a request, generate a URL with scheme and
|
||||||
|
# domain by default.
|
||||||
|
if _external is None:
|
||||||
|
_external = True
|
||||||
|
|
||||||
|
# It is an error to set _scheme when _external=False, in order
|
||||||
|
# to avoid accidental insecure URLs.
|
||||||
|
if _scheme is not None and not _external:
|
||||||
|
raise ValueError("When specifying '_scheme', '_external' must be True.")
|
||||||
|
|
||||||
|
self.inject_url_defaults(endpoint, values)
|
||||||
|
|
||||||
|
try:
|
||||||
|
rv = url_adapter.build(
|
||||||
|
endpoint,
|
||||||
|
values,
|
||||||
|
method=_method,
|
||||||
|
url_scheme=_scheme,
|
||||||
|
force_external=_external,
|
||||||
|
)
|
||||||
|
except BuildError as error:
|
||||||
|
values.update(
|
||||||
|
_anchor=_anchor, _method=_method, _scheme=_scheme, _external=_external
|
||||||
|
)
|
||||||
|
return self.handle_url_build_error(error, endpoint, values)
|
||||||
|
|
||||||
|
if _anchor is not None:
|
||||||
|
rv = f"{rv}#{url_quote(_anchor)}"
|
||||||
|
|
||||||
|
return rv
|
||||||
|
|
||||||
def redirect(self, location: str, code: int = 302) -> BaseResponse:
|
def redirect(self, location: str, code: int = 302) -> BaseResponse:
|
||||||
"""Create a redirect response object.
|
"""Create a redirect response object.
|
||||||
|
|
||||||
|
|
@ -1860,10 +1987,21 @@ class Flask(Scaffold):
|
||||||
func(endpoint, values)
|
func(endpoint, values)
|
||||||
|
|
||||||
def handle_url_build_error(
|
def handle_url_build_error(
|
||||||
self, error: Exception, endpoint: str, values: dict
|
self, error: BuildError, endpoint: str, values: t.Dict[str, t.Any]
|
||||||
) -> str:
|
) -> str:
|
||||||
"""Handle :class:`~werkzeug.routing.BuildError` on
|
"""Called by :meth:`.url_for` if a
|
||||||
:meth:`url_for`.
|
:exc:`~werkzeug.routing.BuildError` was raised. If this returns
|
||||||
|
a value, it will be returned by ``url_for``, otherwise the error
|
||||||
|
will be re-raised.
|
||||||
|
|
||||||
|
Each function in :attr:`url_build_error_handlers` is called with
|
||||||
|
``error``, ``endpoint`` and ``values``. If a function returns
|
||||||
|
``None`` or raises a ``BuildError``, it is skipped. Otherwise,
|
||||||
|
its return value is returned by ``url_for``.
|
||||||
|
|
||||||
|
:param error: The active ``BuildError`` being handled.
|
||||||
|
:param endpoint: The endpoint being built.
|
||||||
|
:param values: The keyword arguments passed to ``url_for``.
|
||||||
"""
|
"""
|
||||||
for handler in self.url_build_error_handlers:
|
for handler in self.url_build_error_handlers:
|
||||||
try:
|
try:
|
||||||
|
|
|
||||||
|
|
@ -11,11 +11,8 @@ from threading import RLock
|
||||||
|
|
||||||
import werkzeug.utils
|
import werkzeug.utils
|
||||||
from werkzeug.exceptions import abort as _wz_abort
|
from werkzeug.exceptions import abort as _wz_abort
|
||||||
from werkzeug.routing import BuildError
|
|
||||||
from werkzeug.urls import url_quote
|
|
||||||
from werkzeug.utils import redirect as _wz_redirect
|
from werkzeug.utils import redirect as _wz_redirect
|
||||||
|
|
||||||
from .globals import _app_ctx_stack
|
|
||||||
from .globals import _request_ctx_stack
|
from .globals import _request_ctx_stack
|
||||||
from .globals import current_app
|
from .globals import current_app
|
||||||
from .globals import request
|
from .globals import request
|
||||||
|
|
@ -193,155 +190,58 @@ def make_response(*args: t.Any) -> "Response":
|
||||||
return current_app.make_response(args) # type: ignore
|
return current_app.make_response(args) # type: ignore
|
||||||
|
|
||||||
|
|
||||||
def url_for(endpoint: str, **values: t.Any) -> str:
|
def url_for(
|
||||||
"""Generates a URL to the given endpoint with the method provided.
|
endpoint: str,
|
||||||
|
*,
|
||||||
|
_anchor: t.Optional[str] = None,
|
||||||
|
_method: t.Optional[str] = None,
|
||||||
|
_scheme: t.Optional[str] = None,
|
||||||
|
_external: t.Optional[bool] = None,
|
||||||
|
**values: t.Any,
|
||||||
|
) -> str:
|
||||||
|
"""Generate a URL to the given endpoint with the given values.
|
||||||
|
|
||||||
Variable arguments that are unknown to the target endpoint are appended
|
This requires an active request or application context, and calls
|
||||||
to the generated URL as query arguments. If the value of a query argument
|
:meth:`current_app.url_for() <flask.Flask.url_for>`. See that method
|
||||||
is ``None``, the whole pair is skipped. In case blueprints are active
|
for full documentation.
|
||||||
you can shortcut references to the same blueprint by prefixing the
|
|
||||||
local endpoint with a dot (``.``).
|
|
||||||
|
|
||||||
This will reference the index function local to the current blueprint::
|
:param endpoint: The endpoint name associated with the URL to
|
||||||
|
generate. If this starts with a ``.``, the current blueprint
|
||||||
|
name (if any) will be used.
|
||||||
|
:param _anchor: If given, append this as ``#anchor`` to the URL.
|
||||||
|
:param _method: If given, generate the URL associated with this
|
||||||
|
method for the endpoint.
|
||||||
|
:param _scheme: If given, the URL will have this scheme if it is
|
||||||
|
external.
|
||||||
|
:param _external: If given, prefer the URL to be internal (False) or
|
||||||
|
require it to be external (True). External URLs include the
|
||||||
|
scheme and domain. When not in an active request, URLs are
|
||||||
|
external by default.
|
||||||
|
:param values: Values to use for the variable parts of the URL rule.
|
||||||
|
Unknown keys are appended as query string arguments, like
|
||||||
|
``?a=b&c=d``.
|
||||||
|
|
||||||
url_for('.index')
|
.. versionchanged:: 2.2
|
||||||
|
Calls ``current_app.url_for``, allowing an app to override the
|
||||||
|
behavior.
|
||||||
|
|
||||||
See :ref:`url-building`.
|
.. versionchanged:: 0.10
|
||||||
|
The ``_scheme`` parameter was added.
|
||||||
|
|
||||||
Configuration values ``APPLICATION_ROOT`` and ``SERVER_NAME`` are only used when
|
.. versionchanged:: 0.9
|
||||||
generating URLs outside of a request context.
|
The ``_anchor`` and ``_method`` parameters were added.
|
||||||
|
|
||||||
To integrate applications, :class:`Flask` has a hook to intercept URL build
|
.. versionchanged:: 0.9
|
||||||
errors through :attr:`Flask.url_build_error_handlers`. The `url_for`
|
Calls ``app.handle_url_build_error`` on build errors.
|
||||||
function results in a :exc:`~werkzeug.routing.BuildError` when the current
|
|
||||||
app does not have a URL for the given endpoint and values. When it does, the
|
|
||||||
:data:`~flask.current_app` calls its :attr:`~Flask.url_build_error_handlers` if
|
|
||||||
it is not ``None``, which can return a string to use as the result of
|
|
||||||
`url_for` (instead of `url_for`'s default to raise the
|
|
||||||
:exc:`~werkzeug.routing.BuildError` exception) or re-raise the exception.
|
|
||||||
An example::
|
|
||||||
|
|
||||||
def external_url_handler(error, endpoint, values):
|
|
||||||
"Looks up an external URL when `url_for` cannot build a URL."
|
|
||||||
# This is an example of hooking the build_error_handler.
|
|
||||||
# Here, lookup_url is some utility function you've built
|
|
||||||
# which looks up the endpoint in some external URL registry.
|
|
||||||
url = lookup_url(endpoint, **values)
|
|
||||||
if url is None:
|
|
||||||
# External lookup did not have a URL.
|
|
||||||
# Re-raise the BuildError, in context of original traceback.
|
|
||||||
exc_type, exc_value, tb = sys.exc_info()
|
|
||||||
if exc_value is error:
|
|
||||||
raise exc_type(exc_value).with_traceback(tb)
|
|
||||||
else:
|
|
||||||
raise error
|
|
||||||
# url_for will use this result, instead of raising BuildError.
|
|
||||||
return url
|
|
||||||
|
|
||||||
app.url_build_error_handlers.append(external_url_handler)
|
|
||||||
|
|
||||||
Here, `error` is the instance of :exc:`~werkzeug.routing.BuildError`, and
|
|
||||||
`endpoint` and `values` are the arguments passed into `url_for`. Note
|
|
||||||
that this is for building URLs outside the current application, and not for
|
|
||||||
handling 404 NotFound errors.
|
|
||||||
|
|
||||||
.. versionadded:: 0.10
|
|
||||||
The `_scheme` parameter was added.
|
|
||||||
|
|
||||||
.. versionadded:: 0.9
|
|
||||||
The `_anchor` and `_method` parameters were added.
|
|
||||||
|
|
||||||
.. versionadded:: 0.9
|
|
||||||
Calls :meth:`Flask.handle_build_error` on
|
|
||||||
:exc:`~werkzeug.routing.BuildError`.
|
|
||||||
|
|
||||||
:param endpoint: the endpoint of the URL (name of the function)
|
|
||||||
:param values: the variable arguments of the URL rule
|
|
||||||
:param _external: if set to ``True``, an absolute URL is generated. Server
|
|
||||||
address can be changed via ``SERVER_NAME`` configuration variable which
|
|
||||||
falls back to the `Host` header, then to the IP and port of the request.
|
|
||||||
:param _scheme: a string specifying the desired URL scheme. The `_external`
|
|
||||||
parameter must be set to ``True`` or a :exc:`ValueError` is raised. The default
|
|
||||||
behavior uses the same scheme as the current request, or
|
|
||||||
:data:`PREFERRED_URL_SCHEME` if no request context is available.
|
|
||||||
This also can be set to an empty string to build protocol-relative
|
|
||||||
URLs.
|
|
||||||
:param _anchor: if provided this is added as anchor to the URL.
|
|
||||||
:param _method: if provided this explicitly specifies an HTTP method.
|
|
||||||
"""
|
"""
|
||||||
appctx = _app_ctx_stack.top
|
return current_app.url_for(
|
||||||
reqctx = _request_ctx_stack.top
|
endpoint,
|
||||||
|
_anchor=_anchor,
|
||||||
if appctx is None:
|
_method=_method,
|
||||||
raise RuntimeError(
|
_scheme=_scheme,
|
||||||
"Attempted to generate a URL without the application context being"
|
_external=_external,
|
||||||
" pushed. This has to be executed when application context is"
|
**values,
|
||||||
" available."
|
)
|
||||||
)
|
|
||||||
|
|
||||||
# If request specific information is available we have some extra
|
|
||||||
# features that support "relative" URLs.
|
|
||||||
if reqctx is not None:
|
|
||||||
url_adapter = reqctx.url_adapter
|
|
||||||
blueprint_name = request.blueprint
|
|
||||||
|
|
||||||
if endpoint[:1] == ".":
|
|
||||||
if blueprint_name is not None:
|
|
||||||
endpoint = f"{blueprint_name}{endpoint}"
|
|
||||||
else:
|
|
||||||
endpoint = endpoint[1:]
|
|
||||||
|
|
||||||
external = values.pop("_external", False)
|
|
||||||
|
|
||||||
# Otherwise go with the url adapter from the appctx and make
|
|
||||||
# the URLs external by default.
|
|
||||||
else:
|
|
||||||
url_adapter = appctx.url_adapter
|
|
||||||
|
|
||||||
if url_adapter is None:
|
|
||||||
raise RuntimeError(
|
|
||||||
"Application was not able to create a URL adapter for request"
|
|
||||||
" independent URL generation. You might be able to fix this by"
|
|
||||||
" setting the SERVER_NAME config variable."
|
|
||||||
)
|
|
||||||
|
|
||||||
external = values.pop("_external", True)
|
|
||||||
|
|
||||||
anchor = values.pop("_anchor", None)
|
|
||||||
method = values.pop("_method", None)
|
|
||||||
scheme = values.pop("_scheme", None)
|
|
||||||
appctx.app.inject_url_defaults(endpoint, values)
|
|
||||||
|
|
||||||
# This is not the best way to deal with this but currently the
|
|
||||||
# underlying Werkzeug router does not support overriding the scheme on
|
|
||||||
# a per build call basis.
|
|
||||||
old_scheme = None
|
|
||||||
if scheme is not None:
|
|
||||||
if not external:
|
|
||||||
raise ValueError("When specifying _scheme, _external must be True")
|
|
||||||
old_scheme = url_adapter.url_scheme
|
|
||||||
url_adapter.url_scheme = scheme
|
|
||||||
|
|
||||||
try:
|
|
||||||
try:
|
|
||||||
rv = url_adapter.build(
|
|
||||||
endpoint, values, method=method, force_external=external
|
|
||||||
)
|
|
||||||
finally:
|
|
||||||
if old_scheme is not None:
|
|
||||||
url_adapter.url_scheme = old_scheme
|
|
||||||
except BuildError as error:
|
|
||||||
# We need to inject the values again so that the app callback can
|
|
||||||
# deal with that sort of stuff.
|
|
||||||
values["_external"] = external
|
|
||||||
values["_anchor"] = anchor
|
|
||||||
values["_method"] = method
|
|
||||||
values["_scheme"] = scheme
|
|
||||||
return appctx.app.handle_url_build_error(error, endpoint, values)
|
|
||||||
|
|
||||||
if anchor is not None:
|
|
||||||
rv += f"#{url_quote(anchor)}"
|
|
||||||
return rv
|
|
||||||
|
|
||||||
|
|
||||||
def redirect(
|
def redirect(
|
||||||
|
|
|
||||||
|
|
@ -119,11 +119,15 @@ class TestUrlFor:
|
||||||
)
|
)
|
||||||
|
|
||||||
def test_url_for_with_scheme_not_external(self, app, req_ctx):
|
def test_url_for_with_scheme_not_external(self, app, req_ctx):
|
||||||
@app.route("/")
|
app.add_url_rule("/", endpoint="index")
|
||||||
def index():
|
|
||||||
return "42"
|
|
||||||
|
|
||||||
pytest.raises(ValueError, flask.url_for, "index", _scheme="https")
|
# Implicit external with scheme.
|
||||||
|
url = flask.url_for("index", _scheme="https")
|
||||||
|
assert url == "https://localhost/"
|
||||||
|
|
||||||
|
# Error when external=False with scheme
|
||||||
|
with pytest.raises(ValueError):
|
||||||
|
flask.url_for("index", _scheme="https", _external=False)
|
||||||
|
|
||||||
def test_url_for_with_alternating_schemes(self, app, req_ctx):
|
def test_url_for_with_alternating_schemes(self, app, req_ctx):
|
||||||
@app.route("/")
|
@app.route("/")
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue