diff --git a/docs/requirements.txt b/docs/requirements.txt index 3a5c45a..19a73e6 100644 --- a/docs/requirements.txt +++ b/docs/requirements.txt @@ -1,4 +1,4 @@ flask>=2.2.0 +Pallets-Sphinx-Themes sphinx==7.1.2 sphinx-notfound-page -sphinx-rtd-theme==1.3.0rc1 diff --git a/docs/source/api.rst b/docs/source/api.rst index 19dc004..f520aaa 100644 --- a/docs/source/api.rst +++ b/docs/source/api.rst @@ -6,6 +6,12 @@ API This part of the documentation covers all the interfaces of Flask-Utils +Extension +--------- + +.. automodule:: flask_utils.extension + :members: + Custom exceptions ----------------- @@ -42,3 +48,5 @@ Private API .. autofunction:: flask_utils.decorators._check_type .. autofunction:: flask_utils.errors._error_template._generate_error_json + +.. autofunction:: flask_utils.errors._register_error_handlers diff --git a/docs/source/conf.py b/docs/source/conf.py index ac6e889..7f8666f 100644 --- a/docs/source/conf.py +++ b/docs/source/conf.py @@ -25,6 +25,7 @@ "sphinx.ext.intersphinx", "sphinx.ext.viewcode", "notfound.extension", + "pallets_sphinx_themes", ] autodoc_default_options = { @@ -43,7 +44,7 @@ # -- Options for HTML output -html_theme = "sphinx_rtd_theme" +html_theme = "flask" # -- Options for EPUB output epub_show_urls = "footnote" diff --git a/flask_utils/__init__.py b/flask_utils/__init__.py index 146405d..bca407c 100644 --- a/flask_utils/__init__.py +++ b/flask_utils/__init__.py @@ -1,5 +1,5 @@ # Increment versions here according to SemVer -__version__ = "0.4.0" +__version__ = "0.5.0" from flask_utils.errors import ConflictError from flask_utils.errors import ForbiddenError @@ -14,10 +14,9 @@ from flask_utils.errors import ServiceUnavailableError from flask_utils.decorators import validate_params - from flask_utils.utils import is_it_true -from flask_utils.errors import register_error_handlers +from flask_utils.extension import FlaskUtils __all__ = [ "ConflictError", @@ -25,7 +24,6 @@ "UnauthorizedError", "NotFoundError", "BadRequestError", - "register_error_handlers", "FailedDependencyError", "OriginIsUnreachableError", "WebServerIsDownError", @@ -34,4 +32,5 @@ "ServiceUnavailableError", "validate_params", "is_it_true", + "FlaskUtils", ] diff --git a/flask_utils/decorators.py b/flask_utils/decorators.py index 6f328f6..68960e5 100644 --- a/flask_utils/decorators.py +++ b/flask_utils/decorators.py @@ -13,8 +13,8 @@ from flask_utils.errors import BadRequestError -# TODO: Turn flask-utils into a class that registers the app (like Flask-Cors for example) -# and the error handlers optionally, that way we can either use BadRequestError or just return a 400 +# TODO: Change validate_params to either use BadRequestError or just return a 400 depending +# on if the error handler is registered or not in the FlaskUtils class VALIDATE_PARAMS_MAX_DEPTH = 4 diff --git a/flask_utils/errors/__init__.py b/flask_utils/errors/__init__.py index 733356b..13c55e9 100644 --- a/flask_utils/errors/__init__.py +++ b/flask_utils/errors/__init__.py @@ -15,12 +15,32 @@ from flask_utils.errors.web_server_is_down import WebServerIsDownError -def register_error_handlers(application: Flask) -> None: +def _register_error_handlers(application: Flask) -> None: """ This function will register all the error handlers for the application :param application: The Flask application to register the error handlers :return: None + + .. versionchanged:: 0.5.0 + Made the function private. If you want to register the custom error handlers, you need to + pass `register_error_handlers=True` to the :class:`flask_utils.extension.FlaskUtils` class + or to :meth:`flask_utils.extension.FlaskUtils.init_app` + + .. code-block:: python + + from flask import Flask + from flask_utils import FlaskUtils + + app = Flask(__name__) + utils = FlaskUtils(app) + + # OR + + utils = FlaskUtils() + utils.init_app(app) + + .. versionadded:: 0.1.0 """ @application.errorhandler(BadRequestError) @@ -168,5 +188,5 @@ def generate_service_unavailable(error: ServiceUnavailableError) -> Response: "GoneError", "UnprocessableEntityError", "ServiceUnavailableError", - "register_error_handlers", + "_register_error_handlers", ] diff --git a/flask_utils/extension.py b/flask_utils/extension.py new file mode 100644 index 0000000..b93ea49 --- /dev/null +++ b/flask_utils/extension.py @@ -0,0 +1,69 @@ +from typing import Optional + +from flask import Flask + +from flask_utils.errors import _register_error_handlers + + +class FlaskUtils(object): + """ + FlaskUtils extension class. + + This class currently optionally register the custom error handlers found in :mod:`flask_utils.errors`. + Call :meth:`init_app` to configure the extension on an application. + + :param app: Flask application instance. + :param register_error_handlers: Register the custom error handlers. Default is True. + + :Example: + + .. code-block:: python + + from flask import Flask + from flask_utils import FlaskUtils + + app = Flask(__name__) + fu = FlaskUtils(app) + + # or + + fu = FlaskUtils() + fu.init_app(app) + + .. versionadded:: 0.5.0 + """ + + def __init__(self, app: Optional[Flask] = None, register_error_handlers: bool = True): + if app is not None: + self.init_app(app, register_error_handlers) + + def init_app(self, app: Flask, register_error_handlers: bool = True): + """Initialize a Flask application for use with this extension instance. This + must be called before any request is handled by the application. + + If the app is created with the factory pattern, this should be called after the app + is created to configure the extension. + + If `register_error_handlers` is True, the custom error handlers will be registered and + can then be used in routes to raise errors. + + :param app: The Flask application to initialize. + :param register_error_handlers: Register the custom error handlers. Default is True. + + :Example: + + .. code-block:: python + + from flask import Flask + from flask_utils import FlaskUtils + + app = Flask(__name__) + fu = FlaskUtils() + fu.init_app(app) + + .. versionadded:: 0.5.0 + """ + if register_error_handlers: + _register_error_handlers(app) + + app.extensions["flask_utils"] = self diff --git a/tests/conftest.py b/tests/conftest.py index 30dc940..b1e9def 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -1,13 +1,13 @@ import pytest from flask import Flask -from flask_utils import register_error_handlers # Adjust import according to your package structure +from flask_utils import FlaskUtils # Adjust import according to your package structure @pytest.fixture def flask_client(): app = Flask(__name__) - register_error_handlers(app) + FlaskUtils(app) return app diff --git a/tests/test_error_handlers.py b/tests/test_errors.py similarity index 100% rename from tests/test_error_handlers.py rename to tests/test_errors.py diff --git a/tests/test_extension.py b/tests/test_extension.py new file mode 100644 index 0000000..68483c2 --- /dev/null +++ b/tests/test_extension.py @@ -0,0 +1,49 @@ +from flask import Flask + +from flask_utils import BadRequestError +from flask_utils import FlaskUtils + + +class TestExtension: + def test_init_app(self): + app = Flask(__name__) + assert "flask_utils" not in app.extensions + fu = FlaskUtils() + + fu.init_app(app) + assert "flask_utils" in app.extensions + + def test_normal_instantiation(self): + app = Flask(__name__) + + assert "flask_utils" not in app.extensions + + FlaskUtils(app) + + assert "flask_utils" in app.extensions + + def test_error_handlers_not_registered(self): + app = Flask(__name__) + + FlaskUtils(app, register_error_handlers=False) + + @app.route("/") + def index(): + raise BadRequestError("Bad Request") + + with app.test_client() as client: + response = client.get("/") + assert response.status_code == 500 + + def test_error_handlers_registered(self): + app = Flask(__name__) + + FlaskUtils(app, register_error_handlers=True) + + @app.route("/") + def index(): + raise BadRequestError("Bad Request") + + with app.test_client() as client: + response = client.get("/") + assert response.status_code == 400