Skip to content
Open
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
4 changes: 2 additions & 2 deletions configurations/__init__.py
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
from .base import Configuration # noqa
from .decorators import pristinemethod # noqa
from .decorators import environ_prefix, pristinemethod # noqa
from .version import __version__ # noqa


__all__ = ['Configuration', 'pristinemethod']
__all__ = ['Configuration', 'environ_prefix', 'pristinemethod']


def _setup():
Expand Down
4 changes: 3 additions & 1 deletion configurations/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
from django.conf import global_settings
from django.core.exceptions import ImproperlyConfigured

from .utils import uppercase_attributes
from .utils import uppercase_attributes, UNSET
from .values import Value, setup_value

__all__ = ['Configuration']
Expand Down Expand Up @@ -99,6 +99,7 @@ def OTHER(self):

"""
DOTENV_LOADED = None
_environ_prefix = UNSET

@classmethod
def load_dotenv(cls):
Expand Down Expand Up @@ -154,4 +155,5 @@ def post_setup(cls):
def setup(cls):
for name, value in uppercase_attributes(cls).items():
if isinstance(value, Value):
value._class_environ_prefix = cls._environ_prefix
setup_value(cls, name, value)
30 changes: 30 additions & 0 deletions configurations/decorators.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
from django.core.exceptions import ImproperlyConfigured


def pristinemethod(func):
"""
A decorator for handling pristine settings like callables.
Expand All @@ -17,3 +20,30 @@ def USER_CHECK(user):
"""
func.pristine = True
return staticmethod(func)


def environ_prefix(prefix):
"""
A class Configuration class decorator that prefixes ``prefix``
to environment names.

Use it like this::

@environ_prefix("MYAPP")
class Develop(Configuration):
SOMETHING = values.Value()

To remove the prefix from environment names::

@environ_prefix(None)
class Develop(Configuration):
SOMETHING = values.Value()

"""
if not isinstance(prefix, (type(None), str)):
raise ImproperlyConfigured("environ_prefix accepts only str and None values.")

def decorator(conf_cls):
conf_cls._environ_prefix = prefix
return conf_cls
return decorator
8 changes: 8 additions & 0 deletions configurations/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -99,3 +99,11 @@ def getargspec(func):
if not inspect.isfunction(func):
raise TypeError('%r is not a Python function' % func)
return inspect.getfullargspec(func)


class Unset:
def __repr__(self): # pragma: no cover
return "UNSET"


UNSET = Unset()
22 changes: 17 additions & 5 deletions configurations/values.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,10 @@

from django.core import validators
from django.core.exceptions import ValidationError, ImproperlyConfigured
from django.utils.functional import cached_property
from django.utils.module_loading import import_string

from .utils import getargspec
from .utils import getargspec, UNSET


def setup_value(target, name, value):
Expand Down Expand Up @@ -58,16 +59,14 @@ def __new__(cls, *args, **kwargs):
return instance

def __init__(self, default=None, environ=True, environ_name=None,
environ_prefix='DJANGO', environ_required=False,
environ_prefix=UNSET, environ_required=False,
*args, **kwargs):
if isinstance(default, Value) and default.default is not None:
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_prefix = environ_prefix
self.environ_name = environ_name
self.environ_required = environ_required

Expand Down Expand Up @@ -116,6 +115,19 @@ def to_python(self, value):
"""
return value

@cached_property
def environ_prefix(self):
prefix = UNSET
if self._environ_prefix is not UNSET:
prefix = self._environ_prefix
elif (class_prefix := getattr(self, "_class_environ_prefix", UNSET)) is not UNSET:
prefix = class_prefix
if prefix is not UNSET:
if isinstance(prefix, str) and prefix.endswith("_"):
return prefix[:-1]
return prefix
return "DJANGO"


class MultipleMixin:
multiple = True
Expand Down
26 changes: 26 additions & 0 deletions tests/settings/prefix_decorator.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
from configurations import Configuration, environ_prefix, values


@environ_prefix("ACME")
class PrefixDecoratorConf1(Configuration):
FOO = values.Value()


@environ_prefix("ACME")
class PrefixDecoratorConf2(Configuration):
FOO = values.BooleanValue(False)


@environ_prefix("ACME")
class PrefixDecoratorConf3(Configuration):
FOO = values.Value(environ_prefix="ZEUS")


@environ_prefix("")
class PrefixDecoratorConf4(Configuration):
FOO = values.Value()


@environ_prefix(None)
class PrefixDecoratorConf5(Configuration):
FOO = values.Value()
57 changes: 57 additions & 0 deletions tests/test_prefix_decorator.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
import os
import importlib

from django.core.exceptions import ImproperlyConfigured
from django.test import TestCase
from unittest.mock import patch

from configurations import environ_prefix
from tests.settings import prefix_decorator


class EnvironPrefixDecoratorTests(TestCase):
@patch.dict(os.environ, clear=True,
DJANGO_CONFIGURATION="PrefixDecoratorConf1",
DJANGO_SETTINGS_MODULE="tests.settings.prefix_decorator",
ACME_FOO="bar")
def test_prefix_decorator_with_value(self):
importlib.reload(prefix_decorator)
self.assertEqual(prefix_decorator.FOO, "bar")

@patch.dict(os.environ, clear=True,
DJANGO_CONFIGURATION="PrefixDecoratorConf2",
DJANGO_SETTINGS_MODULE="tests.settings.prefix_decorator",
ACME_FOO="True")
def test_prefix_decorator_for_value_subclasses(self):
importlib.reload(prefix_decorator)
self.assertIs(prefix_decorator.FOO, True)

@patch.dict(os.environ, clear=True,
DJANGO_CONFIGURATION="PrefixDecoratorConf3",
DJANGO_SETTINGS_MODULE="tests.settings.prefix_decorator",
ZEUS_FOO="bar")
def test_value_prefix_takes_precedence(self):
importlib.reload(prefix_decorator)
self.assertEqual(prefix_decorator.FOO, "bar")

@patch.dict(os.environ, clear=True,
DJANGO_CONFIGURATION="PrefixDecoratorConf4",
DJANGO_SETTINGS_MODULE="tests.settings.prefix_decorator",
FOO="bar")
def test_prefix_decorator_empty_string_value(self):
importlib.reload(prefix_decorator)
self.assertEqual(prefix_decorator.FOO, "bar")

@patch.dict(os.environ, clear=True,
DJANGO_CONFIGURATION="PrefixDecoratorConf5",
DJANGO_SETTINGS_MODULE="tests.settings.prefix_decorator",
FOO="bar")
def test_prefix_decorator_none_value(self):
importlib.reload(prefix_decorator)
self.assertEqual(prefix_decorator.FOO, "bar")

def test_prefix_value_must_be_none_or_str(self):
class Conf:
pass

self.assertRaises(ImproperlyConfigured, lambda: environ_prefix(1)(Conf))
Loading