From eea0d2fdf9c6e1ece0e12be7abd1d669572136ba Mon Sep 17 00:00:00 2001 From: Jannis Leidel Date: Wed, 1 Dec 2010 00:20:23 +0100 Subject: [PATCH] Abstracted constance backends to and added a cached database backend. Moved around the settings code and simplified the module loading. --- constance/admin.py | 11 +-- constance/backends.py | 84 +++++++++++++++++++ constance/config.py | 40 +++------ constance/models.py | 25 ++++++ constance/settings.py | 17 ++++ constance/utils.py | 5 ++ .../testproject/test_app/tests/test_config.py | 28 +++++-- 7 files changed, 170 insertions(+), 40 deletions(-) create mode 100644 constance/backends.py create mode 100644 constance/settings.py create mode 100644 constance/utils.py diff --git a/constance/admin.py b/constance/admin.py index f318d35..b9ca7ac 100644 --- a/constance/admin.py +++ b/constance/admin.py @@ -6,18 +6,15 @@ from django import forms from django.contrib import admin from django.contrib.admin import widgets from django.contrib.admin.options import csrf_protect_m -from django.conf import settings from django.conf.urls.defaults import patterns, url -from django.core.urlresolvers import reverse from django.forms import fields from django.http import HttpResponseRedirect from django.shortcuts import render_to_response from django.template.context import RequestContext -from django.utils.functional import update_wrapper from django.utils.formats import localize from django.utils.translation import ugettext_lazy as _ -from constance import config +from constance import config, settings NUMERIC_WIDGET = forms.TextInput(attrs={'size': 10}) @@ -42,7 +39,7 @@ FIELDS = { class ConstanceForm(forms.Form): def __init__(self, *args, **kwargs): super(ConstanceForm, self).__init__(*args, **kwargs) - for name, (default, help_text) in settings.CONSTANCE_CONFIG.items(): + for name, (default, help_text) in settings.CONFIG.items(): field_class, kwargs = FIELDS[type(default)] self.fields[name] = field_class(label=name, **kwargs) @@ -65,7 +62,7 @@ class ConstanceAdmin(admin.ModelAdmin): @csrf_protect_m def changelist_view(self, request, extra_context=None): form = ConstanceForm(initial=dict((name, getattr(config, name)) - for name in settings.CONSTANCE_CONFIG)) + for name in settings.CONFIG)) if request.method == 'POST': form = ConstanceForm(request.POST) if form.is_valid(): @@ -81,7 +78,7 @@ class ConstanceAdmin(admin.ModelAdmin): 'form': form, 'media': self.media + form.media, } - for name, (default, help_text) in settings.CONSTANCE_CONFIG.iteritems(): + for name, (default, help_text) in settings.CONFIG.iteritems(): value = getattr(config, name) context['config'].append({ 'name': name, diff --git a/constance/backends.py b/constance/backends.py new file mode 100644 index 0000000..4ff915c --- /dev/null +++ b/constance/backends.py @@ -0,0 +1,84 @@ +from django.core.exceptions import ImproperlyConfigured +from django.db.models.signals import post_save +from django.utils.functional import memoize + +from constance import settings +from constance.utils import import_module_attr + +try: + from cPickle import loads, dumps +except ImportError: + from pickle import loads, dumps + + +class Backend(object): + + def __init__(self, prefix): + self._prefix = prefix + + def get(self, key): + """ + Get the key from the backend store and return it. + Return None if not found. + """ + raise NotImplementedError + + def set(self, key, value): + """ + Add the value to the backend store given the key. + """ + raise NotImplementedError + +db_cache = {} + +class DatabaseBackend(Backend): + + def __init__(self, prefix): + super(DatabaseBackend, self).__init__(prefix) + from constance.models import Constance + if not Constance._meta.installed: + raise ImproperlyConfigured( + "The constance app isn't installed correctly. " + "Make sure it's listed in the INSTALLED_APPS setting.") + self._model = Constance + # Clear simple cache. + post_save.connect(self.clear, sender=self._model) + + def _get(self, key): + try: + value = self._model._default_manager.get(key=key).value + except self._model.DoesNotExist: + return None + return value + get = memoize(_get, db_cache, 2) + + def set(self, key, value): + constance, created = self._model._default_manager.get_or_create(key=key, defaults={'value': value}) + if not created: + constance.value = value + constance.save() + + def clear(self, sender, instance, created, **kwargs): + if not created: + db_cache.clear() + +class RedisBackend(Backend): + + def __init__(self, prefix): + super(RedisBackend, self).__init__(prefix) + connection_cls = settings.CONNECTION_CLASS + if connection_cls is not None: + self._rd = import_module_attr(connection_cls)() + else: + import redis + self._rd = redis.Redis(**settings.REDIS_CONNECTION) + + def get(self, key): + value = self._rd.get("%s%s" % (self._prefix, key)) + if value: + return loads(value) + return None + + def set(self, key, value): + self._rd.set("%s%s" % (self._prefix, key), dumps(value)) + diff --git a/constance/config.py b/constance/config.py index 0bf670a..24860ab 100644 --- a/constance/config.py +++ b/constance/config.py @@ -1,44 +1,30 @@ -import os -import redis - -try: - from cPickle import loads, dumps -except ImportError: - from pickle import loads, dumps - -def import_module(path): - package, module = path.rsplit('.', 1) - return getattr(__import__(package, None, None, [module]), module) - -settings = import_module(os.getenv('CONSTANCE_SETTINGS_MODULE', 'django.conf.settings')) - +from constance import settings +from constance.utils import import_module_attr class Config(object): - + """ + The global config wrapper that handles the backend. + """ def __init__(self): - super(Config, self).__setattr__('_prefix', getattr(settings, 'CONSTANCE_PREFIX', 'constance:')) - try: - super(Config, self).__setattr__('_rd', import_module(settings.CONSTANCE_CONNECTION_CLASS)()) - except AttributeError: - super(Config, self).__setattr__('_rd', redis.Redis(**settings.CONSTANCE_CONNECTION)) + super(Config, self).__setattr__( + '_backend', import_module_attr(settings.BACKEND)(settings.PREFIX)) def __getattr__(self, key): try: - default, help_text = settings.CONSTANCE_CONFIG[key] + default, help_text = settings.CONFIG[key] except KeyError, e: raise AttributeError(key) - result = self._rd.get("%s%s" % (self._prefix, key)) + result = self._backend.get(key) if result is None: result = default setattr(self, key, default) return result - return loads(result) + return result def __setattr__(self, key, value): - if key not in settings.CONSTANCE_CONFIG: + if key not in settings.CONFIG: raise AttributeError(key) - self._rd.set("%s%s" % (self._prefix, key), dumps(value)) + self._backend.set(key, value) def __dir__(self): - return settings.CONSTANCE_CONFIG.keys() - + return settings.CONFIG.iterkeys() diff --git a/constance/models.py b/constance/models.py index e69de29..e4de4ed 100644 --- a/constance/models.py +++ b/constance/models.py @@ -0,0 +1,25 @@ +from constance import settings + +if settings.BACKEND == 'constance.backends.DatabaseBackend': + + from django.db import models + from django.core.exceptions import ImproperlyConfigured + + from django.utils.translation import ugettext_lazy as _ + + try: + from picklefield import PickledObjectField + except ImportError: + raise ImproperlyConfigured("Couldn't find the the 3rd party app " + "django-picklefield which is required for " + "the constance database backend.") + + class Constance(models.Model): + key = models.TextField() + value = PickledObjectField() + + class Meta: + verbose_name = _('constance') + verbose_name_plural = _('constances') + db_table = 'constance_config' + diff --git a/constance/settings.py b/constance/settings.py new file mode 100644 index 0000000..6756abc --- /dev/null +++ b/constance/settings.py @@ -0,0 +1,17 @@ +import os +from constance.utils import import_module_attr + +settings = import_module_attr( + os.getenv('CONSTANCE_SETTINGS_MODULE', 'django.conf.settings') +) + +PREFIX = getattr(settings, 'CONSTANCE_PREFIX', 'constance:') + +BACKEND = getattr(settings, 'CONSTANCE_BACKEND', 'constance.backends.DatabaseBackend') + +CONFIG = getattr(settings, 'CONSTANCE_CONFIG', {}) + +CONNECTION_CLASS = getattr(settings, 'CONSTANCE_CONNECTION_CLASS', None) + +REDIS_CONNECTION = getattr(settings, 'CONSTANCE_REDIS_CONNECTION', + getattr(settings, 'CONSTANCE_CONNECTION', {})) diff --git a/constance/utils.py b/constance/utils.py new file mode 100644 index 0000000..f6dc970 --- /dev/null +++ b/constance/utils.py @@ -0,0 +1,5 @@ +from django.utils.importlib import import_module + +def import_module_attr(path): + package, module = path.rsplit('.', 1) + return getattr(import_module(package), module) diff --git a/tests/testproject/test_app/tests/test_config.py b/tests/testproject/test_app/tests/test_config.py index 1ca6756..0b2cc47 100644 --- a/tests/testproject/test_app/tests/test_config.py +++ b/tests/testproject/test_app/tests/test_config.py @@ -1,5 +1,6 @@ # -*- encoding: utf-8 -*- +import sys from datetime import datetime, date, time from decimal import Decimal @@ -8,21 +9,19 @@ from django.conf import settings from django.contrib import admin from django.contrib.auth.models import User -from constance import config +from constance import settings from constance.admin import Config # Use django RequestFactory later on from testproject.test_app.tests.helpers import FakeRequest - -class TestStorage(TestCase): - - def tearDown(self): - config._rd.clear() +class TestStorage(object): def test_store(self): # read defaults + del sys.modules['constance'] + from constance import config self.assertEquals(config.INT_VALUE, 1) self.assertEquals(config.LONG_VALUE, 123456L) self.assertEquals(config.BOOL_VALUE, True) @@ -59,6 +58,7 @@ class TestStorage(TestCase): self.assertEquals(config.TIME_VALUE, time(1, 59, 0)) def test_nonexistent(self): + from constance import config try: config.NON_EXISTENT except Exception, e: @@ -71,6 +71,22 @@ class TestStorage(TestCase): pass self.assertEquals(type(e), AttributeError) +class TestRedis(TestCase, TestStorage): + + def setUp(self): + self.old_backend = settings.BACKEND + settings.BACKEND = 'constance.backends.RedisBackend' + + def tearDown(self): + del sys.modules['constance'] + from constance import config + config._backend._rd.clear() + settings.BACKEND = self.old_backend + import constance + constance.config = Config() + +class TestDatabase(TestCase, TestStorage): + pass class TestAdmin(TestCase): model = Config