From eb5dd9f5ef255c578cbbe13c1cb4dd11389d5519 Mon Sep 17 00:00:00 2001 From: dzcode <9089037+dzcode@users.noreply.github.com> Date: Mon, 2 May 2022 10:16:12 -0600 Subject: [PATCH] add aborter object to app --- CHANGES.rst | 4 ++++ src/flask/__init__.py | 2 +- src/flask/app.py | 30 ++++++++++++++++++++++++++++++ src/flask/helpers.py | 27 +++++++++++++++++++++++++++ tests/test_helpers.py | 30 ++++++++++++++++++++++++++++++ 5 files changed, 92 insertions(+), 1 deletion(-) diff --git a/CHANGES.rst b/CHANGES.rst index 71100355..153e964b 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -5,6 +5,10 @@ Version 2.2.0 Unreleased +- Add ``aborter_class`` and ``aborter`` attributes to the Flask app + object. ``flask.abort`` will call ``app.aborter``. This makes it + possible for an app to override how aborts work, including custom + status codes. :issue:`4567` - Add an ``app.redirect`` method, which ``flask.redirect`` will call. This makes it possible for an app to override how redirects work. :issue:`4569` diff --git a/src/flask/__init__.py b/src/flask/__init__.py index f71a7d42..bc93e0a3 100644 --- a/src/flask/__init__.py +++ b/src/flask/__init__.py @@ -1,6 +1,5 @@ from markupsafe import escape from markupsafe import Markup -from werkzeug.exceptions import abort as abort from . import json as json from .app import Flask as Flask @@ -18,6 +17,7 @@ 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 abort as abort 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 diff --git a/src/flask/app.py b/src/flask/app.py index 8ce14301..219cde24 100644 --- a/src/flask/app.py +++ b/src/flask/app.py @@ -12,6 +12,7 @@ from types import TracebackType from werkzeug.datastructures import Headers from werkzeug.datastructures import ImmutableDict +from werkzeug.exceptions import Aborter from werkzeug.exceptions import BadRequest from werkzeug.exceptions import BadRequestKeyError from werkzeug.exceptions import HTTPException @@ -201,6 +202,16 @@ class Flask(Scaffold): #: :class:`~flask.Response` for more information. response_class = Response + #: The class of the object assigned to :attr:`aborter`, created by + #: :meth:`create_aborter`. That object is called by + #: :func:`flask.abort` to raise HTTP errors, and can be + #: called directly as well. + #: + #: Defaults to :class:`werkzeug.exceptions.Aborter`. + #: + #: .. versionadded:: 2.2 + aborter_class = Aborter + #: The class that is used for the Jinja environment. #: #: .. versionadded:: 0.11 @@ -421,6 +432,13 @@ class Flask(Scaffold): #: to load a config from files. self.config = self.make_config(instance_relative_config) + #: An instance of :attr:`aborter_class` created by + #: :meth:`make_aborter`. This is called by :func:`flask.abort` + #: to raise HTTP errors, and can be called directly as well. + #: + #: .. 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 @@ -628,6 +646,18 @@ class Flask(Scaffold): defaults["DEBUG"] = get_debug_flag() return self.config_class(root_path, defaults) + def make_aborter(self) -> Aborter: + """Create the object to assign to :attr:`aborter`. That object + is called by :func:`flask.abort` to raise HTTP errors, and can + be called directly as well. + + By default, this creates an instance of :attr:`aborter_class`, + which defaults to :class:`werkzeug.exceptions.Aborter`. + + .. versionadded:: 2.2 + """ + return self.aborter_class() + def auto_find_instance_path(self) -> str: """Tries to locate the instance path if it was not provided to the constructor of the application class. It will basically calculate diff --git a/src/flask/helpers.py b/src/flask/helpers.py index e3883ed6..070a4daf 100644 --- a/src/flask/helpers.py +++ b/src/flask/helpers.py @@ -10,6 +10,7 @@ from functools import update_wrapper 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 @@ -24,6 +25,7 @@ from .signals import message_flashed if t.TYPE_CHECKING: # pragma: no cover from werkzeug.wrappers import Response as BaseResponse from .wrappers import Response + import typing_extensions as te def get_env() -> str: @@ -364,6 +366,31 @@ def redirect( return _wz_redirect(location, code=code, Response=Response) +def abort( # type: ignore[misc] + code: t.Union[int, "BaseResponse"], *args: t.Any, **kwargs: t.Any +) -> "te.NoReturn": + """Raise an :exc:`~werkzeug.exceptions.HTTPException` for the given + status code. + + If :data:`~flask.current_app` is available, it will call its + :attr:`~flask.Flask.aborter` object, otherwise it will use + :func:`werkzeug.exceptions.abort`. + + :param code: The status code for the exception, which must be + registered in ``app.aborter``. + :param args: Passed to the exception. + :param kwargs: Passed to the exception. + + .. versionadded:: 2.2 + Calls ``current_app.aborter`` if available instead of always + using Werkzeug's default ``abort``. + """ + if current_app: + current_app.aborter(code, *args, **kwargs) + + _wz_abort(code, *args, **kwargs) + + def get_template_attribute(template_name: str, attribute: str) -> t.Any: """Loads a macro (or variable) a template exports. This can be used to invoke a macro from within Python code. If you for example have a diff --git a/tests/test_helpers.py b/tests/test_helpers.py index 63dbbc13..0893893f 100644 --- a/tests/test_helpers.py +++ b/tests/test_helpers.py @@ -2,6 +2,7 @@ import io import os import pytest +import werkzeug.exceptions import flask from flask.helpers import get_debug_flag @@ -174,6 +175,35 @@ def test_redirect_with_app(app): flask.redirect("other") +def test_abort_no_app(): + with pytest.raises(werkzeug.exceptions.Unauthorized): + flask.abort(401) + + with pytest.raises(LookupError): + flask.abort(900) + + +def test_app_aborter_class(): + class MyAborter(werkzeug.exceptions.Aborter): + pass + + class MyFlask(flask.Flask): + aborter_class = MyAborter + + app = MyFlask(__name__) + assert isinstance(app.aborter, MyAborter) + + +def test_abort_with_app(app): + class My900Error(werkzeug.exceptions.HTTPException): + code = 900 + + app.aborter.mapping[900] = My900Error + + with app.app_context(), pytest.raises(My900Error): + flask.abort(900) + + class TestNoImports: """Test Flasks are created without import.