import ast
import copy
import decimal
import os
import sys
import typing
from django.core import validators
from django.core.exceptions import ValidationError, ImproperlyConfigured
from django.utils.module_loading import import_string
from .utils import getargspec
def setup_value(target, name, value):
actual_value = value.setup(name)
# overwriting the original Value class with the result
setattr(target, name, value.value)
if value.multiple:
for multiple_name, multiple_value in actual_value.items():
setattr(target, multiple_name, multiple_value)
[docs]
class Value:
"""
A single settings value that is able to interpret env variables
and implements a simple validation scheme.
"""
multiple = False
late_binding = False
environ_required = False
@property
def value(self):
value = self.default
if not hasattr(self, "_value") and self.environ_name:
self.setup(self.environ_name)
if hasattr(self, "_value"):
value = self._value
return value
@value.setter
def value(self, value):
self._value = value
def __new__(cls, *args, **kwargs):
"""
checks if the creation can end up directly in the final value.
That is the case whenever environ = False or environ_name is given.
"""
instance = object.__new__(cls)
if "late_binding" in kwargs:
instance.late_binding = kwargs.get("late_binding")
if not instance.late_binding:
instance.__init__(*args, **kwargs)
if (instance.environ and instance.environ_name) or (
not instance.environ and instance.default
):
instance = instance.setup(instance.environ_name)
return instance
def __init__(
self,
default=None,
environ=True,
environ_name=None,
environ_prefix="DJANGO",
environ_required=False,
*args,
**kwargs,
):
if isinstance(default, Value) and default.default is not None:
self.default = copy.copy(default.default)
else:
self.default = default
self.environ = environ
if environ_prefix and environ_prefix.endswith("_"):
environ_prefix = environ_prefix[:-1]
self.environ_prefix = environ_prefix
self.environ_name = environ_name
self.environ_required = environ_required
def __str__(self):
return str(self.value)
def __repr__(self):
return repr(self.value)
def __eq__(self, other):
return self.value == other
def __bool__(self):
return bool(self.value)
# Compatibility with python 2
__nonzero__ = __bool__
def full_environ_name(self, name):
if self.environ_name:
environ_name = self.environ_name
else:
environ_name = name.upper()
if self.environ_prefix:
environ_name = f"{self.environ_prefix}_{environ_name}"
return environ_name
[docs]
def setup(self, name):
value = self.default
if self.environ:
full_environ_name = self.full_environ_name(name)
if full_environ_name in os.environ:
value = self.to_python(os.environ[full_environ_name])
elif self.environ_required:
raise ValueError(
"Value {!r} is required to be set as the "
"environment variable {!r}".format(name, full_environ_name)
)
self.value = value
return value
[docs]
def to_python(self, value):
"""
Convert the given value of a environment variable into an
appropriate Python representation of the value.
This should be overridden when subclassing.
"""
return value
[docs]
class MultipleMixin:
multiple = True
[docs]
class BooleanValue(Value):
true_values = ("yes", "y", "true", "1")
false_values = ("no", "n", "false", "0", "")
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
if self.default not in (True, False):
raise ValueError(
"Default value {!r} is not a boolean value".format(self.default)
)
def to_python(self, value):
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 ValueError("Cannot interpret boolean value {!r}".format(value))
[docs]
class CastingMixin:
exception: typing.Any = (TypeError, ValueError)
message = "Cannot interpret value {0!r}"
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
if isinstance(self.caster, str):
try:
self._caster = import_string(self.caster)
except ImportError as err:
msg = f"Could not import {self.caster!r}"
raise ImproperlyConfigured(msg) from err
elif callable(self.caster):
self._caster = self.caster
else:
error = "Cannot use caster of {} ({!r})".format(self, self.caster)
raise ValueError(error)
try:
arg_names = getargspec(self._caster)[0]
self._params = {name: kwargs[name] for name in arg_names if name in kwargs}
except TypeError:
self._params = {}
def to_python(self, value):
try:
if self._params:
return self._caster(value, **self._params)
else:
return self._caster(value)
except self.exception:
raise ValueError(self.message.format(value))
[docs]
class IntegerValue(CastingMixin, Value):
caster = int
[docs]
class PositiveIntegerValue(IntegerValue):
def to_python(self, value):
int_value = super().to_python(value)
if int_value < 0:
raise ValueError(self.message.format(value))
return int_value
[docs]
class FloatValue(CastingMixin, Value):
caster = float
[docs]
class DecimalValue(CastingMixin, Value):
caster = decimal.Decimal
exception = decimal.InvalidOperation
[docs]
class SequenceValue(Value):
"""
Common code for sequence-type values (lists and tuples).
Do not use this class directly. Instead use a subclass.
"""
# Specify this value in subclasses, e.g. with 'list' or 'tuple'
# TODO: make the typing better
sequence_type: typing.Optional[typing.Any] = None
converter = None
def __init__(self, *args, **kwargs):
msg = "Cannot interpret {0} item {{0!r}} in {0} {{1!r}}"
self.message = msg.format(self.sequence_type.__name__)
self.separator = kwargs.pop("separator", ",")
converter = kwargs.pop("converter", None)
if converter is not None:
self.converter = converter
super().__init__(*args, **kwargs)
# make sure the default is the correct sequence type
if self.default is None:
self.default = self.sequence_type()
else:
self.default = self.sequence_type(self.default)
# initial conversion
if self.converter is not None:
self.default = self._convert(self.default)
def _convert(self, sequence):
converted_values = []
for value in sequence:
try:
converted_values.append(self.converter(value))
except (TypeError, ValueError):
raise ValueError(self.message.format(value, value))
return self.sequence_type(converted_values)
def to_python(self, value):
split_value = [v.strip() for v in value.strip().split(self.separator)]
# removing empty items
value_list = self.sequence_type(filter(None, split_value))
if self.converter is not None:
value_list = self._convert(value_list)
return self.sequence_type(value_list)
[docs]
class ListValue(SequenceValue):
sequence_type = list
[docs]
class TupleValue(SequenceValue):
sequence_type = tuple
[docs]
class SingleNestedSequenceValue(SequenceValue):
"""
Common code for nested sequences (list of lists, or tuple of tuples).
Do not use this class directly. Instead use a subclass.
"""
def __init__(self, *args, **kwargs):
self.seq_separator = kwargs.pop("seq_separator", ";")
super().__init__(*args, **kwargs)
def _convert(self, items):
# This could receive either a bare or nested sequence
if items and isinstance(items[0], self.sequence_type):
converted_sequences = [
super(SingleNestedSequenceValue, self)._convert(i) for i in items
]
return self.sequence_type(converted_sequences)
return self.sequence_type(super()._convert(items))
def to_python(self, value):
split_value = [v.strip() for v in value.strip().split(self.seq_separator)]
# Remove empty items
filtered = self.sequence_type(filter(None, split_value))
sequence = [
super(SingleNestedSequenceValue, self).to_python(f) for f in filtered
]
return self.sequence_type(sequence)
[docs]
class SingleNestedListValue(SingleNestedSequenceValue):
sequence_type = list
[docs]
class SingleNestedTupleValue(SingleNestedSequenceValue):
sequence_type = tuple
[docs]
class BackendsValue(ListValue):
def converter(self, value):
try:
import_string(value)
except ImportError as err:
raise ValueError(err).with_traceback(sys.exc_info()[2])
return value
[docs]
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:
self.default = set()
else:
self.default = set(self.default)
def to_python(self, value):
return set(super().to_python(value))
[docs]
class DictValue(Value):
message = "Cannot interpret dict value {0!r}"
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
if self.default is None:
self.default = {}
else:
self.default = dict(self.default)
def to_python(self, value):
value = super().to_python(value)
if not value:
return {}
try:
evaled_value = ast.literal_eval(value)
except ValueError:
raise ValueError(self.message.format(value))
if not isinstance(evaled_value, dict):
raise ValueError(self.message.format(value))
return evaled_value
[docs]
class ValidationMixin:
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
if isinstance(self.validator, str):
try:
self._validator = import_string(self.validator)
except ImportError as err:
msg = f"Could not import {self.validator!r}"
raise ImproperlyConfigured(msg) from err
elif callable(self.validator):
self._validator = self.validator
else:
raise ValueError(
"Cannot use validator of {} ({!r})".format(self, self.validator)
)
if self.default:
self.to_python(self.default)
def to_python(self, value):
try:
self._validator(value)
except ValidationError:
raise ValueError(self.message.format(value))
else:
return value
[docs]
class EmailValue(ValidationMixin, Value):
message = "Cannot interpret email value {0!r}"
validator = "django.core.validators.validate_email"
[docs]
class URLValue(ValidationMixin, Value):
message = "Cannot interpret URL value {0!r}"
validator = validators.URLValidator()
[docs]
class IPValue(ValidationMixin, Value):
message = "Cannot interpret IP value {0!r}"
validator = "django.core.validators.validate_ipv46_address"
[docs]
class RegexValue(ValidationMixin, Value):
message = "Regex doesn't match value {0!r}"
def __init__(self, *args, **kwargs):
regex = kwargs.pop("regex", None)
self.validator = validators.RegexValidator(regex=regex)
super().__init__(*args, **kwargs)
[docs]
class PathValue(Value):
def __init__(self, *args, **kwargs):
self.check_exists = kwargs.pop("check_exists", True)
super().__init__(*args, **kwargs)
def setup(self, name):
value = super().setup(name)
value = os.path.expanduser(value)
if self.check_exists and not os.path.exists(value):
raise ValueError(f"Path {value!r} does not exist.")
return os.path.abspath(value)
[docs]
class SecretValue(Value):
def __init__(self, *args, **kwargs):
kwargs["environ"] = True
kwargs["environ_required"] = True
super().__init__(*args, **kwargs)
if self.default is not None:
raise ValueError(
"Secret values are only allowed to be set as environment variables"
)
def setup(self, name):
value = super().setup(name)
if not value:
raise ValueError(f"Secret value {name!r} is not set")
return value
[docs]
class EmailURLValue(CastingMixin, MultipleMixin, Value):
caster = "dj_email_url.parse"
message = "Cannot interpret email URL value {0!r}"
late_binding = True
def __init__(self, *args, **kwargs):
kwargs.setdefault("environ", True)
kwargs.setdefault("environ_prefix", None)
kwargs.setdefault("environ_name", "EMAIL_URL")
super().__init__(*args, **kwargs)
if self.default is None:
self.default = {}
else:
self.default = self.to_python(self.default)
class DictBackendMixin(Value):
default_alias = "default"
def __init__(self, *args, **kwargs):
self.alias = kwargs.pop("alias", self.default_alias)
kwargs.setdefault("environ", True)
kwargs.setdefault("environ_prefix", None)
kwargs.setdefault("environ_name", self.environ_name)
super().__init__(*args, **kwargs)
if self.default is None:
self.default = {}
else:
self.default = self.to_python(self.default)
def to_python(self, value):
value = super().to_python(value)
return {self.alias: value}
[docs]
class DatabaseURLValue(DictBackendMixin, CastingMixin, Value):
caster = "dj_database_url.parse"
message = "Cannot interpret database URL value {0!r}"
environ_name = "DATABASE_URL"
late_binding = True
[docs]
class CacheURLValue(DictBackendMixin, CastingMixin, Value):
caster = "django_cache_url.parse"
message = "Cannot interpret cache URL value {0!r}"
environ_name = "CACHE_URL"
late_binding = True
[docs]
class SearchURLValue(DictBackendMixin, CastingMixin, Value):
caster = "dj_search_url.parse"
message = "Cannot interpret Search URL value {0!r}"
environ_name = "SEARCH_URL"
late_binding = True