diff --git a/kirovy/settings/_base.py b/kirovy/settings/_base.py index a3bba82..b662235 100644 --- a/kirovy/settings/_base.py +++ b/kirovy/settings/_base.py @@ -31,7 +31,7 @@ SECRET_KEY = get_env_var("SECRET_KEY", validation_callback=secret_key_validator) # SECURITY WARNING: don't run with debug turned on in production! -DEBUG = get_env_var("DEBUG", False, validation_callback=not_allowed_on_prod) +DEBUG = get_env_var("DEBUG", default=False, validation_callback=not_allowed_on_prod, value_type=bool) ALLOWED_HOSTS = get_env_var("ALLOWED_HOSTS", "localhost,mapdb-nginx").split(",") diff --git a/kirovy/typing/__init__.py b/kirovy/typing/__init__.py index d3a6228..9d18514 100644 --- a/kirovy/typing/__init__.py +++ b/kirovy/typing/__init__.py @@ -1,6 +1,7 @@ """ Any types specific to our application should live in this package. """ + import uuid # import typing for re-export. This avoids having two different typing imports. @@ -10,7 +11,7 @@ # arg[0]: The key of the env var for the exception. # arg[1]: The value we fetched from the env var # No return, raise an error. -SettingsValidationCallback = Callable[[str, Any], NoReturn] +SettingsValidationCallback = Callable[[str, Any], None] FileExtension = str diff --git a/kirovy/utils/settings_utils.py b/kirovy/utils/settings_utils.py index ed63572..19b6299 100644 --- a/kirovy/utils/settings_utils.py +++ b/kirovy/utils/settings_utils.py @@ -4,19 +4,29 @@ """ import os -from typing import Any, Optional, NoReturn +from collections.abc import Callable + +from distutils.util import strtobool +from typing import Any, Type from kirovy import exceptions from kirovy.typing import SettingsValidationCallback from kirovy.settings import settings_constants MINIMUM_SECRET_KEY_LENGTH = 32 +_NOT_SET = object() + + +def _unvalidated_env_var(_: str, __: Any) -> None: + return def get_env_var( key: str, - default: Optional[Any] = None, - validation_callback: Optional[SettingsValidationCallback] = None, + default: Any | None = _NOT_SET, + validation_callback: SettingsValidationCallback = _unvalidated_env_var, + *, + value_type: Type[Callable[[object], Any]] = str, ) -> Any: """Get an env var and validate it. @@ -25,16 +35,24 @@ def get_env_var( Do not provide defaults for e.g. passwords. - :param str key: + :param key: The env var key to search for. - :param Optional[Any] default: + :param default: The default value. Use to make an env var not raise an error if no env var is found. Never use for secrets. If you use with ``validation_callback`` then make sure your default value will pass your validation check. - :param Optional[SettingsValidationCallback] validation_callback: + :param validation_callback: A function to call on a value to make sure it's valid. Raises an exception if invalid. + :param value_type: + Convert the string from ``os.environ`` to this type. The type must be callable. + No validation is performed on the environment string before attempting to cast, + so you're responsible for handling cast errors. + + .. note:: + + If you provide ``bool`` then we will use ``distutils.util.strtobool``. :return Any: The env var value @@ -45,28 +63,29 @@ def get_env_var( """ - value: Optional[Any] = os.environ.get(key) - - if value is None: - value = default + value: str | None = os.environ.get(key) - if value is None: + if value is None and default is _NOT_SET: raise exceptions.ConfigurationException(key, "Env var is required and cannot be None.") - if validation_callback is not None: - validation_callback(key, value) + if value_type == bool: + value_type = strtobool + + value = value_type(value) if value is not None else default + + validation_callback(key, value) return value -def secret_key_validator(key: str, value: str) -> NoReturn: +def secret_key_validator(key: str, value: str) -> None: """Validate the secret key. :param str key: env var key. :param str value: The value found. - :return NoReturn: + :return: :raises exceptions.ConfigurationException: """ diff --git a/tests/test_settings_utils.py b/tests/test_settings_utils.py index 81d02bb..5dcf0c7 100644 --- a/tests/test_settings_utils.py +++ b/tests/test_settings_utils.py @@ -2,7 +2,7 @@ import pytest -from kirovy import exceptions +from kirovy import exceptions, typing as t from kirovy.utils import settings_utils @@ -59,3 +59,24 @@ def test_run_environment_valid(run_environment: str, expect_error: bool): assert e else: settings_utils.get_env_var("meh", run_environment, settings_utils.run_environment_valid) + + +@pytest.mark.parametrize( + "value,expected,value_type", + [ + ("1", 1, int), + ("1.1", 1.1, float), + ("1", "1", str), + ("1", True, bool), + ("true", True, bool), + ("0", False, bool), + ("false", False, bool), + ], +) +def test_get_env_var_value(mocker, value: str, expected: t.Any, value_type: t.Type[t.Any]): + """Test the strings can be properly cast using the environment loader. + + Necessary because ``environ.get`` always returns ``str | None``. + """ + mocker.patch.dict(os.environ, {"test_get_env_var_value": value}) + assert settings_utils.get_env_var("test_get_env_var_value", value_type=value_type) == expected