Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

refactor DatabaseBackend #534

Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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 constance import settings


class Backend:

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

def get(self, key):
"""
Get the key from the backend store and return the value.
Expand Down
131 changes: 24 additions & 107 deletions constance/backends/database.py
Original file line number Diff line number Diff line change
@@ -1,129 +1,46 @@
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):
result = []
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 result

Check warning on line 14 in constance/backends/database.py

View check run for this annotation

Codecov / codecov/patch

constance/backends/database.py#L14

Added line #L14 was not covered by tests

objects = Constance.objects.filter(key__in=[self.add_prefix(key) for key in keys])
for obj in objects:
result.append((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

Check warning on line 34 in constance/backends/database.py

View check run for this annotation

Codecov / codecov/patch

constance/backends/database.py#L34

Added line #L34 was not covered by tests
if value == old_value:
return

Check warning on line 36 in constance/backends/database.py

View check run for this annotation

Codecov / codecov/patch

constance/backends/database.py#L36

Added line #L36 was not covered by tests
else:
obj.value = value
obj.save()

Check warning on line 39 in constance/backends/database.py

View check run for this annotation

Codecov / codecov/patch

constance/backends/database.py#L38-L39

Added lines #L38 - L39 were not covered by tests
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()
7 changes: 2 additions & 5 deletions constance/backends/memory.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,17 +11,14 @@
_storage = {}
_lock = Lock()

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

def get(self, key):
with self._lock:
return self._storage.get(key)

def mget(self, keys):
if not keys:
return
result = []
if not keys:
return result

Check warning on line 21 in constance/backends/memory.py

View check run for this annotation

Codecov / codecov/patch

constance/backends/memory.py#L21

Added line #L21 was not covered by tests
with self._lock:
for key in keys:
value = self._storage.get(key)
Expand Down
16 changes: 11 additions & 5 deletions constance/backends/redisd.py
Original file line number Diff line number Diff line change
Expand Up @@ -37,12 +37,15 @@
return None

def mget(self, keys):
result = []
if not keys:
return
return result

Check warning on line 42 in constance/backends/redisd.py

View check run for this annotation

Codecov / codecov/patch

constance/backends/redisd.py#L42

Added line #L42 was not covered by tests

prefixed_keys = [self.add_prefix(key) for key in keys]
for key, value in zip(keys, self._rd.mget(prefixed_keys)):
if value:
yield key, loads(value)
if value is not None:
result.append((key, loads(value)))
return result

def set(self, key, value):
old_value = self.get(key)
Expand Down Expand Up @@ -85,9 +88,12 @@
self._cache_value(key, value)

def mget(self, keys):
result = []
if not keys:
return
return result

Check warning on line 93 in constance/backends/redisd.py

View check run for this annotation

Codecov / codecov/patch

constance/backends/redisd.py#L93

Added line #L93 was not covered by tests

for key in keys:
value = self.get(key)
if value is not None:
yield key, value
result.append((key, value))
return result
63 changes: 38 additions & 25 deletions constance/base.py
Original file line number Diff line number Diff line change
@@ -1,32 +1,45 @@
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.")

Check warning on line 22 in constance/base.py

View check run for this annotation

Codecov / codecov/patch

constance/base.py#L22

Added line #L22 was not covered by tests
if key not in settings.CONFIG:
raise AttributeError
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.")

Check warning on line 33 in constance/base.py

View check run for this annotation

Codecov / codecov/patch

constance/base.py#L33

Added line #L33 was not covered by tests

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

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

Check warning on line 40 in constance/base.py

View check run for this annotation

Codecov / codecov/patch

constance/base.py#L40

Added line #L40 was not covered by tests

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 @@
"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 @@
"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 @@
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(

Check warning on line 85 in constance/checks.py

View check run for this annotation

Codecov / codecov/patch

constance/checks.py#L85

Added line #L85 was not covered by tests
_(
"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)

Check warning on line 93 in constance/checks.py

View check run for this annotation

Codecov / codecov/patch

constance/checks.py#L93

Added line #L93 was not covered by tests

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
Loading
Loading