From 92acd05d9bd33419ab885e4472b28a1d5cc15d2e Mon Sep 17 00:00:00 2001 From: Ivan Sushkov Date: Mon, 2 May 2022 12:29:55 -0600 Subject: [PATCH 1/2] add url_for method to app --- src/flask/app.py | 45 ++++++++++++++++++++++++++++++++++++++++++++ src/flask/helpers.py | 38 +------------------------------------ 2 files changed, 46 insertions(+), 37 deletions(-) diff --git a/src/flask/app.py b/src/flask/app.py index ad859b27..de883a92 100644 --- a/src/flask/app.py +++ b/src/flask/app.py @@ -23,6 +23,7 @@ from werkzeug.routing import MapAdapter from werkzeug.routing import RequestRedirect from werkzeug.routing import RoutingException from werkzeug.routing import Rule +from werkzeug.urls import url_quote from werkzeug.utils import redirect as _wz_redirect from werkzeug.wrappers import Response as BaseResponse @@ -1661,6 +1662,50 @@ class Flask(Scaffold): return asgiref_async_to_sync(func) + def url_for( + self, + endpoint: str, + external: bool, + url_adapter, + **values: t.Any, + ) -> str: + + anchor = values.pop("_anchor", None) + method = values.pop("_method", None) + scheme = values.pop("_scheme", None) + self.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 self.handle_url_build_error(error, endpoint, values) + + if anchor is not None: + rv += f"#{url_quote(anchor)}" + return rv + def redirect(self, location: str, code: int = 302) -> BaseResponse: """Create a redirect response object. diff --git a/src/flask/helpers.py b/src/flask/helpers.py index dec9b771..167fa132 100644 --- a/src/flask/helpers.py +++ b/src/flask/helpers.py @@ -11,8 +11,6 @@ from threading import RLock import werkzeug.utils 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 .globals import _app_ctx_stack @@ -307,41 +305,7 @@ def url_for(endpoint: str, **values: t.Any) -> str: 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 + return current_app.url_for(endpoint, external, url_adapter, **values) def redirect( From 39f93632964ecabfcb9c1980e2add922098aad80 Mon Sep 17 00:00:00 2001 From: David Lord Date: Sat, 14 May 2022 12:43:38 -0700 Subject: [PATCH 2/2] finish moving url_for to app move entire implementation to app make special build args actual keyword-only args handle no app context in method mention other config in server_name error implicit external with scheme use adapter.build url_scheme argument rewrite documentation --- src/flask/app.py | 171 ++++++++++++++++++++++++++++++++---------- src/flask/helpers.py | 154 +++++++++++-------------------------- tests/test_helpers.py | 12 ++- 3 files changed, 185 insertions(+), 152 deletions(-) diff --git a/src/flask/app.py b/src/flask/app.py index de883a92..4da84e30 100644 --- a/src/flask/app.py +++ b/src/flask/app.py @@ -34,6 +34,7 @@ from .config import ConfigAttribute from .ctx import _AppCtxGlobals from .ctx import AppContext from .ctx import RequestContext +from .globals import _app_ctx_stack from .globals import _request_ctx_stack from .globals import g from .globals import request @@ -440,15 +441,16 @@ class Flask(Scaffold): #: .. versionadded:: 2.2 self.aborter = self.make_aborter() - #: A list of functions that are called when :meth:`url_for` raises a - #: :exc:`~werkzeug.routing.BuildError`. Each function registered here - #: is called with `error`, `endpoint` and `values`. If a function - #: returns ``None`` or raises a :exc:`BuildError` the next function is - #: tried. + #: A list of functions that are called by + #: :meth:`handle_url_build_error` when :meth:`.url_for` raises a + #: :exc:`~werkzeug.routing.BuildError`. Each function 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``. #: #: .. versionadded:: 0.9 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 @@ -1665,45 +1667,125 @@ class Flask(Scaffold): def url_for( self, endpoint: str, - external: bool, - url_adapter, + *, + _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() `, 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.") - anchor = values.pop("_anchor", None) - method = values.pop("_method", None) - scheme = values.pop("_scheme", None) self.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 + rv = url_adapter.build( + endpoint, + values, + method=_method, + url_scheme=_scheme, + force_external=_external, + ) 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 + 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"#{url_quote(anchor)}" + if _anchor is not None: + rv = f"{rv}#{url_quote(_anchor)}" + return rv def redirect(self, location: str, code: int = 302) -> BaseResponse: @@ -1905,10 +1987,21 @@ class Flask(Scaffold): func(endpoint, values) def handle_url_build_error( - self, error: Exception, endpoint: str, values: dict + self, error: BuildError, endpoint: str, values: t.Dict[str, t.Any] ) -> str: - """Handle :class:`~werkzeug.routing.BuildError` on - :meth:`url_for`. + """Called by :meth:`.url_for` if a + :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: try: diff --git a/src/flask/helpers.py b/src/flask/helpers.py index 167fa132..6928b203 100644 --- a/src/flask/helpers.py +++ b/src/flask/helpers.py @@ -13,7 +13,6 @@ import werkzeug.utils from werkzeug.exceptions import abort as _wz_abort from werkzeug.utils import redirect as _wz_redirect -from .globals import _app_ctx_stack from .globals import _request_ctx_stack from .globals import current_app from .globals import request @@ -191,121 +190,58 @@ def make_response(*args: t.Any) -> "Response": return current_app.make_response(args) # type: ignore -def url_for(endpoint: str, **values: t.Any) -> str: - """Generates a URL to the given endpoint with the method provided. +def url_for( + 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 - to the generated URL as query arguments. If the value of a query argument - is ``None``, the whole pair is skipped. In case blueprints are active - you can shortcut references to the same blueprint by prefixing the - local endpoint with a dot (``.``). + This requires an active request or application context, and calls + :meth:`current_app.url_for() `. See that method + for full documentation. - 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 - generating URLs outside of a request context. + .. versionchanged:: 0.9 + The ``_anchor`` and ``_method`` parameters were added. - To integrate applications, :class:`Flask` has a hook to intercept URL build - errors through :attr:`Flask.url_build_error_handlers`. The `url_for` - 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. + .. versionchanged:: 0.9 + Calls ``app.handle_url_build_error`` on build errors. """ - appctx = _app_ctx_stack.top - reqctx = _request_ctx_stack.top - - if appctx is None: - raise RuntimeError( - "Attempted to generate a URL without the application context being" - " pushed. This has to be executed when application context is" - " 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) - - return current_app.url_for(endpoint, external, url_adapter, **values) + return current_app.url_for( + endpoint, + _anchor=_anchor, + _method=_method, + _scheme=_scheme, + _external=_external, + **values, + ) def redirect( diff --git a/tests/test_helpers.py b/tests/test_helpers.py index 0893893f..cc2daaf7 100644 --- a/tests/test_helpers.py +++ b/tests/test_helpers.py @@ -119,11 +119,15 @@ class TestUrlFor: ) def test_url_for_with_scheme_not_external(self, app, req_ctx): - @app.route("/") - def index(): - return "42" + app.add_url_rule("/", endpoint="index") - 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): @app.route("/")