This commit is contained in:
Finn-Thorben Sell 2022-03-24 16:07:48 +00:00 committed by GitHub
commit c23bea499e
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
13 changed files with 412 additions and 81 deletions

View file

@ -1,8 +1,11 @@
from . import importer
from .errors import with_error_handler
importer.install()
from django.core.asgi import get_asgi_application # noqa: E402
from django.core.asgi import get_asgi_application as dj_get_asgi_application # noqa: E402
get_asgi_application = with_error_handler(dj_get_asgi_application)
# this is just for the crazy ones
application = get_asgi_application()

View file

@ -4,6 +4,7 @@ import re
from django.conf import global_settings
from django.core.exceptions import ImproperlyConfigured
from .errors import ConfigurationError, SetupError
from .utils import uppercase_attributes
from .values import Value, setup_value
@ -142,6 +143,13 @@ class Configuration(metaclass=ConfigurationBase):
@classmethod
def setup(cls):
exceptions = []
for name, value in uppercase_attributes(cls).items():
if isinstance(value, Value):
setup_value(cls, name, value)
try:
setup_value(cls, name, value)
except ConfigurationError as err:
exceptions.append(err)
if len(exceptions) > 0:
raise SetupError(f"Couldn't setup values of configuration {cls.__name__}", exceptions)

132
configurations/errors.py Normal file
View file

@ -0,0 +1,132 @@
from typing import TYPE_CHECKING, List, Callable
from functools import wraps
import sys
import os
if TYPE_CHECKING:
from .values import Value # pragma: no cover
class TermStyles:
BOLD = "\033[1m" if os.isatty(sys.stderr.fileno()) else ""
RED = "\033[91m" if os.isatty(sys.stderr.fileno()) else ""
END = "\033[0m" if os.isatty(sys.stderr.fileno()) else ""
def extract_explanation_lines_from_value(value_instance: 'Value') -> List[str]:
result = []
if value_instance.help_text is not None:
result.append(f"Help: {value_instance.help_text}")
if value_instance.help_reference is not None:
result.append(f"Reference: {value_instance.help_reference}")
if value_instance.destination_name is not None:
result.append(f"{value_instance.destination_name} is taken from the environment variable "
f"{value_instance.full_environ_name} as a {type(value_instance).__name__}")
if value_instance.example_generator is not None:
result.append(f"Example value: '{value_instance.example_generator()}'")
return result
class SetupError(Exception):
"""
Exception that gets raised when a configuration class cannot be set up by the importer
"""
def __init__(self, msg: str, child_errors: List['ConfigurationError'] = None) -> None:
"""
:param step_verb: Which step the importer tried to perform (e.g. import, setup)
:param configuration_path: The full module path of the configuration that was supposed to be set up
:param child_errors: Optional child configuration errors that caused this error
"""
super().__init__(msg)
self.child_errors = child_errors or []
class ConfigurationError(ValueError):
"""
Base error class that is used to indicate that something went wrong during configuration.
This error type (and subclasses) is caught and pretty-printed by django-configurations so that an end-user does not
see an unwieldy traceback but instead a helpful error message.
"""
def __init__(self, main_error_msg: str, explanation_lines: List[str]) -> None:
"""
:param main_error_msg: Main message that describes the error.
This will be displayed before all *explanation_lines* and in the traceback (although tracebacks are normally
not rendered)
:param explanation_lines: Additional lines of explanations which further describe the error or give hints on
how to fix it.
"""
super().__init__(main_error_msg)
self.main_error_msg = main_error_msg
self.explanation_lines = explanation_lines
class ValueRetrievalError(ConfigurationError):
"""
Exception that is raised when errors occur during the retrieval of a Value by one of the `Value` classes.
This can happen when the environment variable corresponding to the value is not defined.
"""
def __init__(self, value_instance: "Value", *extra_explanation_lines: str):
"""
:param value_instance: The `Value` instance which caused the generation of this error
:param extra_explanation_lines: Extra lines that will be appended to `ConfigurationError.explanation_lines`
in addition the ones automatically generated from the provided *value_instance*.
"""
super().__init__(
f"Value of {value_instance.destination_name} could not be retrieved from environment",
list(extra_explanation_lines) + extract_explanation_lines_from_value(value_instance)
)
class ValueProcessingError(ConfigurationError):
"""
Exception that is raised when a dynamic Value failed to be processed by one of the `Value` classes after retrieval.
Processing could be i.e. converting from string to a native datatype or validation.
"""
def __init__(self, value_instance: "Value", raw_value: str, *extra_explanation_lines: str):
"""
:param value_instance: The `Value` instance which caused the generation of this error
:param raw_value: The raw value that was retrieved from the environment and which could not be processed further
:param extra_explanation_lines: Extra lines that will be prepended to `ConfigurationError.explanation_lines`
in addition the ones automatically generated from the provided *value_instance*.
"""
error = f"{value_instance.destination_name} was given an invalid value"
if hasattr(value_instance, "message"):
error += ": " + value_instance.message.format(raw_value)
explanation_lines = list(extra_explanation_lines) + extract_explanation_lines_from_value(value_instance)
explanation_lines.append(f"'{raw_value}' was received but that is invalid")
super().__init__(error, explanation_lines)
def with_error_handler(callee: Callable) -> Callable:
"""
A decorator which is designed to wrap django entry points with an error handler so that django-configuration
originated errors can be caught and rendered to the user in a readable format.
"""
@wraps(callee)
def wrapper(*args, **kwargs):
try:
return callee(*args, **kwargs)
except SetupError as e:
msg = f"{str(e)}"
for child_error in e.child_errors:
msg += f"\n * {child_error.main_error_msg}"
for explanation_line in child_error.explanation_lines:
msg += f"\n - {explanation_line}"
print(msg, file=sys.stderr)
return wrapper

View file

@ -0,0 +1,59 @@
from typing import Callable
import secrets
import base64
import django
from django.core.management.utils import get_random_secret_key
from django.utils.crypto import get_random_string
if django.VERSION[0] > 3 or \
(django.VERSION[0] == 3 and django.VERSION[1] >= 2):
# RANDOM_STRING_CHARS was only introduced in django 3.2
from django.utils.crypto import RANDOM_STRING_CHARS
else:
RANDOM_STRING_CHARS = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789" # pragma: no cover
def gen_django_secret_key() -> str:
"""
Generate a cryptographically secure random string that can safely be used as a SECRET_KEY in django
"""
return get_random_secret_key()
def gen_random_string(length: int, allowed_chars: str = RANDOM_STRING_CHARS) -> Callable[[], str]:
"""
Create a parameterized generator which generates a cryptographically secure random string of the given length
containing the given characters.
"""
def _gen_random_string() -> str:
return get_random_string(length, allowed_chars)
return _gen_random_string
def gen_bytes(length: int, encoding: str) -> Callable[[], str]:
"""
Create a parameterized generator which generates a cryptographically secure random assortments of bytes of the given
length and encoded in the given format
:param length: How many bytes should be generated. Not how long the encoded string will be.
:param encoding: How the generated bytes should be encoded.
Accepted values are "base64", "base64_urlsafe" and "hex" (case is ignored)
"""
encoding = encoding.lower()
if encoding not in ("base64", "base64_urlsafe", "hex"):
raise ValueError(f"Cannot gen_bytes with encoding '{encoding}'. Valid encodings are 'base64', 'base64_urlsafe'"
f" and 'hex'")
def _gen_bytes() -> str:
b = secrets.token_bytes(length)
if encoding == "base64":
return base64.standard_b64encode(b).decode("ASCII")
elif encoding == "base64_urlsafe":
return base64.urlsafe_b64encode(b).decode("ASCII")
elif encoding == "hex":
return b.hex().upper()
return _gen_bytes

View file

@ -1,5 +1,8 @@
from . import importer
from .errors import with_error_handler
importer.install()
from django.core.servers.fastcgi import runfastcgi # noqa
from django.core.servers.fastcgi import dj_runfastcgi # noqa
runfastcgi = with_error_handler(dj_runfastcgi)

View file

@ -8,6 +8,7 @@ from django.conf import ENVIRONMENT_VARIABLE as SETTINGS_ENVIRONMENT_VARIABLE
from django.core.exceptions import ImproperlyConfigured
from django.core.management import base
from .errors import SetupError, ConfigurationError
from .utils import uppercase_attributes, reraise
from .values import Value, setup_value
@ -149,10 +150,10 @@ class ConfigurationLoader:
try:
cls = getattr(mod, self.name)
except AttributeError as err: # pragma: no cover
reraise(err, "Couldn't find configuration '{0}' "
"in module '{1}'".format(self.name,
mod.__package__))
except AttributeError: # pragma: no cover
raise SetupError(f"Couldn't find configuration '{self.name}' in module {mod.__package__}.\n"
f"Hint: '{self.name}' is taken from the environment variable '{CONFIGURATION_ENVIRONMENT_VARIABLE}'"
f"and '{mod.__package__}' from the environment variable '{SETTINGS_ENVIRONMENT_VARIABLE}'.")
try:
cls.pre_setup()
cls.setup()
@ -172,6 +173,10 @@ class ConfigurationLoader:
self.name))
cls.post_setup()
except SetupError:
raise
except ConfigurationError as err:
raise SetupError(f"Couldn't setup configuration '{cls_path}'", [err])
except Exception as err:
reraise(err, "Couldn't setup configuration '{0}'".format(cls_path))

View file

@ -1,6 +1,10 @@
from . import importer
from .errors import with_error_handler
importer.install(check_options=True)
from django.core.management import (execute_from_command_line, # noqa
call_command)
from django.core.management import (execute_from_command_line as dj_execute_from_command_line, # noqa
call_command as dj_call_command)
execute_from_command_line = with_error_handler(dj_execute_from_command_line)
call_command = with_error_handler(dj_call_command)

View file

@ -2,12 +2,12 @@ import ast
import copy
import decimal
import os
import sys
from django.core import validators
from django.core.exceptions import ValidationError, ImproperlyConfigured
from django.utils.module_loading import import_string
from .errors import ValueRetrievalError, ValueProcessingError
from .utils import getargspec
@ -58,8 +58,8 @@ class Value:
return instance
def __init__(self, default=None, environ=True, environ_name=None,
environ_prefix='DJANGO', environ_required=False,
*args, **kwargs):
environ_prefix='DJANGO', environ_required=False, example_generator=None,
help_text=None, help_reference=None, *args, **kwargs):
if isinstance(default, Value) and default.default is not None:
self.default = copy.copy(default.default)
else:
@ -70,6 +70,10 @@ class Value:
self.environ_prefix = environ_prefix
self.environ_name = environ_name
self.environ_required = environ_required
self.destination_name = None
self.help_text = help_text
self.help_reference = help_reference
self.example_generator = example_generator
def __str__(self):
return str(self.value)
@ -86,33 +90,46 @@ class Value:
# Compatibility with python 2
__nonzero__ = __bool__
def full_environ_name(self, name):
@property
def full_environ_name(self):
"""
The full name of the environment variable (including prefix and capitalization) from which this value should be
retrieved
"""
if self.environ_name:
environ_name = self.environ_name
else:
environ_name = name.upper()
environ_name = self.destination_name.upper()
if self.environ_prefix:
environ_name = '{0}_{1}'.format(self.environ_prefix, environ_name)
return environ_name
def setup(self, name):
"""
Set up this value instance by retrieving the configured value from the environment and converting it to a native
python data type
:param name: Destination name for which this value is used.
For example in the scenario of `DEBUG = Value()` in a `Configuration` subclass, this would be `DEBUG`.
"""
self.destination_name = name
value = self.default
if self.environ:
full_environ_name = self.full_environ_name(name)
full_environ_name = self.full_environ_name
if full_environ_name in os.environ:
value = self.to_python(os.environ[full_environ_name])
elif self.environ_required:
raise ValueError('Value {0!r} is required to be set as the '
'environment variable {1!r}'
.format(name, full_environ_name))
raise ValueRetrievalError(self)
self.value = value
return value
def to_python(self, value):
def to_python(self, value: str):
"""
Convert the given value of a environment variable into an
Convert the given value of an environment variable into an
appropriate Python representation of the value.
This should be overriden when subclassing.
This should be overridden when subclassing.
:param value: The value that should be converted to a python representation
"""
return value
@ -127,19 +144,18 @@ class BooleanValue(Value):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
if self.default not in (True, False):
raise ValueError('Default value {0!r} is not a '
'boolean value'.format(self.default))
if not self.environ_required and self.default not in (True, False):
raise ImproperlyConfigured('Default value {0!r} is not a '
'boolean value'.format(self.default))
def to_python(self, value):
def to_python(self, value: str):
normalized_value = value.strip().lower()
if normalized_value in self.true_values:
return True
elif normalized_value in self.false_values:
return False
else:
raise ValueError('Cannot interpret '
'boolean value {0!r}'.format(value))
raise ValueProcessingError(self, value)
class CastingMixin:
@ -159,21 +175,21 @@ class CastingMixin:
else:
error = 'Cannot use caster of {0} ({1!r})'.format(self,
self.caster)
raise ValueError(error)
raise ImproperlyConfigured(error)
try:
arg_names = getargspec(self._caster)[0]
self._params = {name: kwargs[name] for name in arg_names if name in kwargs}
except TypeError:
self._params = {}
def to_python(self, value):
def to_python(self, value: str):
try:
if self._params:
return self._caster(value, **self._params)
else:
return self._caster(value)
except self.exception:
raise ValueError(self.message.format(value))
raise ValueProcessingError(self, value)
class IntegerValue(CastingMixin, Value):
@ -182,10 +198,10 @@ class IntegerValue(CastingMixin, Value):
class PositiveIntegerValue(IntegerValue):
def to_python(self, value):
def to_python(self, value: str):
int_value = super().to_python(value)
if int_value < 0:
raise ValueError(self.message.format(value))
raise ValueProcessingError(self, value, f"Value needs to be positive or zero but {int_value} isn't")
return int_value
@ -209,7 +225,7 @@ class SequenceValue(Value):
converter = None
def __init__(self, *args, **kwargs):
msg = 'Cannot interpret {0} item {{0!r}} in {0} {{1!r}}'
msg = 'Cannot interpret {0} item in {0} {{0!r}}'
self.message = msg.format(self.sequence_type.__name__)
self.separator = kwargs.pop('separator', ',')
converter = kwargs.pop('converter', None)
@ -231,10 +247,10 @@ class SequenceValue(Value):
try:
converted_values.append(self.converter(value))
except (TypeError, ValueError):
raise ValueError(self.message.format(value, value))
raise ValueProcessingError(self, self.separator.join(sequence))
return self.sequence_type(converted_values)
def to_python(self, value):
def to_python(self, value: str):
split_value = [v.strip() for v in value.strip().split(self.separator)]
# removing empty items
value_list = self.sequence_type(filter(None, split_value))
@ -270,7 +286,7 @@ class SingleNestedSequenceValue(SequenceValue):
return self.sequence_type(converted_sequences)
return self.sequence_type(super()._convert(items))
def to_python(self, value):
def to_python(self, value: str):
split_value = [
v.strip() for v in value.strip().split(self.seq_separator)
]
@ -296,13 +312,11 @@ class BackendsValue(ListValue):
try:
import_string(value)
except ImportError as err:
raise ValueError(err).with_traceback(sys.exc_info()[2])
raise ValueProcessingError(self, value) from err
return value
class SetValue(ListValue):
message = 'Cannot interpret set item {0!r} in set {1!r}'
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
if self.default is None:
@ -310,7 +324,7 @@ class SetValue(ListValue):
else:
self.default = set(self.default)
def to_python(self, value):
def to_python(self, value: str):
return set(super().to_python(value))
@ -324,16 +338,16 @@ class DictValue(Value):
else:
self.default = dict(self.default)
def to_python(self, value):
def to_python(self, value: str):
value = super().to_python(value)
if not value:
return {}
try:
evaled_value = ast.literal_eval(value)
except ValueError:
raise ValueError(self.message.format(value))
raise ValueProcessingError(self, value)
if not isinstance(evaled_value, dict):
raise ValueError(self.message.format(value))
raise ValueProcessingError(self, value)
return evaled_value
@ -350,16 +364,19 @@ class ValidationMixin:
elif callable(self.validator):
self._validator = self.validator
else:
raise ValueError('Cannot use validator of '
'{0} ({1!r})'.format(self, self.validator))
raise ImproperlyConfigured('Cannot use validator of '
'{0} ({1!r})'.format(self, self.validator))
if self.default:
self.to_python(self.default)
try:
self.to_python(self.default)
except ValueProcessingError as e:
raise ImproperlyConfigured(e.main_error_msg) from e
def to_python(self, value):
def to_python(self, value: str):
try:
self._validator(value)
except ValidationError:
raise ValueError(self.message.format(value))
except ValidationError as e:
raise ValueProcessingError(self, value, f"Validation failed: {e.message}")
else:
return value
@ -397,7 +414,7 @@ class PathValue(Value):
value = super().setup(name)
value = os.path.expanduser(value)
if self.check_exists and not os.path.exists(value):
raise ValueError('Path {0!r} does not exist.'.format(value))
raise ValueProcessingError(self, value, f"Path {value} does not exist")
return os.path.abspath(value)
@ -408,13 +425,13 @@ class SecretValue(Value):
kwargs['environ_required'] = True
super().__init__(*args, **kwargs)
if self.default is not None:
raise ValueError('Secret values are only allowed to '
'be set as environment variables')
raise ImproperlyConfigured('Secret values are only allowed to '
'be set as environment variables')
def setup(self, name):
value = super().setup(name)
if not value:
raise ValueError('Secret value {0!r} is not set'.format(name))
raise ValueRetrievalError(self)
return value
@ -448,7 +465,7 @@ class DictBackendMixin(Value):
else:
self.default = self.to_python(self.default)
def to_python(self, value):
def to_python(self, value: str):
value = super().to_python(value)
return {self.alias: value}

View file

@ -1,8 +1,11 @@
from . import importer
from .errors import with_error_handler
importer.install()
from django.core.wsgi import get_wsgi_application # noqa: E402
from django.core.wsgi import get_wsgi_application as dj_get_wsgi_application # noqa: E402
get_wsgi_application = with_error_handler(dj_get_wsgi_application)
# this is just for the crazy ones
application = get_wsgi_application()

View file

@ -4,7 +4,8 @@ from configurations import Configuration, values
class Base(Configuration):
# Django settings for test_project project.
DEBUG = values.BooleanValue(True, environ=True)
DEBUG = values.BooleanValue(True, environ=True, help_text="Enables or disables django debug mode",
help_reference="https://docs.djangoproject.com/en/dev/ref/settings/#debug")
ADMINS = (
# ('Your Name', 'your_email@example.com'),

View file

@ -0,0 +1,43 @@
import io
from unittest.mock import patch
from django.test import TestCase
from configurations.errors import ValueRetrievalError, SetupError, with_error_handler
from configurations.values import Value
class ErrorHandlingTestCase(TestCase):
def test_help_text_in_explanation_lines(self):
value_instance = Value(help_text="THIS IS A TEST")
exception = ValueRetrievalError(value_instance)
self.assertIn("Help: THIS IS A TEST", exception.explanation_lines)
def test_help_reference_in_explanation_lines(self):
value_instance = Value(help_reference="https://example.com")
exception = ValueRetrievalError(value_instance)
self.assertIn("Reference: https://example.com", exception.explanation_lines)
def test_example_in_explanation_lines(self):
value_instance = Value(example_generator=lambda: "test")
exception = ValueRetrievalError(value_instance)
self.assertIn("Example value: 'test'", exception.explanation_lines)
def test_error_handler_rendering(self):
# setup
with patch("configurations.errors.sys.stderr", new=io.StringIO()) as mock:
def inner():
try:
value_instance = Value(environ_required=True)
value_instance.setup("TEST")
except ValueRetrievalError as err:
raise SetupError("This is a test exception", [err])
# execution
with_error_handler(inner)()
# verification
self.assertEqual(mock.getvalue().strip(),
"This is a test exception\n"
" * Value of TEST could not be retrieved from environment\n"
" - TEST is taken from the environment variable DJANGO_TEST as a Value")

View file

@ -0,0 +1,35 @@
import base64
from django.test import TestCase
from configurations.example_generators import gen_bytes, gen_random_string, gen_django_secret_key
class ExampleGeneratorsTestCase(TestCase):
def test_generators_dont_raise_exceptions(self):
for gen in [gen_bytes(64, "hex"), gen_bytes(64, "base64"), gen_bytes(64, "base64_urlsafe"),
gen_random_string(16, "ab"), gen_random_string(5),
gen_django_secret_key]:
with self.subTest(gen.__name__):
gen()
# gen_django_secret_key() and gen_random_string() are not tested beyond the above general test case
# because they are just wrappers around existing django utilities.
# They are thus assumed to work.
def test_gen_bytes(self):
with self.subTest("base64"):
result = gen_bytes(64, "base64")()
b = base64.standard_b64decode(result.encode("ASCII"))
self.assertEqual(len(b), 64)
with self.subTest("base64_urlsafe"):
result = gen_bytes(64, "base64_urlsafe")()
b = base64.urlsafe_b64decode(result.encode("ASCII"))
self.assertEqual(len(b), 64)
with self.subTest("hex"):
result = gen_bytes(64, "hex")()
b = bytes.fromhex(result)
self.assertEqual(len(b), 64)
with self.subTest("invalid"):
self.assertRaises(ValueError, gen_bytes, 64, "invalid")

View file

@ -2,6 +2,7 @@ import decimal
import os
from contextlib import contextmanager
from django import VERSION as DJANGO_VERSION
from django.test import TestCase
from django.core.exceptions import ImproperlyConfigured
@ -15,8 +16,9 @@ from configurations.values import (Value, BooleanValue, IntegerValue,
RegexValue, PathValue, SecretValue,
DatabaseURLValue, EmailURLValue,
CacheURLValue, BackendsValue,
CastingMixin, SearchURLValue,
setup_value, PositiveIntegerValue)
SearchURLValue, PositiveIntegerValue, CastingMixin,
setup_value)
from configurations.errors import ValueRetrievalError, ValueProcessingError
@contextmanager
@ -58,6 +60,19 @@ class ValueTests(TestCase):
self.assertEqual(repr(value), repr('override'))
def test_environ_required(self):
for ValueClass in (Value, BooleanValue, IntegerValue,
FloatValue, DecimalValue, ListValue,
TupleValue, SingleNestedTupleValue,
SingleNestedListValue, SetValue,
DictValue, URLValue, EmailValue, IPValue,
RegexValue, PathValue, SecretValue,
DatabaseURLValue, EmailURLValue,
CacheURLValue, BackendsValue,
SearchURLValue, PositiveIntegerValue):
value = ValueClass(environ_required=True)
self.assertRaises(ValueRetrievalError, value.setup, "TEST")
def test_value_truthy(self):
value = Value('default')
self.assertTrue(bool(value))
@ -110,7 +125,7 @@ class ValueTests(TestCase):
self.assertTrue(bool(value.setup('TEST')))
def test_boolean_values_faulty(self):
self.assertRaises(ValueError, BooleanValue, 'false')
self.assertRaises(ImproperlyConfigured, BooleanValue, 'false')
def test_boolean_values_false(self):
value = BooleanValue(True)
@ -121,7 +136,7 @@ class ValueTests(TestCase):
def test_boolean_values_nonboolean(self):
value = BooleanValue(True)
with env(DJANGO_TEST='nonboolean'):
self.assertRaises(ValueError, value.setup, 'TEST')
self.assertRaises(ValueProcessingError, value.setup, 'TEST')
def test_boolean_values_assign_false_to_another_booleanvalue(self):
value1 = BooleanValue(False)
@ -134,30 +149,30 @@ class ValueTests(TestCase):
with env(DJANGO_TEST='2'):
self.assertEqual(value.setup('TEST'), 2)
with env(DJANGO_TEST='noninteger'):
self.assertRaises(ValueError, value.setup, 'TEST')
self.assertRaises(ValueProcessingError, value.setup, 'TEST')
def test_positive_integer_values(self):
value = PositiveIntegerValue(1)
with env(DJANGO_TEST='2'):
self.assertEqual(value.setup('TEST'), 2)
with env(DJANGO_TEST='noninteger'):
self.assertRaises(ValueError, value.setup, 'TEST')
self.assertRaises(ValueProcessingError, value.setup, 'TEST')
with env(DJANGO_TEST='-1'):
self.assertRaises(ValueError, value.setup, 'TEST')
self.assertRaises(ValueProcessingError, value.setup, 'TEST')
def test_float_values(self):
value = FloatValue(1.0)
with env(DJANGO_TEST='2.0'):
self.assertEqual(value.setup('TEST'), 2.0)
with env(DJANGO_TEST='noninteger'):
self.assertRaises(ValueError, value.setup, 'TEST')
self.assertRaises(ValueProcessingError, value.setup, 'TEST')
def test_decimal_values(self):
value = DecimalValue(decimal.Decimal(1))
with env(DJANGO_TEST='2'):
self.assertEqual(value.setup('TEST'), decimal.Decimal(2))
with env(DJANGO_TEST='nondecimal'):
self.assertRaises(ValueError, value.setup, 'TEST')
self.assertRaises(ValueProcessingError, value.setup, 'TEST')
def test_failing_caster(self):
self.assertRaises(ImproperlyConfigured, FailingCasterValue)
@ -194,7 +209,7 @@ class ValueTests(TestCase):
def test_list_values_converter_exception(self):
value = ListValue(converter=int)
with env(DJANGO_TEST='2,b'):
self.assertRaises(ValueError, value.setup, 'TEST')
self.assertRaises(ValueProcessingError, value.setup, 'TEST')
def test_tuple_values_default(self):
value = TupleValue()
@ -292,21 +307,21 @@ class ValueTests(TestCase):
with env(DJANGO_TEST=''):
self.assertEqual(value.setup('TEST'), {})
with env(DJANGO_TEST='spam'):
self.assertRaises(ValueError, value.setup, 'TEST')
self.assertRaises(ValueProcessingError, value.setup, 'TEST')
def test_email_values(self):
value = EmailValue('spam@eg.gs')
with env(DJANGO_TEST='spam@sp.am'):
self.assertEqual(value.setup('TEST'), 'spam@sp.am')
with env(DJANGO_TEST='spam'):
self.assertRaises(ValueError, value.setup, 'TEST')
self.assertRaises(ValueProcessingError, value.setup, 'TEST')
def test_url_values(self):
value = URLValue('http://eggs.spam')
with env(DJANGO_TEST='http://spam.eggs'):
self.assertEqual(value.setup('TEST'), 'http://spam.eggs')
with env(DJANGO_TEST='httb://spam.eggs'):
self.assertRaises(ValueError, value.setup, 'TEST')
self.assertRaises(ValueProcessingError, value.setup, 'TEST')
def test_url_values_with_no_default(self):
value = URLValue() # no default
@ -314,7 +329,7 @@ class ValueTests(TestCase):
self.assertEqual(value.setup('TEST'), 'http://spam.eggs')
def test_url_values_with_wrong_default(self):
self.assertRaises(ValueError, URLValue, 'httb://spam.eggs')
self.assertRaises(ImproperlyConfigured, URLValue, 'httb://spam.eggs')
def test_ip_values(self):
value = IPValue('0.0.0.0')
@ -323,14 +338,14 @@ class ValueTests(TestCase):
with env(DJANGO_TEST='::1'):
self.assertEqual(value.setup('TEST'), '::1')
with env(DJANGO_TEST='spam.eggs'):
self.assertRaises(ValueError, value.setup, 'TEST')
self.assertRaises(ValueProcessingError, value.setup, 'TEST')
def test_regex_values(self):
value = RegexValue('000--000', regex=r'\d+--\d+')
with env(DJANGO_TEST='123--456'):
self.assertEqual(value.setup('TEST'), '123--456')
with env(DJANGO_TEST='123456'):
self.assertRaises(ValueError, value.setup, 'TEST')
self.assertRaises(ValueProcessingError, value.setup, 'TEST')
def test_path_values_with_check(self):
value = PathValue()
@ -339,7 +354,7 @@ class ValueTests(TestCase):
with env(DJANGO_TEST='~/'):
self.assertEqual(value.setup('TEST'), os.path.expanduser('~'))
with env(DJANGO_TEST='/does/not/exist'):
self.assertRaises(ValueError, value.setup, 'TEST')
self.assertRaises(ValueProcessingError, value.setup, 'TEST')
def test_path_values_no_check(self):
value = PathValue(check_exists=False)
@ -354,17 +369,17 @@ class ValueTests(TestCase):
def test_secret_value(self):
# no default allowed, only environment values are
self.assertRaises(ValueError, SecretValue, 'default')
self.assertRaises(ImproperlyConfigured, SecretValue, 'default')
value = SecretValue()
self.assertRaises(ValueError, value.setup, 'TEST')
self.assertRaises(ValueRetrievalError, value.setup, 'TEST')
with env(DJANGO_SECRET_KEY='123'):
self.assertEqual(value.setup('SECRET_KEY'), '123')
value = SecretValue(environ_name='FACEBOOK_API_SECRET',
environ_prefix=None,
late_binding=True)
self.assertRaises(ValueError, value.setup, 'TEST')
self.assertRaises(ValueRetrievalError, value.setup, 'TEST')
with env(FACEBOOK_API_SECRET='123'):
self.assertEqual(value.setup('TEST'), '123')
@ -411,6 +426,7 @@ class ValueTests(TestCase):
'EMAIL_HOST_PASSWORD': 'password',
'EMAIL_HOST_USER': 'user@domain.com',
'EMAIL_PORT': 587,
'EMAIL_TIMEOUT': None,
'EMAIL_USE_SSL': False,
'EMAIL_USE_TLS': True})
with env(EMAIL_URL='console://'):
@ -421,15 +437,16 @@ class ValueTests(TestCase):
'EMAIL_HOST_PASSWORD': None,
'EMAIL_HOST_USER': None,
'EMAIL_PORT': None,
'EMAIL_TIMEOUT': None,
'EMAIL_USE_SSL': False,
'EMAIL_USE_TLS': False})
with env(EMAIL_URL='smtps://user@domain.com:password@smtp.example.com:wrong'): # noqa: E501
self.assertRaises(ValueError, value.setup, 'TEST')
self.assertRaises(ValueProcessingError, value.setup, 'TEST')
def test_cache_url_value(self):
cache_setting = {
'default': {
'BACKEND': 'django_redis.cache.RedisCache',
'BACKEND': 'django_redis.cache.RedisCache' if DJANGO_VERSION[0] < 4 else 'django.core.cache.backends.redis.RedisCache',
'LOCATION': 'redis://host:6379/1',
}
}
@ -445,11 +462,11 @@ class ValueTests(TestCase):
value.setup('TEST')
self.assertEqual(cm.exception.args[0], 'Unknown backend: "wrong"')
with env(CACHE_URL='redis://user@host:port/1'):
with self.assertRaises(ValueError) as cm:
with self.assertRaises(ValueProcessingError) as cm:
value.setup('TEST')
self.assertEqual(
cm.exception.args[0],
"Cannot interpret cache URL value 'redis://user@host:port/1'")
"TEST was given an invalid value: Cannot interpret cache URL value 'redis://user@host:port/1'")
def test_search_url_value(self):
value = SearchURLValue()
@ -468,7 +485,7 @@ class ValueTests(TestCase):
self.assertEqual(value.setup('TEST'), backends)
backends = ['non.existing.Backend']
self.assertRaises(ValueError, BackendsValue, backends)
self.assertRaises(ValueProcessingError, BackendsValue, backends)
def test_tuple_value(self):
value = TupleValue(None)
@ -503,6 +520,7 @@ class ValueTests(TestCase):
'EMAIL_HOST_PASSWORD': 'password',
'EMAIL_HOST_USER': 'user@domain.com',
'EMAIL_PORT': 587,
'EMAIL_TIMEOUT': None,
'EMAIL_USE_SSL': False,
'EMAIL_USE_TLS': True
})