diff --git a/configurations/base.py b/configurations/base.py index 14185fe..d71cf89 100644 --- a/configurations/base.py +++ b/configurations/base.py @@ -4,6 +4,7 @@ import re from django.conf import global_settings from django.core.exceptions import ImproperlyConfigured +from .errors import ConfigurationError, SetupError from .utils import uppercase_attributes from .values import Value, setup_value @@ -142,6 +143,13 @@ class Configuration(metaclass=ConfigurationBase): @classmethod def setup(cls): + exceptions = [] for name, value in uppercase_attributes(cls).items(): if isinstance(value, Value): - setup_value(cls, name, value) + try: + setup_value(cls, name, value) + except ConfigurationError as err: + exceptions.append(err) + + if len(exceptions) > 0: + raise SetupError(f"Couldn't setup values of configuration {cls.__name__}", exceptions) diff --git a/configurations/errors.py b/configurations/errors.py index da8bbe8..102badf 100644 --- a/configurations/errors.py +++ b/configurations/errors.py @@ -13,6 +13,20 @@ class TermStyles: END = "\033[0m" if os.isatty(sys.stderr.fileno()) else "" +class SetupError(Exception): + """ + Exception that gets raised when a configuration class cannot be set up by the importer + """ + def __init__(self, msg: str, child_errors: List['ConfigurationError'] = None) -> None: + """ + :param step_verb: Which step the importer tried to perform (e.g. import, setup) + :param configuration_path: The full module path of the configuration that was supposed to be set up + :param child_errors: Optional child configuration errors that caused this error + """ + super().__init__(msg) + self.child_errors = child_errors or [] + + class ConfigurationError(ValueError): """ Base error class that is used to indicate that something went wrong during configuration. @@ -95,10 +109,13 @@ def with_error_handler(callee: Callable) -> Callable: def wrapper(*args, **kwargs): try: return callee(*args, **kwargs) - except ConfigurationError as e: - msg = "{}{}{}".format(TermStyles.RED + TermStyles.BOLD, e, TermStyles.END) - for line in e.explanation_lines: - msg += f"\n {line}" + except SetupError as e: + msg = f"{str(e)}" + for child_error in e.child_errors: + msg += f"\n * {child_error.main_error_msg}" + for explanation_line in child_error.explanation_lines: + msg += f"\n - {explanation_line}" + print(msg, file=sys.stderr) return wrapper diff --git a/configurations/importer.py b/configurations/importer.py index e3573f4..df5f6b9 100644 --- a/configurations/importer.py +++ b/configurations/importer.py @@ -8,6 +8,7 @@ from django.conf import ENVIRONMENT_VARIABLE as SETTINGS_ENVIRONMENT_VARIABLE from django.core.exceptions import ImproperlyConfigured from django.core.management import base +from .errors import SetupError, ConfigurationError from .utils import uppercase_attributes, reraise from .values import Value, setup_value @@ -149,10 +150,10 @@ class ConfigurationLoader: try: cls = getattr(mod, self.name) - except AttributeError as err: # pragma: no cover - reraise(err, "Couldn't find configuration '{0}' " - "in module '{1}'".format(self.name, - mod.__package__)) + except AttributeError: # pragma: no cover + raise SetupError(f"Couldn't find configuration '{self.name}' in module {mod.__package__}.\n" + f"Hint: '{self.name}' is taken from the environment variable '{CONFIGURATION_ENVIRONMENT_VARIABLE}'" + f"and '{mod.__package__}' from the environment variable '{SETTINGS_ENVIRONMENT_VARIABLE}'.") try: cls.pre_setup() cls.setup() @@ -172,6 +173,10 @@ class ConfigurationLoader: self.name)) cls.post_setup() + except SetupError: + raise + except ConfigurationError as err: + raise SetupError(f"Couldn't setup configuration '{cls_path}'", [err]) except Exception as err: reraise(err, "Couldn't setup configuration '{0}'".format(cls_path))