django-configurations/configurations/errors.py
Finn-Thorben Sell 5cd3a86106
implement example generators
Example generators are an addition to the 'Value' classes that allow the error handler to display
example values for the failed value.
For example if no django secret key is found to be defined in the environment, an example (secure)
value for such a secret key can now be generated on the fly and be suggested to the user
2022-03-24 14:25:41 +01:00

132 lines
5.4 KiB
Python

from typing import TYPE_CHECKING, List, Callable
from functools import wraps
import sys
import os
if TYPE_CHECKING:
from .values import Value
class TermStyles:
BOLD = "\033[1m" if os.isatty(sys.stderr.fileno()) else ""
RED = "\033[91m" if os.isatty(sys.stderr.fileno()) else ""
END = "\033[0m" if os.isatty(sys.stderr.fileno()) else ""
def extract_explanation_lines_from_value(value_instance: 'Value') -> List[str]:
result = []
if value_instance.help_text is not None:
result.append(f"Help: {value_instance.help_text}")
if value_instance.help_reference is not None:
result.append(f"Reference: {value_instance.help_reference}")
if value_instance.destination_name is not None:
result.append(f"{value_instance.destination_name} is taken from the environment variable "
f"{value_instance.full_environ_name} as a {type(value_instance).__name__}")
if value_instance.example_generator is not None:
result.append(f"Example value: '{value_instance.example_generator()}'")
return result
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.
This error type (and subclasses) is caught and pretty-printed by django-configurations so that an end-user does not
see an unwieldy traceback but instead a helpful error message.
"""
def __init__(self, main_error_msg: str, explanation_lines: List[str]) -> None:
"""
:param main_error_msg: Main message that describes the error.
This will be displayed before all *explanation_lines* and in the traceback (although tracebacks are normally
not rendered)
:param explanation_lines: Additional lines of explanations which further describe the error or give hints on
how to fix it.
"""
super().__init__(main_error_msg)
self.main_error_msg = main_error_msg
self.explanation_lines = explanation_lines
class ValueRetrievalError(ConfigurationError):
"""
Exception that is raised when errors occur during the retrieval of a Value by one of the `Value` classes.
This can happen when the environment variable corresponding to the value is not defined.
"""
def __init__(self, value_instance: "Value", *extra_explanation_lines: str):
"""
:param value_instance: The `Value` instance which caused the generation of this error
:param extra_explanation_lines: Extra lines that will be appended to `ConfigurationError.explanation_lines`
in addition the ones automatically generated from the provided *value_instance*.
"""
super().__init__(
f"Value of {value_instance.destination_name} could not be retrieved from environment",
list(extra_explanation_lines) + extract_explanation_lines_from_value(value_instance)
)
class ValueProcessingError(ConfigurationError):
"""
Exception that is raised when a dynamic Value failed to be processed by one of the `Value` classes after retrieval.
Processing could be i.e. converting from string to a native datatype or validation.
"""
def __init__(self, value_instance: "Value", raw_value: str, *extra_explanation_lines: str):
"""
:param value_instance: The `Value` instance which caused the generation of this error
:param raw_value: The raw value that was retrieved from the environment and which could not be processed further
:param extra_explanation_lines: Extra lines that will be prepended to `ConfigurationError.explanation_lines`
in addition the ones automatically generated from the provided *value_instance*.
"""
error = f"{value_instance.destination_name} was given an invalid value"
if hasattr(value_instance, "message"):
error += ": " + value_instance.message.format(raw_value)
explanation_lines = list(extra_explanation_lines) + extract_explanation_lines_from_value(value_instance)
explanation_lines.append(f"'{raw_value}' was received but that is invalid")
super().__init__(error, explanation_lines)
def with_error_handler(callee: Callable) -> Callable:
"""
A decorator which is designed to wrap django entry points with an error handler so that django-configuration
originated errors can be caught and rendered to the user in a readable format.
"""
@wraps(callee)
def wrapper(*args, **kwargs):
try:
return callee(*args, **kwargs)
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