mirror of
https://github.com/jazzband/django-configurations.git
synced 2026-05-23 22:55:50 +00:00
transport additional information in error types
This commit is contained in:
parent
ece1521acd
commit
7de2615441
2 changed files with 128 additions and 67 deletions
|
|
@ -3,36 +3,86 @@ from functools import wraps
|
|||
import sys
|
||||
import os
|
||||
|
||||
|
||||
class ValueRetrievalError(ValueError):
|
||||
"""
|
||||
Exception is raised when errors occur during the retrieval of a dynamic Value.
|
||||
This can happen when the environment variable corresponding to the value is not defined.
|
||||
"""
|
||||
|
||||
def __init__(self, value_instance: 'configurations.values.Value', error_msg: str) -> None:
|
||||
super().__init__(error_msg)
|
||||
self.value_instance = value_instance
|
||||
self.error_msg = error_msg
|
||||
|
||||
|
||||
class ValueProcessingError(ValueError):
|
||||
"""
|
||||
Exception that is raised when a dynamic Value failed to be processed after retrieval.
|
||||
Processing could be i.e. converting from string to a native datatype but also validation.
|
||||
"""
|
||||
|
||||
def __init__(self, value_instance: 'configurations.values.Value', str_value: str, error_msg: str) -> None:
|
||||
super().__init__(error_msg)
|
||||
self.value_instance = value_instance
|
||||
self.str_value = str_value
|
||||
self.error_msg = error_msg
|
||||
if TYPE_CHECKING:
|
||||
from .values import Value
|
||||
|
||||
|
||||
class TermStyles:
|
||||
BOLD = "\033[1m"
|
||||
RED = "\033[91m"
|
||||
END = "\033[0m"
|
||||
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 ""
|
||||
|
||||
|
||||
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*.
|
||||
"""
|
||||
explanation_lines = list(extra_explanation_lines)
|
||||
if value_instance.destination_name is not None:
|
||||
explanation_lines.append(f"{value_instance.destination_name} is taken from the environment variable "
|
||||
f"{value_instance.full_environ_name} as a {type(value_instance).__name__}")
|
||||
|
||||
super().__init__(
|
||||
f"Value of {value_instance.destination_name} could not be retrieved from environment",
|
||||
explanation_lines
|
||||
)
|
||||
|
||||
|
||||
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)
|
||||
if value_instance.destination_name is not None:
|
||||
explanation_lines.append(f"{value_instance.destination_name} is taken from the environment variable "
|
||||
f"{value_instance.full_environ_name} as a {type(value_instance).__name__}")
|
||||
|
||||
explanation_lines.append(f"'{raw_value}' was received but that is invalid")
|
||||
|
||||
super().__init__(error, explanation_lines)
|
||||
|
||||
|
||||
def with_error_handler(callee: Callable) -> Callable:
|
||||
|
|
@ -40,14 +90,15 @@ 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 (ValueRetrievalError, ValueProcessingError) as e:
|
||||
msg = "{}{}{}".format(TermStyles.RED + TermStyles.BOLD, e, TermStyles.END) \
|
||||
if os.isatty(sys.stderr.fileno()) \
|
||||
else str(e)
|
||||
except ConfigurationError as e:
|
||||
msg = "{}{}{}".format(TermStyles.RED + TermStyles.BOLD, e, TermStyles.END)
|
||||
for line in e.explanation_lines:
|
||||
msg += f"\n {line}"
|
||||
print(msg, file=sys.stderr)
|
||||
|
||||
return wrapper
|
||||
|
|
|
|||
|
|
@ -2,13 +2,12 @@ import ast
|
|||
import copy
|
||||
import decimal
|
||||
import os
|
||||
import sys
|
||||
|
||||
from django.core import validators
|
||||
from django.core.exceptions import ValidationError, ImproperlyConfigured
|
||||
from django.utils.module_loading import import_string
|
||||
|
||||
from .errors import ValueRetrievalError, ValueProcessingError
|
||||
from .errors import ValueRetrievalError, ValueProcessingError, ConfigurationError
|
||||
from .utils import getargspec
|
||||
|
||||
|
||||
|
|
@ -71,6 +70,7 @@ class Value:
|
|||
self.environ_prefix = environ_prefix
|
||||
self.environ_name = environ_name
|
||||
self.environ_required = environ_required
|
||||
self.destination_name = None
|
||||
|
||||
def __str__(self):
|
||||
return str(self.value)
|
||||
|
|
@ -87,33 +87,46 @@ class Value:
|
|||
# Compatibility with python 2
|
||||
__nonzero__ = __bool__
|
||||
|
||||
def full_environ_name(self, name):
|
||||
@property
|
||||
def full_environ_name(self):
|
||||
"""
|
||||
The full name of the environment variable (including prefix and capitalization) from which this value should be
|
||||
retrieved
|
||||
"""
|
||||
if self.environ_name:
|
||||
environ_name = self.environ_name
|
||||
else:
|
||||
environ_name = name.upper()
|
||||
environ_name = self.destination_name.upper()
|
||||
if self.environ_prefix:
|
||||
environ_name = '{0}_{1}'.format(self.environ_prefix, environ_name)
|
||||
return environ_name
|
||||
|
||||
def setup(self, name):
|
||||
"""
|
||||
Set up this value instance by retrieving the configured value from the environment and converting it to a native
|
||||
python data type
|
||||
|
||||
:param name: Destination name for which this value is used.
|
||||
For example in the scenario of `DEBUG = Value()` in a `Configuration` subclass, this would be `DEBUG`.
|
||||
"""
|
||||
self.destination_name = name
|
||||
value = self.default
|
||||
if self.environ:
|
||||
full_environ_name = self.full_environ_name(name)
|
||||
full_environ_name = self.full_environ_name
|
||||
if full_environ_name in os.environ:
|
||||
value = self.to_python(os.environ[full_environ_name])
|
||||
elif self.environ_required:
|
||||
raise ValueRetrievalError(self, 'Value {0!r} is required to be set as the '
|
||||
'environment variable {1!r}'
|
||||
.format(name, full_environ_name))
|
||||
raise ValueRetrievalError(self)
|
||||
self.value = value
|
||||
return value
|
||||
|
||||
def to_python(self, value):
|
||||
def to_python(self, value: str):
|
||||
"""
|
||||
Convert the given value of a environment variable into an
|
||||
Convert the given value of an environment variable into an
|
||||
appropriate Python representation of the value.
|
||||
This should be overriden when subclassing.
|
||||
This should be overridden when subclassing.
|
||||
|
||||
:param value: The value that should be converted to a python representation
|
||||
"""
|
||||
return value
|
||||
|
||||
|
|
@ -132,15 +145,14 @@ class BooleanValue(Value):
|
|||
raise ImproperlyConfigured('Default value {0!r} is not a '
|
||||
'boolean value'.format(self.default))
|
||||
|
||||
def to_python(self, value):
|
||||
def to_python(self, value: str):
|
||||
normalized_value = value.strip().lower()
|
||||
if normalized_value in self.true_values:
|
||||
return True
|
||||
elif normalized_value in self.false_values:
|
||||
return False
|
||||
else:
|
||||
raise ValueProcessingError(self, value, 'Cannot interpret '
|
||||
'boolean value {0!r}'.format(value))
|
||||
raise ValueProcessingError(self, value)
|
||||
|
||||
|
||||
class CastingMixin:
|
||||
|
|
@ -167,14 +179,14 @@ class CastingMixin:
|
|||
except TypeError:
|
||||
self._params = {}
|
||||
|
||||
def to_python(self, value):
|
||||
def to_python(self, value: str):
|
||||
try:
|
||||
if self._params:
|
||||
return self._caster(value, **self._params)
|
||||
else:
|
||||
return self._caster(value)
|
||||
except self.exception:
|
||||
raise ValueProcessingError(self, value, self.message.format(value))
|
||||
raise ValueProcessingError(self, value)
|
||||
|
||||
|
||||
class IntegerValue(CastingMixin, Value):
|
||||
|
|
@ -183,10 +195,10 @@ class IntegerValue(CastingMixin, Value):
|
|||
|
||||
class PositiveIntegerValue(IntegerValue):
|
||||
|
||||
def to_python(self, value):
|
||||
def to_python(self, value: str):
|
||||
int_value = super().to_python(value)
|
||||
if int_value < 0:
|
||||
raise ValueProcessingError(self, value, self.message.format(value))
|
||||
raise ValueProcessingError(self, value, f"Value needs to be positive or zero but {int_value} isn't")
|
||||
return int_value
|
||||
|
||||
|
||||
|
|
@ -210,7 +222,7 @@ class SequenceValue(Value):
|
|||
converter = None
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
msg = 'Cannot interpret {0} item {{0!r}} in {0} {{1!r}}'
|
||||
msg = 'Cannot interpret {0} item in {0} {{0!r}}'
|
||||
self.message = msg.format(self.sequence_type.__name__)
|
||||
self.separator = kwargs.pop('separator', ',')
|
||||
converter = kwargs.pop('converter', None)
|
||||
|
|
@ -232,10 +244,10 @@ class SequenceValue(Value):
|
|||
try:
|
||||
converted_values.append(self.converter(value))
|
||||
except (TypeError, ValueError):
|
||||
raise ValueProcessingError(self, self.separator.join(sequence), self.message.format(value, value))
|
||||
raise ValueProcessingError(self, self.separator.join(sequence))
|
||||
return self.sequence_type(converted_values)
|
||||
|
||||
def to_python(self, value):
|
||||
def to_python(self, value: str):
|
||||
split_value = [v.strip() for v in value.strip().split(self.separator)]
|
||||
# removing empty items
|
||||
value_list = self.sequence_type(filter(None, split_value))
|
||||
|
|
@ -271,7 +283,7 @@ class SingleNestedSequenceValue(SequenceValue):
|
|||
return self.sequence_type(converted_sequences)
|
||||
return self.sequence_type(super()._convert(items))
|
||||
|
||||
def to_python(self, value):
|
||||
def to_python(self, value: str):
|
||||
split_value = [
|
||||
v.strip() for v in value.strip().split(self.seq_separator)
|
||||
]
|
||||
|
|
@ -297,13 +309,11 @@ class BackendsValue(ListValue):
|
|||
try:
|
||||
import_string(value)
|
||||
except ImportError as err:
|
||||
raise ValueProcessingError(self, value, str(err)).with_traceback(sys.exc_info()[2])
|
||||
raise ValueProcessingError(self, value)
|
||||
return value
|
||||
|
||||
|
||||
class SetValue(ListValue):
|
||||
message = 'Cannot interpret set item {0!r} in set {1!r}'
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
if self.default is None:
|
||||
|
|
@ -311,7 +321,7 @@ class SetValue(ListValue):
|
|||
else:
|
||||
self.default = set(self.default)
|
||||
|
||||
def to_python(self, value):
|
||||
def to_python(self, value: str):
|
||||
return set(super().to_python(value))
|
||||
|
||||
|
||||
|
|
@ -325,16 +335,16 @@ class DictValue(Value):
|
|||
else:
|
||||
self.default = dict(self.default)
|
||||
|
||||
def to_python(self, value):
|
||||
def to_python(self, value: str):
|
||||
value = super().to_python(value)
|
||||
if not value:
|
||||
return {}
|
||||
try:
|
||||
evaled_value = ast.literal_eval(value)
|
||||
except ValueError:
|
||||
raise ValueProcessingError(self, value, self.message.format(value))
|
||||
raise ValueProcessingError(self, value)
|
||||
if not isinstance(evaled_value, dict):
|
||||
raise ValueProcessingError(self, value, self.message.format(value))
|
||||
raise ValueProcessingError(self, value)
|
||||
return evaled_value
|
||||
|
||||
|
||||
|
|
@ -357,13 +367,13 @@ class ValidationMixin:
|
|||
try:
|
||||
self.to_python(self.default)
|
||||
except ValueProcessingError as e:
|
||||
raise ImproperlyConfigured(e.error_msg)
|
||||
raise ImproperlyConfigured(e.main_error_msg) from e
|
||||
|
||||
def to_python(self, value):
|
||||
def to_python(self, value: str):
|
||||
try:
|
||||
self._validator(value)
|
||||
except ValidationError:
|
||||
raise ValueProcessingError(self, value, self.message.format(value))
|
||||
except ValidationError as e:
|
||||
raise ValueProcessingError(self, value, f"Validation failed: {e.message}")
|
||||
else:
|
||||
return value
|
||||
|
||||
|
|
@ -401,7 +411,7 @@ class PathValue(Value):
|
|||
value = super().setup(name)
|
||||
value = os.path.expanduser(value)
|
||||
if self.check_exists and not os.path.exists(value):
|
||||
raise ValueProcessingError(self, value, 'Path {0!r} does not exist.'.format(value))
|
||||
raise ValueProcessingError(self, value, f"Path {value} does not exist")
|
||||
return os.path.abspath(value)
|
||||
|
||||
|
||||
|
|
@ -418,7 +428,7 @@ class SecretValue(Value):
|
|||
def setup(self, name):
|
||||
value = super().setup(name)
|
||||
if not value:
|
||||
raise ValueRetrievalError(self, 'Secret value {0!r} is not set'.format(name))
|
||||
raise ValueRetrievalError(self)
|
||||
return value
|
||||
|
||||
|
||||
|
|
@ -452,7 +462,7 @@ class DictBackendMixin(Value):
|
|||
else:
|
||||
self.default = self.to_python(self.default)
|
||||
|
||||
def to_python(self, value):
|
||||
def to_python(self, value: str):
|
||||
value = super().to_python(value)
|
||||
return {self.alias: value}
|
||||
|
||||
|
|
|
|||
Loading…
Reference in a new issue