Skip to content

Commit

Permalink
refactor DatabaseBackend
Browse files Browse the repository at this point in the history
  • Loading branch information
Iurchenko Sergei committed Nov 21, 2023
1 parent d03bea8 commit 7807dd6
Show file tree
Hide file tree
Showing 9 changed files with 108 additions and 149 deletions.
6 changes: 3 additions & 3 deletions constance/admin.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,12 +13,12 @@
from django.utils.formats import localize
from django.utils.translation import gettext_lazy as _

from . import LazyConfig, settings

from constance import config
from . import settings
from .forms import ConstanceForm
from .utils import get_values

config = LazyConfig()


class ConstanceAdmin(admin.ModelAdmin):
change_list_template = 'admin/constance/change_list.html'
Expand Down
4 changes: 2 additions & 2 deletions constance/apps.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,5 +8,5 @@ class ConstanceConfig(AppConfig):
default_auto_field = 'django.db.models.AutoField'

def ready(self):
from . import checks

from constance import checks, config
config.init()
7 changes: 7 additions & 0 deletions constance/backends/__init__.py
Original file line number Diff line number Diff line change
@@ -1,10 +1,17 @@
"""
Defines the base constance backend
"""
from django.conf import settings


class Backend:

def get_default(self, key):
"""
Get the key from the settings config and return the value.
"""
return settings.CONSTANCE_CONFIG[key][0]

def get(self, key):
"""
Get the key from the backend store and return the value.
Expand Down
132 changes: 25 additions & 107 deletions constance/backends/database.py
Original file line number Diff line number Diff line change
@@ -1,129 +1,47 @@
from django.core.cache import caches
from django.core.cache.backends.locmem import LocMemCache
from django.core.exceptions import ImproperlyConfigured
from django.db import (
IntegrityError,
OperationalError,
ProgrammingError,
transaction,
)
from django.db.models.signals import post_save

from constance.backends import Backend
from constance import settings, signals, config
from constance.models import Constance


class DatabaseBackend(Backend):
def __init__(self):
from constance.models import Constance
self._model = Constance
self._prefix = settings.DATABASE_PREFIX
self._autofill_timeout = settings.DATABASE_CACHE_AUTOFILL_TIMEOUT
self._autofill_cachekey = 'autofilled'

if self._model._meta.app_config is None:
raise ImproperlyConfigured(
"The constance.backends.database app isn't installed "
"correctly. Make sure it's in your INSTALLED_APPS setting.")

if settings.DATABASE_CACHE_BACKEND:
self._cache = caches[settings.DATABASE_CACHE_BACKEND]
if isinstance(self._cache, LocMemCache):
raise ImproperlyConfigured(
"The CONSTANCE_DATABASE_CACHE_BACKEND setting refers to a "
"subclass of Django's local-memory backend (%r). Please "
"set it to a backend that supports cross-process caching."
% settings.DATABASE_CACHE_BACKEND)
else:
self._cache = None
self.autofill()
# Clear simple cache.
post_save.connect(self.clear, sender=self._model)

def add_prefix(self, key):
return "%s%s" % (self._prefix, key)

def autofill(self):
if not self._autofill_timeout or not self._cache:
return
full_cachekey = self.add_prefix(self._autofill_cachekey)
if self._cache.get(full_cachekey):
return
autofill_values = {}
autofill_values[full_cachekey] = 1
for key, value in self.mget(settings.CONFIG):
autofill_values[self.add_prefix(key)] = value
self._cache.set_many(autofill_values, timeout=self._autofill_timeout)
return "%s%s" % (settings.DATABASE_PREFIX, key)

def mget(self, keys):
if not keys:
return
keys = {self.add_prefix(key): key for key in keys}
try:
stored = self._model._default_manager.filter(key__in=keys)
for const in stored:
yield keys[const.key], const.value
except (OperationalError, ProgrammingError):
pass
return {}

objects = Constance.objects.filter(key__in=[self.add_prefix(key) for key in keys])
# all keys should be present in result even they are absent in database
result = {key: self.get_default(key) for key in keys}
for obj in objects:
result[obj.key] = obj.value
return result

def get(self, key):
key = self.add_prefix(key)
if self._cache:
value = self._cache.get(key)
if value is None:
self.autofill()
value = self._cache.get(key)
else:
try:
obj = Constance.objects.get(key=self.add_prefix(key))
value = obj.value
except Constance.DoesNotExist:
value = None
if value is None:
try:
value = self._model._default_manager.get(key=key).value
except (OperationalError, ProgrammingError, self._model.DoesNotExist):
pass
else:
if self._cache:
self._cache.add(key, value)
return value

def set(self, key, value):
key = self.add_prefix(key)
created = False
queryset = self._model._default_manager.all()
# Set _for_write attribute as get_or_create method does
# https://github.com/django/django/blob/2.2.11/django/db/models/query.py#L536
queryset._for_write = True
db_key = self.add_prefix(key)

try:
constance = queryset.get(key=key)
except (OperationalError, ProgrammingError):
# database is not created, noop
return
except self._model.DoesNotExist:
try:
with transaction.atomic(using=queryset.db):
queryset.create(key=key, value=value)
created = True
except IntegrityError as error:
# Allow concurrent writes
constance = queryset.get(key=key)

if not created:
old_value = constance.value
constance.value = value
constance.save()
else:
old_value = None

if self._cache:
self._cache.set(key, value)
obj = Constance.objects.get(key=db_key)
old_value = obj.value
if value == old_value:
return
else:
obj.value = value
obj.save()
except Constance.DoesNotExist:
old_value = self.get_default(key)
Constance.objects.create(key=db_key, value=value)

signals.config_updated.send(
sender=config, key=key, old_value=old_value, new_value=value
)

def clear(self, sender, instance, created, **kwargs):
if self._cache and not created:
keys = [self.add_prefix(k) for k in settings.CONFIG]
keys.append(self.add_prefix(self._autofill_cachekey))
self._cache.delete_many(keys)
self.autofill()
3 changes: 0 additions & 3 deletions constance/backends/memory.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,9 +11,6 @@ class MemoryBackend(Backend):
_storage = {}
_lock = Lock()

def __init__(self):
super().__init__()

def get(self, key):
with self._lock:
return self._storage.get(key)
Expand Down
62 changes: 37 additions & 25 deletions constance/base.py
Original file line number Diff line number Diff line change
@@ -1,32 +1,44 @@
from django.core.exceptions import AppRegistryNotReady

from . import settings, utils


class Config:
"""
The global config wrapper that handles the backend.
"""
def __init__(self):
super().__setattr__('_backend',
utils.import_module_attr(settings.BACKEND)())
def _get_config_class():

def __getattr__(self, key):
try:
if not len(settings.CONFIG[key]) in (2, 3):
raise AttributeError(key)
default = settings.CONFIG[key][0]
except KeyError:
raise AttributeError(key)
result = self._backend.get(key)
if result is None:
result = default
setattr(self, key, default)
is_ready = False

class _Config:
"""
The global config wrapper that handles the backend.
"""

def init(self):
super().__setattr__('_backend', utils.import_module_attr(settings.BACKEND)())
nonlocal is_ready
is_ready = True

def __getattr__(self, key):
if not is_ready:
raise AppRegistryNotReady("Apps aren't loaded yet.")

result = self._backend.get(key)
if result is None:
result = self._backend.get_default(key)
return result
return result
return result

def __setattr__(self, key, value):
if key not in settings.CONFIG:
raise AttributeError(key)
self._backend.set(key, value)
def __setattr__(self, key, value):
if not is_ready:
raise AppRegistryNotReady("Apps aren't loaded yet.")

if key not in settings.CONFIG:
raise AttributeError(key)
self._backend.set(key, value)

def __dir__(self):
return settings.CONFIG.keys()

return _Config


def __dir__(self):
return settings.CONFIG.keys()
Config = _get_config_class()
29 changes: 27 additions & 2 deletions constance/checks.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ def check_fieldsets(*args, **kwargs) -> List[CheckMessage]:
"field(s) that exists in CONSTANCE_CONFIG."
),
hint=", ".join(sorted(missing_keys)),
obj="settings.CONSTANCE_CONFIG",
obj="settings.CONFIG_FIELDSETS",
id="constance.E001",
)
errors.append(check)
Expand All @@ -34,7 +34,7 @@ def check_fieldsets(*args, **kwargs) -> List[CheckMessage]:
"field(s) that does not exist in CONFIG."
),
hint=", ".join(sorted(extra_keys)),
obj="settings.CONSTANCE_CONFIG",
obj="settings.CONFIG_FIELDSETS",
id="constance.E002",
)
errors.append(check)
Expand Down Expand Up @@ -68,3 +68,28 @@ def get_inconsistent_fieldnames() -> Tuple[Set, Set]:
missing_keys = config_keys - unique_field_names
extra_keys = unique_field_names - config_keys
return missing_keys, extra_keys


@checks.register("constance")
def check_config(*args, **kwargs) -> List[CheckMessage]:
"""
A Django system check to make sure that, CONSTANCE_CONFIG is 2 or 3 length tuple.
"""
from . import settings

errors = []
allowed_length = (2, 3)

for key, value in settings.CONFIG.items():
if len(value) not in allowed_length:
check = checks.ERROR(
_(
"CONSTANCE_CONFIG values should be 2 or 3 length tuple"
),
hint="Set default value, description and optionally type",
obj="settings.CONSTANCE_CONFIG",
id="constance.E003",
)
errors.append(check)

return errors
5 changes: 2 additions & 3 deletions constance/forms.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,11 +15,10 @@
from django.utils.text import normalize_newlines
from django.utils.translation import gettext_lazy as _

from . import LazyConfig, settings
from constance import config
from . import settings
from .checks import get_inconsistent_fieldnames

config = LazyConfig()

NUMERIC_WIDGET = forms.TextInput(attrs={'size': 10})

INTEGER_LIKE = (fields.IntegerField, {'widget': NUMERIC_WIDGET})
Expand Down
9 changes: 5 additions & 4 deletions constance/utils.py
Original file line number Diff line number Diff line change
@@ -1,17 +1,18 @@
from importlib import import_module

from . import LazyConfig, settings

config = LazyConfig()
from constance import config
from . import settings


def import_module_attr(path):
package, module = path.rsplit('.', 1)
return getattr(import_module(package), module)


def get_values():
"""
Get dictionary of values from the backend
:return:
"""

# First load a mapping between config name and default value
Expand All @@ -20,4 +21,4 @@ def get_values():
# Then update the mapping with actually values from the backend
initial = dict(default_initial, **dict(config._backend.mget(settings.CONFIG)))

return initial
return initial

0 comments on commit 7807dd6

Please sign in to comment.