diff --git a/configurations/values.py b/configurations/values.py index 467a0f9..2be89ab 100644 --- a/configurations/values.py +++ b/configurations/values.py @@ -176,7 +176,16 @@ class ListValue(Value): self.default = [] # initial conversion if self.converter is not None: - self.default = [self.converter(value) for value in self.default] + self.default = self._convert(self.default) + + def _convert(self, list_): + converted_values = [] + for value in list_: + try: + converted_values.append(self.converter(value)) + except (TypeError, ValueError): + raise ValueError(self.message.format(value, value)) + return converted_values def to_python(self, value): split_value = [v.strip() for v in value.strip().split(self.separator)] @@ -184,14 +193,7 @@ class ListValue(Value): value_list = filter(None, split_value) if self.converter is None: return list(value_list) - - converted_values = [] - for list_value in value_list: - try: - converted_values.append(self.converter(list_value)) - except (TypeError, ValueError): - raise ValueError(self.message.format(list_value, value)) - return converted_values + return self._convert(value_list) class BackendsValue(ListValue): @@ -218,6 +220,33 @@ class TupleValue(ListValue): return tuple(super(TupleValue, self).to_python(value)) +class TupleOfTuplesValue(TupleValue): + def __init__(self, *args, **kwargs): + self.tuple_separator = kwargs.pop('tuple_separator', ';') + super(TupleOfTuplesValue, self).__init__(*args, **kwargs) + + def _convert(self, items): + # This could receive either a bare tuple or tuple of tuples + if items and isinstance(items[0], tuple): + converted_tuples = [] + for inner in items: + converted = super(TupleOfTuplesValue, self)._convert(inner) + converted_tuples.append(tuple(converted)) + return tuple(converted_tuples) + return tuple(super(TupleOfTuplesValue, self)._convert(items)) + + def to_python(self, value): + split_value = [ + v.strip() for v in value.strip().split(self.tuple_separator) + ] + # Remove empty items + value_list = filter(None, split_value) + tuples = [ + super(TupleOfTuplesValue, self).to_python(v) for v in value_list + ] + return tuple(tuples) + + class SetValue(ListValue): message = 'Cannot interpret set item {0!r} in set {1!r}' diff --git a/docs/values.rst b/docs/values.rst index a107cc5..56c39fe 100644 --- a/docs/values.rst +++ b/docs/values.rst @@ -274,6 +274,27 @@ Type values See the :class:`~ListValue` examples above. +.. class:: TupleOfTuplesValue(default, [tuple_separator=';', separator=',', converter=None]) + + A :class:`~TupleValue` subclass that handles tuple of tuples values. + + :param tuple_separator: the separator to split each tuple with + :param separator: the separator to split the inner tuple contents with + :param converter: the optional converter callable to apply for each inner + tuple item + + Useful for ADMINS, MANAGERS, and the like. For example:: + + ADMINS = TupleOfTuplesValue(( + ('John', 'jcleese@site.com'), + ('Eric', 'eidle@site.com'), + )) + + Override using environment variables like this:: + + DJANGO_ADMINS=Terry,tjones@site.com;Graham,gchapman@site.com + + .. class:: SetValue A :class:`~Value` subclass that handles set values. diff --git a/tests/test_values.py b/tests/test_values.py index 2c3d5ec..d7c2239 100644 --- a/tests/test_values.py +++ b/tests/test_values.py @@ -9,8 +9,8 @@ from mock import patch from configurations.values import (Value, BooleanValue, IntegerValue, FloatValue, DecimalValue, ListValue, - TupleValue, SetValue, DictValue, - URLValue, EmailValue, IPValue, + TupleValue, TupleOfTuplesValue, SetValue, + DictValue, URLValue, EmailValue, IPValue, RegexValue, PathValue, SecretValue, DatabaseURLValue, EmailURLValue, CacheURLValue, BackendsValue, @@ -175,6 +175,37 @@ class ValueTests(TestCase): with env(DJANGO_TEST=''): self.assertEqual(value.setup('TEST'), ()) + def test_tuple_of_tuples_values_default(self): + value = TupleOfTuplesValue() + with env(DJANGO_TEST='2,3;4,5'): + expected = (('2', '3'), ('4', '5')) + self.assertEqual(value.setup('TEST'), expected) + with env(DJANGO_TEST='2;3;4;5'): + expected = (('2',), ('3',), ('4',), ('5',)) + self.assertEqual(value.setup('TEST'), expected) + with env(DJANGO_TEST='2,3,4,5'): + expected = (('2', '3', '4', '5'),) + self.assertEqual(value.setup('TEST'), expected) + with env(DJANGO_TEST='2, 3 , ; 4 , 5 ; '): + expected = (('2', '3'), ('4', '5')) + self.assertEqual(value.setup('TEST'), expected) + with env(DJANGO_TEST=''): + self.assertEqual(value.setup('TEST'), ()) + + def test_tuple_of_tuples_values_separator(self): + value = TupleOfTuplesValue(tuple_separator=':') + with env(DJANGO_TEST='2,3:4,5'): + self.assertEqual(value.setup('TEST'), (('2', '3'), ('4', '5'))) + + def test_tuple_of_tuples_values_converter(self): + value = TupleOfTuplesValue(converter=int) + with env(DJANGO_TEST='2,3;4,5'): + self.assertEqual(value.setup('TEST'), ((2, 3), (4, 5))) + + def test_tuple_of_tuples_values_converter_default(self): + value = TupleOfTuplesValue((('2', '3'), ('4', '5')), converter=int) + self.assertEqual(value.value, ((2, 3), (4, 5))) + def test_set_values_default(self): value = SetValue() with env(DJANGO_TEST='2,2'):