add environ_prefix class decorator (#240)

- allows to configure environ_prefix on a per-class basis
This commit is contained in:
Giannis Terzopoulos 2024-12-02 12:29:31 +01:00
parent 3d0d4216ca
commit 14776c83ac
7 changed files with 143 additions and 8 deletions

View file

@ -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():

View file

@ -4,7 +4,7 @@ import re
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']
@ -99,6 +99,7 @@ class Configuration(metaclass=ConfigurationBase):
"""
DOTENV_LOADED = None
_environ_prefix = UNSET
@classmethod
def load_dotenv(cls):
@ -154,4 +155,5 @@ class Configuration(metaclass=ConfigurationBase):
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)

View file

@ -1,3 +1,6 @@
from django.core.exceptions import ImproperlyConfigured
def pristinemethod(func):
"""
A decorator for handling pristine settings like callables.
@ -17,3 +20,30 @@ def pristinemethod(func):
"""
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

View file

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

View file

@ -6,9 +6,10 @@ import sys
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):
@ -58,16 +59,14 @@ class Value:
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
@ -116,6 +115,19 @@ class 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

View file

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

View file

@ -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))