import copy
import logging
import os
from ConfigParser import ConfigParser, Error as ConfigParserError
from decimal import Decimal
from django import forms
from django.conf import settings as django_settings
from django.utils import importlib
__all__ = ('AVAILABLE_SETTINGS', 'parse_config')
DEFAULT_SETTINGS_FILENAME = 'settings.cfg'
logger = logging.getLogger('setman')
[docs]class Setting(object):
"""
Base class for setting values that can provided in configuration definition
file.
"""
default = None
field_args = ('label', 'help_text', 'initial', 'required', 'validators')
field_klass = None
field_kwargs = {}
help_text = None
label = None
name = None
required = True
type = None
validators = None
def __init__(self, **kwargs):
restricted = ('field_klass', 'field_args', 'field_kwargs')
for key, _ in kwargs.items():
if not hasattr(self, key):
kwargs.pop(key)
if key in restricted:
kwargs.pop(key)
self.__dict__.update(kwargs)
self.required = force_bool(self.required)
self.validators = self._parse_validators(self.validators)
def __repr__(self):
return u'<%s: %s>' % (self.__class__.__name__, self.__unicode__())
def __unicode__(self):
return u'%s = %r' % (self.name, self.initial)
@property
[docs] def initial(self):
"""
Read real setting value from database or if impossible - just send
default setting value.
"""
from setman import settings
return getattr(settings, self.name, self.default)
[docs] def to_field(self, **kwargs):
"""
Convert current setting instance to form field.
"""
if not self.field_klass:
raise ValueError('Please, supply `field_klass` attribute first.')
field_kwargs = {}
for arg in self.field_args:
value = kwargs[arg] if arg in kwargs else getattr(self, arg)
field_kwargs.update({arg: value})
field_kwargs.update(**self.field_kwargs)
return self.field_klass(**field_kwargs)
[docs] def to_python(self, value):
"""
Convert setting value to necessary Python type. By default, returns
same value without any conversion.
"""
return value
[docs] def _parse_validators(self, value):
"""
Parse validators string and try to convert it to list with actual
validator functions.
"""
if not value:
return []
items = map(lambda item: item.strip(), value.split(','))
validators = []
for item in items:
module, attr = item.rsplit('.', 1)
try:
mod = importlib.import_module(module)
except ImportError:
logger.exception('Cannot load %r validator for %s setting.',
item, self.name)
continue
try:
validator = getattr(mod, attr)
except AttributeError:
logger.exception('Cannot load %r validator for %s setting.',
item, self.name)
continue
validators.append(validator)
return validators
class SettingTypeDoesNotExist(Exception):
"""
Simple exception that raised when user tried to load not supported setting
type from configuration definition file.
"""
class BooleanSetting(Setting):
"""
Boolean setting.
"""
field_klass = forms.BooleanField
required = False
type = 'boolean'
def __init__(self, **kwargs):
super(BooleanSetting, self).__init__(**kwargs)
self.default = self.to_python(self.default)
def to_python(self, value):
"""
Convert string to the boolean type.
"""
return force_bool(value)
class ChoiceSetting(Setting):
"""
Choice setting.
"""
choices = None
field_args = Setting.field_args + ('choices', )
field_klass = forms.ChoiceField
type = 'choice'
def __init__(self, **kwargs):
super(ChoiceSetting, self).__init__(**kwargs)
self.choices = self.build_choices(self.choices)
def build_choices(self, value):
return tuple(map(lambda s: s.strip(), value.split(',')))
def to_field(self, **kwargs):
old_choices = self.choices
self.choices = [(choice, choice) for choice in old_choices]
field = super(ChoiceSetting, self).to_field(**kwargs)
self.choices = old_choices
return field
class DecimalSetting(Setting):
"""
Decimal setting.
"""
decimal_places = None
field_args = Setting.field_args + ('decimal_places', 'max_digits',
'max_value', 'min_value')
field_klass = forms.DecimalField
max_digits = None
max_value = None
min_value = None
type = 'decimal'
def __init__(self, **kwargs):
super(DecimalSetting, self).__init__(**kwargs)
int_setting = IntSetting()
self.decimal_places = int_setting.to_python(self.decimal_places)
self.max_digits = int_setting.to_python(self.max_digits)
self.default = self.to_python(self.default)
self.max_value = self.to_python(self.max_value)
self.min_value = self.to_python(self.min_value)
def to_python(self, value):
if value is None:
return value
return Decimal(value)
class IntSetting(Setting):
"""
Integer setting.
"""
field_args = Setting.field_args + ('max_value', 'min_value')
field_klass = forms.IntegerField
max_value = None
min_value = None
type = 'int'
def __init__(self, **kwargs):
super(IntSetting, self).__init__(**kwargs)
self.default = self.to_python(self.default)
self.max_value = self.to_python(self.max_value)
self.min_value = self.to_python(self.min_value)
def to_python(self, value):
try:
return int(value)
except (TypeError, ValueError):
return None
class FloatSetting(IntSetting):
"""
Float setting.
"""
field_klass = forms.FloatField
type = 'float'
def to_python(self, value):
try:
return float(value)
except (TypeError, ValueError):
return None
class StringSetting(Setting):
"""
String setting.
"""
field_args = Setting.field_args + ('max_length', 'min_length')
field_klass = forms.CharField
max_length = None
min_length = None
regex = None
type = 'string'
def to_field(self, **kwargs):
"""
Use ``RegexField`` for string setting if ``regex`` was filled in
configuration definition file.
"""
if self.regex:
if not 'regex' in self.field_args:
self.field_args = self.field_args + ('regex', )
self.field_klass = forms.RegexField
return super(StringSetting, self).to_field(**kwargs)
class SettingsContainer(object):
def __init__(self, path):
self._data = []
self.path = path
def __iter__(self):
return (item for item in self._data)
def __len__(self):
return len(self._data)
def append(self, value):
self._data.append(value)
setattr(self, value.name, value)
def data_to_setting(data):
"""
Convert data dict to setting instance.
"""
setting = None
setting_type = data.get('type')
for value in globals().values():
try:
if not issubclass(value, Setting):
continue
except TypeError:
continue
if not value.type or not setting_type or \
value.type.lower() != setting_type.lower():
continue
setting = value(**data)
if setting is None:
raise SettingTypeDoesNotExist('%r setting type not found.' % \
setting_type)
return setting
def force_bool(value):
"""
Convert string value to boolean instance.
"""
if isinstance(value, (bool, int)):
return bool(value)
boolean_states = ConfigParser._boolean_states
if not value.lower() in boolean_states:
return None
return boolean_states[value.lower()]
def parse_config(path=None):
"""
Parse Configuration Definition File.
In most cases this file needs to be placed in same folder where project
settings module exist and named as ``settings.cfg``.
But you can customize things with using ``SETMAN_SETTINGS_FILE`` option.
Provide there path where settings file actually placed.
Also current function can called with ``path`` string.
"""
if path is None:
path = getattr(django_settings, 'SETMAN_SETTINGS_FILE', None)
if path is None:
module = importlib.import_module(django_settings.SETTINGS_MODULE)
dirname = os.path.dirname(os.path.normpath(module.__file__))
path = os.path.join(dirname, DEFAULT_SETTINGS_FILENAME)
empty_settings = SettingsContainer(path)
if not os.path.isfile(path):
logger.error('Cannot read configuration definition file at %r. Exit ' \
'from parsing!', path)
return empty_settings
config = ConfigParser()
try:
config.read(path)
except ConfigParserError:
logger.exception('Cannot parse configuration definition file from ' \
'%r', path)
return empty_settings
settings = copy.deepcopy(empty_settings)
for setting in config.sections():
data = dict(config.items(setting))
data.update({'name': setting})
try:
setting = data_to_setting(data)
except SettingTypeDoesNotExist:
logger.exception('Cannot find proper setting class for %r type',
data.get('type'))
return empty_settings
settings.append(setting)
return settings
AVAILABLE_SETTINGS = parse_config()