django-configurations/configurations/values.py
Jannis Leidel 1a54847375 Added a configuration values system.
This also adds some advanced features like a setup classmethod to the Configuration class.
Reorganized and extended the documentation.
2013-07-27 12:37:28 +02:00

346 lines
11 KiB
Python

import ast
import copy
import decimal
import os
from django.core import validators
from django.core.exceptions import ValidationError, ImproperlyConfigured
from django.utils import six
from .utils import import_by_path
def setup_value(target, name, value):
actual_value = value.setup(name)
if value.multiple:
# overwriting the original Value class with the result
setattr(target, name, actual_value)
for multiple_name, multiple_value in actual_value.items():
setattr(target, multiple_name, multiple_value)
else:
setattr(target, name, actual_value)
class Value(object):
"""
A single settings value that is able to interpret env variables
and implements a simple validation scheme.
"""
multiple = False
def __init__(self, default=None, environ=False, environ_name=None,
environ_prefix='DJANGO', *args, **kwargs):
if isinstance(default, Value):
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
def __repr__(self):
return "<Value default: {0}>".format(self.default)
def setup(self, name):
value = self.default
if self.environ:
if self.environ_name is None:
environ_name = name.upper()
else:
environ_name = self.environ_name
if self.environ_prefix:
full_environ_name = '{0}_{1}'.format(self.environ_prefix,
environ_name)
else:
full_environ_name = environ_name
if full_environ_name in os.environ:
value = self.to_python(os.environ[full_environ_name])
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 {!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))
class CastingMixin(object):
exception = (TypeError, ValueError)
message = 'Cannot interpret value {!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 ImproperlyConfigured(error)
def to_python(self, value):
try:
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 ListValue(Value):
converter = None
message = 'Cannot interpret list item {!r} in list {!r}'
def __init__(self, *args, **kwargs):
self.separator = kwargs.pop('separator', ',')
converter = kwargs.pop('converter', None)
if converter is not None:
self.converter = converter
super(ListValue, self).__init__(*args, **kwargs)
# make sure the default is a list
if self.default is None:
self.default = []
# initial conversion
if self.converter is not None:
self.default = [self.converter(value) for value in self.default]
def to_python(self, value):
split_value = [v.strip() for v in value.strip().split(self.separator)]
# removing empty items
value_list = filter(None, split_value)
if self.converter is None:
return value_list
converted_values = []
for list_value in value_list:
try:
converted_values.append(self.converter(list_value))
except (TypeError, ValueError):
raise ValueError(self.message.format(list_value, value))
return converted_values
class BackendsValue(ListValue):
def converter(self, value):
import_by_path(value)
return value
class TupleValue(ListValue):
message = 'Cannot interpret tuple item {!r} in tuple {!r}'
def __init__(self, *args, **kwargs):
super(TupleValue, self).__init__(*args, **kwargs)
if self.default is None:
self.default = ()
else:
self.default = tuple(self.default)
def to_python(self, value):
return tuple(super(TupleValue, self).to_python(value))
class SetValue(ListValue):
message = 'Cannot interpret set item {!r} in set {!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):
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 {}
evaled_value = ast.literal_eval(value)
if not isinstance(evaled_value, dict):
raise ValueError('Cannot interpret dict value {!s}'.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:
error = 'Cannot use validator of {0} ({1!r})'.format(self,
self.validator)
raise ImproperlyConfigured(error)
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 {!r}'
validator = 'django.core.validators.validate_email'
class URLValue(ValidationMixin, Value):
message = 'Cannot interpret URL value {!r}'
validator = validators.URLValidator()
class IPValue(ValidationMixin, Value):
message = 'Cannot interpret IP value {!r}'
validator = 'django.core.validators.validate_ipv46_address'
class RegexValue(ValidationMixin, Value):
message = "Regex doesn't match value {!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 {!r} does not exist.'.format(value))
return os.path.abspath(value)
class SecretValue(Value):
def __init__(self, *args, **kwargs):
kwargs['environ'] = 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 {!r} is not set'.format(name))
return value
class DatabaseURLValue(CastingMixin, Value):
caster = 'dj_database_url.parse'
message = 'Cannot interpret database URL value {!r}'
def __init__(self, *args, **kwargs):
self.alias = kwargs.pop('alias', 'default')
kwargs.setdefault('environ', True)
kwargs.setdefault('environ_prefix', None)
kwargs.setdefault('environ_name', 'DATABASE_URL')
super(DatabaseURLValue, 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(DatabaseURLValue, self).to_python(value)
return {self.alias: value}
class EmailURLValue(CastingMixin, MultipleMixin, Value):
caster = 'dj_email_url.parse'
message = 'Cannot interpret email URL value {!r}'
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 CacheURLValue(CastingMixin, Value):
caster = 'django_cache_url.parse'
message = 'Cannot interpret cache URL value {!r}'
def __init__(self, name='default', *args, **kwargs):
self.alias = kwargs.pop('alias', 'default')
kwargs.setdefault('environ', True)
kwargs.setdefault('environ_prefix', None)
kwargs.setdefault('environ_name', 'CACHE_URL')
super(CacheURLValue, 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(CacheURLValue, self).to_python(value)
return {self.alias: value}