From 4ad9cb3c3c44384ed56d56c994d1fc5fa821e673 Mon Sep 17 00:00:00 2001 From: Peter Schutt Date: Wed, 18 Jan 2023 21:10:10 +1000 Subject: [PATCH] refactor: default_factory and validator for dependency-based gates Default values for `PluginConfig.do_cache` et al. are based on the `IS_XXX_INSTALLED` constants. We use `default_factory` so we can patch the module values in tests. Use `@validator` to test for missing dependency when gates are explicitly set. --- src/starlite_saqlalchemy/exceptions.py | 2 +- src/starlite_saqlalchemy/init_plugin.py | 54 +++++++++++++++--------- tests/unit/test_init_plugin_no_extras.py | 25 ++++------- tests/utils/app.py | 11 +---- 4 files changed, 47 insertions(+), 45 deletions(-) diff --git a/src/starlite_saqlalchemy/exceptions.py b/src/starlite_saqlalchemy/exceptions.py index 84ea1181..8f25be7a 100644 --- a/src/starlite_saqlalchemy/exceptions.py +++ b/src/starlite_saqlalchemy/exceptions.py @@ -56,7 +56,7 @@ class AuthorizationError(StarliteSaqlalchemyClientError): """A user tried to do something they shouldn't have.""" -class MissingDependencyError(StarliteSaqlalchemyError): +class MissingDependencyError(StarliteSaqlalchemyError, ValueError): """A required dependency is not installed.""" def __init__(self, module: str, config: str | None = None) -> None: diff --git a/src/starlite_saqlalchemy/init_plugin.py b/src/starlite_saqlalchemy/init_plugin.py index 7126e155..4c28efce 100644 --- a/src/starlite_saqlalchemy/init_plugin.py +++ b/src/starlite_saqlalchemy/init_plugin.py @@ -33,7 +33,7 @@ def example_handler() -> dict: from collections.abc import Callable, Sequence # noqa: TC003 from typing import TYPE_CHECKING, Any, TypeVar -from pydantic import BaseModel +from pydantic import BaseModel, Field, validator from starlite.app import DEFAULT_CACHE_CONFIG, DEFAULT_OPENAPI_CONFIG from starlite.types import TypeEncodersMap # noqa: TC002 from structlog.types import Processor # noqa: TC002 @@ -92,7 +92,7 @@ class PluginConfig(BaseModel): Add the hook handler to [`AppConfig.after_exception`][starlite.config.app.AppConfig.after_exception]. """ - do_cache: bool = False + do_cache: bool = Field(default_factory=lambda: IS_REDIS_INSTALLED) """Configure redis cache backend. Add configuration for the redis-backed cache to @@ -134,7 +134,10 @@ class PluginConfig(BaseModel): Set the OpenAPI config object to [`AppConfig.openapi_config`][starlite.config.app.AppConfig.openapi_config]. """ - do_sentry: bool | None = None + do_sentry: bool = Field( + default_factory=lambda: IS_SENTRY_SDK_INSTALLED + and not (IS_LOCAL_ENVIRONMENT or IS_TEST_ENVIRONMENT) + ) """Configure sentry. Configure the application to initialize Sentry on startup. Adds a handler to @@ -146,7 +149,7 @@ class PluginConfig(BaseModel): Allow the plugin to set the starlite `debug` parameter. Parameter set to value of [`AppConfig.debug`][starlite_saqlalchemy.settings.AppSettings.DEBUG]. """ - do_sqlalchemy_plugin: bool = False + do_sqlalchemy_plugin: bool = Field(default_factory=lambda: IS_SQLALCHEMY_INSTALLED) """Configure SQLAlchemy plugin. Set the SQLAlchemy plugin on the application. Adds the plugin to @@ -155,7 +158,7 @@ class PluginConfig(BaseModel): do_type_encoders: bool = True """Configure custom type encoders on the app.""" - do_worker: bool = False + do_worker: bool = Field(default_factory=lambda: IS_SAQ_INSTALLED) """Configure the async worker on the application. This action instantiates a worker instance and sets handlers for @@ -170,6 +173,31 @@ class PluginConfig(BaseModel): type_encoders: TypeEncodersMap = type_encoders_map """Map of type to serializer callable.""" health_checks: list[type[AbstractHealthCheck]] = [AppHealthCheck] + """Checks executed on calls to health route handler.""" + + @validator("do_cache") + def _validate_do_cache(cls, value: bool) -> bool: + if value is True and not IS_REDIS_INSTALLED: + raise MissingDependencyError(module="redis", config="redis") + return value + + @validator("do_sentry") + def _validate_do_sentry(cls, value: bool) -> bool: + if value is True and not IS_SENTRY_SDK_INSTALLED: + raise MissingDependencyError(module="sentry_sdk", config="sentry") + return value + + @validator("do_sqlalchemy_plugin") + def _validate_do_sqlalchemy_plugin(cls, value: bool) -> bool: + if value is True and not IS_SQLALCHEMY_INSTALLED: + raise MissingDependencyError(module="sqlalchemy", config="sqlalchemy_plugin") + return value + + @validator("do_worker") + def _validate_do_worker(cls, value: bool) -> bool: + if value is True and not IS_SAQ_INSTALLED: + raise MissingDependencyError(module="saq", config="worker") + return value class ConfigureApp: @@ -207,6 +235,7 @@ def __call__(self, app_config: AppConfig) -> AppConfig: self.configure_sqlalchemy_plugin(app_config) self.configure_type_encoders(app_config) self.configure_worker(app_config) + # health check is explicitly configured last self.configure_health_check(app_config) app_config.before_startup = lifespan.before_startup_handler @@ -237,8 +266,6 @@ def configure_cache(self, app_config: AppConfig) -> None: app_config: The Starlite application config object. """ if self.config.do_cache and app_config.cache_config == DEFAULT_CACHE_CONFIG: - if not IS_REDIS_INSTALLED: - raise MissingDependencyError(module="redis", config="redis") from starlite_saqlalchemy import cache app_config.cache_config = cache.config @@ -340,14 +367,7 @@ def configure_sentry(self, app_config: AppConfig) -> None: Args: app_config: The Starlite application config object. """ - do_sentry = ( - self.config.do_sentry - if self.config.do_sentry is not None - else not (IS_LOCAL_ENVIRONMENT or IS_TEST_ENVIRONMENT) - ) - if do_sentry: - if not IS_SENTRY_SDK_INSTALLED: - raise MissingDependencyError(module="sentry_sdk", config="sentry") + if self.config.do_sentry: from starlite_saqlalchemy import sentry app_config.on_startup.append(sentry.configure) @@ -362,8 +382,6 @@ def configure_sqlalchemy_plugin(self, app_config: AppConfig) -> None: app_config: The Starlite application config object. """ if self.config.do_sqlalchemy_plugin: - if not IS_SQLALCHEMY_INSTALLED: - raise MissingDependencyError(module="sqlalchemy") from starlite.plugins.sql_alchemy import SQLAlchemyPlugin from starlite_saqlalchemy.sqlalchemy_plugin import ( @@ -393,8 +411,6 @@ def configure_worker(self, app_config: AppConfig) -> None: app_config: The Starlite application config object. """ if self.config.do_worker: - if not IS_SAQ_INSTALLED: - raise MissingDependencyError(module="saq", config="worker") from starlite_saqlalchemy.worker import ( create_worker_instance, make_service_callback, diff --git a/tests/unit/test_init_plugin_no_extras.py b/tests/unit/test_init_plugin_no_extras.py index cb1ff7be..5c682c54 100644 --- a/tests/unit/test_init_plugin_no_extras.py +++ b/tests/unit/test_init_plugin_no_extras.py @@ -1,10 +1,9 @@ """Tests for init_plugin.py when no extra dependencies are installed.""" import pytest -from starlite import Starlite +from pydantic import ValidationError from starlite_saqlalchemy import constants, init_plugin -from starlite_saqlalchemy.exceptions import MissingDependencyError SKIP = any( [ @@ -18,15 +17,15 @@ @pytest.mark.skipif(SKIP, reason="test will only run if no extras are installed") @pytest.mark.parametrize( - ("enabled_config", "error"), + ("enabled_config", "error_pattern"), [ - ("do_cache", r"^.*\'redis\' is not installed.*$"), - ("do_sentry", r"^.*\'sentry_sdk\' is not installed.*$"), - ("do_worker", r"^.*\'saq\' is not installed.*$"), - ("do_sqlalchemy_plugin", r"^.*\'sqlalchemy\' is not installed.*$"), + ("do_cache", r"\'redis\' is not installed."), + ("do_sentry", r"\'sentry_sdk\' is not installed."), + ("do_worker", r"\'saq\' is not installed."), + ("do_sqlalchemy_plugin", r"\'sqlalchemy\' is not installed."), ], ) -def test_extra_dependencies_not_installed(enabled_config: str, error: str) -> None: +def test_extra_dependencies_not_installed(enabled_config: str, error_pattern: str) -> None: """Tests that the plugin test required dependencies for switches needing them.""" kwargs = { @@ -45,11 +44,5 @@ def test_extra_dependencies_not_installed(enabled_config: str, error: str) -> No "do_worker": False, **{enabled_config: True}, } - config = init_plugin.PluginConfig(**kwargs) - - with pytest.raises(MissingDependencyError, match=error): - Starlite( - route_handlers=[], - openapi_config=None, - on_app_init=[init_plugin.ConfigureApp(config=config)], - ) + with pytest.raises(ValidationError, match=error_pattern): + init_plugin.PluginConfig(**kwargs) diff --git a/tests/utils/app.py b/tests/utils/app.py index 3c050849..81750f7b 100644 --- a/tests/utils/app.py +++ b/tests/utils/app.py @@ -3,18 +3,11 @@ from starlite import Starlite -from starlite_saqlalchemy import ConfigureApp, PluginConfig +from starlite_saqlalchemy import ConfigureApp from . import controllers def create_app() -> Starlite: """App for our test domain.""" - return Starlite( - route_handlers=[controllers.create_router()], - on_app_init=[ - ConfigureApp( - config=PluginConfig(do_sqlalchemy_plugin=True, do_worker=True, do_cache=True) - ) - ], - ) + return Starlite(route_handlers=[controllers.create_router()], on_app_init=[ConfigureApp()])