Skip to content

Commit 14776c8

Browse files
committed
add environ_prefix class decorator (#240)
- allows to configure environ_prefix on a per-class basis
1 parent 3d0d421 commit 14776c8

File tree

7 files changed

+143
-8
lines changed

7 files changed

+143
-8
lines changed

configurations/__init__.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,9 @@
11
from .base import Configuration # noqa
2-
from .decorators import pristinemethod # noqa
2+
from .decorators import environ_prefix, pristinemethod # noqa
33
from .version import __version__ # noqa
44

55

6-
__all__ = ['Configuration', 'pristinemethod']
6+
__all__ = ['Configuration', 'environ_prefix', 'pristinemethod']
77

88

99
def _setup():

configurations/base.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
from django.conf import global_settings
55
from django.core.exceptions import ImproperlyConfigured
66

7-
from .utils import uppercase_attributes
7+
from .utils import uppercase_attributes, UNSET
88
from .values import Value, setup_value
99

1010
__all__ = ['Configuration']
@@ -99,6 +99,7 @@ def OTHER(self):
9999
100100
"""
101101
DOTENV_LOADED = None
102+
_environ_prefix = UNSET
102103

103104
@classmethod
104105
def load_dotenv(cls):
@@ -154,4 +155,5 @@ def post_setup(cls):
154155
def setup(cls):
155156
for name, value in uppercase_attributes(cls).items():
156157
if isinstance(value, Value):
158+
value._class_environ_prefix = cls._environ_prefix
157159
setup_value(cls, name, value)

configurations/decorators.py

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,6 @@
1+
from django.core.exceptions import ImproperlyConfigured
2+
3+
14
def pristinemethod(func):
25
"""
36
A decorator for handling pristine settings like callables.
@@ -17,3 +20,30 @@ def USER_CHECK(user):
1720
"""
1821
func.pristine = True
1922
return staticmethod(func)
23+
24+
25+
def environ_prefix(prefix):
26+
"""
27+
A class Configuration class decorator that prefixes ``prefix``
28+
to environment names.
29+
30+
Use it like this::
31+
32+
@environ_prefix("MYAPP")
33+
class Develop(Configuration):
34+
SOMETHING = values.Value()
35+
36+
To remove the prefix from environment names::
37+
38+
@environ_prefix(None)
39+
class Develop(Configuration):
40+
SOMETHING = values.Value()
41+
42+
"""
43+
if not isinstance(prefix, (type(None), str)):
44+
raise ImproperlyConfigured("environ_prefix accepts only str and None values.")
45+
46+
def decorator(conf_cls):
47+
conf_cls._environ_prefix = prefix
48+
return conf_cls
49+
return decorator

configurations/utils.py

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -99,3 +99,11 @@ def getargspec(func):
9999
if not inspect.isfunction(func):
100100
raise TypeError('%r is not a Python function' % func)
101101
return inspect.getfullargspec(func)
102+
103+
104+
class Unset:
105+
def __repr__(self): # pragma: no cover
106+
return "UNSET"
107+
108+
109+
UNSET = Unset()

configurations/values.py

Lines changed: 17 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -6,9 +6,10 @@
66

77
from django.core import validators
88
from django.core.exceptions import ValidationError, ImproperlyConfigured
9+
from django.utils.functional import cached_property
910
from django.utils.module_loading import import_string
1011

11-
from .utils import getargspec
12+
from .utils import getargspec, UNSET
1213

1314

1415
def setup_value(target, name, value):
@@ -58,16 +59,14 @@ def __new__(cls, *args, **kwargs):
5859
return instance
5960

6061
def __init__(self, default=None, environ=True, environ_name=None,
61-
environ_prefix='DJANGO', environ_required=False,
62+
environ_prefix=UNSET, environ_required=False,
6263
*args, **kwargs):
6364
if isinstance(default, Value) and default.default is not None:
6465
self.default = copy.copy(default.default)
6566
else:
6667
self.default = default
6768
self.environ = environ
68-
if environ_prefix and environ_prefix.endswith('_'):
69-
environ_prefix = environ_prefix[:-1]
70-
self.environ_prefix = environ_prefix
69+
self._environ_prefix = environ_prefix
7170
self.environ_name = environ_name
7271
self.environ_required = environ_required
7372

@@ -116,6 +115,19 @@ def to_python(self, value):
116115
"""
117116
return value
118117

118+
@cached_property
119+
def environ_prefix(self):
120+
prefix = UNSET
121+
if self._environ_prefix is not UNSET:
122+
prefix = self._environ_prefix
123+
elif (class_prefix := getattr(self, "_class_environ_prefix", UNSET)) is not UNSET:
124+
prefix = class_prefix
125+
if prefix is not UNSET:
126+
if isinstance(prefix, str) and prefix.endswith("_"):
127+
return prefix[:-1]
128+
return prefix
129+
return "DJANGO"
130+
119131

120132
class MultipleMixin:
121133
multiple = True

tests/settings/prefix_decorator.py

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
from configurations import Configuration, environ_prefix, values
2+
3+
4+
@environ_prefix("ACME")
5+
class PrefixDecoratorConf1(Configuration):
6+
FOO = values.Value()
7+
8+
9+
@environ_prefix("ACME")
10+
class PrefixDecoratorConf2(Configuration):
11+
FOO = values.BooleanValue(False)
12+
13+
14+
@environ_prefix("ACME")
15+
class PrefixDecoratorConf3(Configuration):
16+
FOO = values.Value(environ_prefix="ZEUS")
17+
18+
19+
@environ_prefix("")
20+
class PrefixDecoratorConf4(Configuration):
21+
FOO = values.Value()
22+
23+
24+
@environ_prefix(None)
25+
class PrefixDecoratorConf5(Configuration):
26+
FOO = values.Value()

tests/test_prefix_decorator.py

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
import os
2+
import importlib
3+
4+
from django.core.exceptions import ImproperlyConfigured
5+
from django.test import TestCase
6+
from unittest.mock import patch
7+
8+
from configurations import environ_prefix
9+
from tests.settings import prefix_decorator
10+
11+
12+
class EnvironPrefixDecoratorTests(TestCase):
13+
@patch.dict(os.environ, clear=True,
14+
DJANGO_CONFIGURATION="PrefixDecoratorConf1",
15+
DJANGO_SETTINGS_MODULE="tests.settings.prefix_decorator",
16+
ACME_FOO="bar")
17+
def test_prefix_decorator_with_value(self):
18+
importlib.reload(prefix_decorator)
19+
self.assertEqual(prefix_decorator.FOO, "bar")
20+
21+
@patch.dict(os.environ, clear=True,
22+
DJANGO_CONFIGURATION="PrefixDecoratorConf2",
23+
DJANGO_SETTINGS_MODULE="tests.settings.prefix_decorator",
24+
ACME_FOO="True")
25+
def test_prefix_decorator_for_value_subclasses(self):
26+
importlib.reload(prefix_decorator)
27+
self.assertIs(prefix_decorator.FOO, True)
28+
29+
@patch.dict(os.environ, clear=True,
30+
DJANGO_CONFIGURATION="PrefixDecoratorConf3",
31+
DJANGO_SETTINGS_MODULE="tests.settings.prefix_decorator",
32+
ZEUS_FOO="bar")
33+
def test_value_prefix_takes_precedence(self):
34+
importlib.reload(prefix_decorator)
35+
self.assertEqual(prefix_decorator.FOO, "bar")
36+
37+
@patch.dict(os.environ, clear=True,
38+
DJANGO_CONFIGURATION="PrefixDecoratorConf4",
39+
DJANGO_SETTINGS_MODULE="tests.settings.prefix_decorator",
40+
FOO="bar")
41+
def test_prefix_decorator_empty_string_value(self):
42+
importlib.reload(prefix_decorator)
43+
self.assertEqual(prefix_decorator.FOO, "bar")
44+
45+
@patch.dict(os.environ, clear=True,
46+
DJANGO_CONFIGURATION="PrefixDecoratorConf5",
47+
DJANGO_SETTINGS_MODULE="tests.settings.prefix_decorator",
48+
FOO="bar")
49+
def test_prefix_decorator_none_value(self):
50+
importlib.reload(prefix_decorator)
51+
self.assertEqual(prefix_decorator.FOO, "bar")
52+
53+
def test_prefix_value_must_be_none_or_str(self):
54+
class Conf:
55+
pass
56+
57+
self.assertRaises(ImproperlyConfigured, lambda: environ_prefix(1)(Conf))

0 commit comments

Comments
 (0)