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 import six
from .utils import import_by_path, 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)
class Value(object):
"""
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 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 = '{0}_{1}'.format(self.environ_prefix, environ_name)
return environ_name
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 {0!r} is required to be set as the '
'environment variable {1!r}'
.format(name, full_environ_name))
self.value = value
return value
def to_python(self, value):
"""
Convert the given value of a environment variable into an
appropriate Python representation of the value.
This should be overriden when subclassing.
"""
return value
class MultipleMixin(object):
multiple = True
class BooleanValue(Value):
true_values = ('yes', 'y', 'true', '1')
false_values = ('no', 'n', 'false', '0', '')
def __init__(self, *args, **kwargs):
super(BooleanValue, self).__init__(*args, **kwargs)
if self.default not in (True, False):
raise ValueError('Default value {0!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 {0!r}'.format(value))
class CastingMixin(object):
exception = (TypeError, ValueError)
message = 'Cannot interpret value {0!r}'
def __init__(self, *args, **kwargs):
super(CastingMixin, self).__init__(*args, **kwargs)
if isinstance(self.caster, six.string_types):
self._caster = import_by_path(self.caster)
elif callable(self.caster):
self._caster = self.caster
else:
error = 'Cannot use caster of {0} ({1!r})'.format(self,
self.caster)
raise ValueError(error)
try:
arg_names = getargspec(self._caster)[0]
self._params = dict((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))
class IntegerValue(CastingMixin, Value):
caster = int
class FloatValue(CastingMixin, Value):
caster = float
class DecimalValue(CastingMixin, Value):
caster = decimal.Decimal
exception = decimal.InvalidOperation
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'
sequence_type = 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(SequenceValue, self).__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)
class ListValue(SequenceValue):
sequence_type = list
class TupleValue(SequenceValue):
sequence_type = tuple
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(SingleNestedSequenceValue, self).__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(SingleNestedSequenceValue, self)._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)
class SingleNestedListValue(SingleNestedSequenceValue):
sequence_type = list
class SingleNestedTupleValue(SingleNestedSequenceValue):
sequence_type = tuple
class BackendsValue(ListValue):
def converter(self, value):
try:
import_by_path(value)
except ImproperlyConfigured as err:
six.reraise(ValueError, ValueError(err), sys.exc_info()[2])
return value
class SetValue(ListValue):
message = 'Cannot interpret set item {0!r} in set {1!r}'
def __init__(self, *args, **kwargs):
super(SetValue, self).__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(SetValue, self).to_python(value))
class DictValue(Value):
message = 'Cannot interpret dict value {0!r}'
def __init__(self, *args, **kwargs):
super(DictValue, self).__init__(*args, **kwargs)
if self.default is None:
self.default = {}
else:
self.default = dict(self.default)
def to_python(self, value):
value = super(DictValue, self).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
class ValidationMixin(object):
def __init__(self, *args, **kwargs):
super(ValidationMixin, self).__init__(*args, **kwargs)
if isinstance(self.validator, six.string_types):
self._validator = import_by_path(self.validator)
elif callable(self.validator):
self._validator = self.validator
else:
raise ValueError('Cannot use validator of '
'{0} ({1!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
class EmailValue(ValidationMixin, Value):
message = 'Cannot interpret email value {0!r}'
validator = 'django.core.validators.validate_email'
class URLValue(ValidationMixin, Value):
message = 'Cannot interpret URL value {0!r}'
validator = validators.URLValidator()
class IPValue(ValidationMixin, Value):
message = 'Cannot interpret IP value {0!r}'
validator = 'django.core.validators.validate_ipv46_address'
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(RegexValue, self).__init__(*args, **kwargs)
class PathValue(Value):
def __init__(self, *args, **kwargs):
self.check_exists = kwargs.pop('check_exists', True)
super(PathValue, self).__init__(*args, **kwargs)
def setup(self, name):
value = super(PathValue, self).setup(name)
value = os.path.expanduser(value)
if self.check_exists and not os.path.exists(value):
raise ValueError('Path {0!r} does not exist.'.format(value))
return os.path.abspath(value)
class SecretValue(Value):
def __init__(self, *args, **kwargs):
kwargs['environ'] = True
kwargs['environ_required'] = True
super(SecretValue, self).__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(SecretValue, self).setup(name)
if not value:
raise ValueError('Secret value {0!r} is not set'.format(name))
return value
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(EmailURLValue, self).__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(DictBackendMixin, self).__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(DictBackendMixin, self).to_python(value)
return {self.alias: value}
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
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
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