diff --git a/configurations/values.py b/configurations/values.py index 2be89ab..0ff6775 100644 --- a/configurations/values.py +++ b/configurations/values.py @@ -161,39 +161,97 @@ class DecimalValue(CastingMixin, Value): exception = decimal.InvalidOperation -class ListValue(Value): +class SequenceValue(Value): + """ + Common code for sequence-type values (lists and tuples). + Do not use this class directly. Instead use a subclass. + """ + + # Specify this value in subclasses, e.g. with 'list' or 'tuple' + sequence_type = None converter = None - message = 'Cannot interpret list item {0!r} in list {1!r}' def __init__(self, *args, **kwargs): + msg = 'Cannot interpret {0} item {{0!r}} in {0} {{1!r}}' + self.message = msg.format(self.sequence_type.__name__) self.separator = kwargs.pop('separator', ',') converter = kwargs.pop('converter', None) if converter is not None: self.converter = converter - super(ListValue, self).__init__(*args, **kwargs) - # make sure the default is a list + super(SequenceValue, self).__init__(*args, **kwargs) + # make sure the default is the correct sequence type if self.default is None: - self.default = [] + self.default = self.sequence_type() + else: + self.default = self.sequence_type(self.default) # initial conversion if self.converter is not None: self.default = self._convert(self.default) - def _convert(self, list_): + def _convert(self, sequence): converted_values = [] - for value in list_: + for value in sequence: try: converted_values.append(self.converter(value)) except (TypeError, ValueError): raise ValueError(self.message.format(value, value)) - return converted_values + return self.sequence_type(converted_values) def to_python(self, value): split_value = [v.strip() for v in value.strip().split(self.separator)] # removing empty items value_list = filter(None, split_value) - if self.converter is None: - return list(value_list) - return self._convert(value_list) + if self.converter is not None: + value_list = self._convert(value_list) + return self.sequence_type(value_list) + + +class ListValue(SequenceValue): + sequence_type = list + + +class TupleValue(SequenceValue): + sequence_type = tuple + + +class SingleNestedSequenceValue(SequenceValue): + """ + Common code for nested sequences (list of lists, or tuple of tuples). + Do not use this class directly. Instead use a subclass. + """ + + def __init__(self, *args, **kwargs): + self.seq_separator = kwargs.pop('seq_separator', ';') + super(SingleNestedSequenceValue, self).__init__(*args, **kwargs) + + def _convert(self, items): + # This could receive either a bare or nested sequence + if items and isinstance(items[0], self.sequence_type): + converted_sequences = [ + super(SingleNestedSequenceValue, self)._convert(i) for i in items + ] + return self.sequence_type(converted_sequences) + return self.sequence_type( + super(SingleNestedSequenceValue, self)._convert(items)) + + def to_python(self, value): + split_value = [ + v.strip() for v in value.strip().split(self.seq_separator) + ] + # Remove empty items + filtered = filter(None, split_value) + sequence = [ + super(SingleNestedSequenceValue, self).to_python(f) for f in filtered + ] + return self.sequence_type(sequence) + + +class SingleNestedListValue(SingleNestedSequenceValue): + sequence_type = list + + +class SingleNestedTupleValue(SingleNestedSequenceValue): + sequence_type = tuple class BackendsValue(ListValue): @@ -206,47 +264,6 @@ class BackendsValue(ListValue): return value -class TupleValue(ListValue): - message = 'Cannot interpret tuple item {0!r} in tuple {1!r}' - - def __init__(self, *args, **kwargs): - super(TupleValue, self).__init__(*args, **kwargs) - if self.default is None: - self.default = () - else: - self.default = tuple(self.default) - - def to_python(self, value): - 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 56c39fe..c9ef8d7 100644 --- a/docs/values.rst +++ b/docs/values.rst @@ -231,7 +231,7 @@ Type values .. class:: ListValue(default, [separator=',', converter=None]) - A :class:`~Value` subclass that handles list values. + A :class:`~SequenceValue` subclass that handles list values. :param separator: the separator to split environment variables with :param converter: the optional converter callable to apply for each list @@ -266,7 +266,7 @@ Type values .. class:: TupleValue - A :class:`~Value` subclass that handles tuple values. + A :class:`~SequenceValue` subclass that handles tuple values. :param separator: the separator to split environment variables with :param converter: the optional converter callable to apply for each tuple @@ -274,18 +274,19 @@ Type values See the :class:`~ListValue` examples above. -.. class:: TupleOfTuplesValue(default, [tuple_separator=';', separator=',', converter=None]) +.. class:: SingleNestedTupleValue(default, [seq_separator=';', separator=',', converter=None]) - A :class:`~TupleValue` subclass that handles tuple of tuples values. + A :class:`~SingleNestedSequenceValue` subclass that handles single nested tuple values, + e.g. ``((a, b), (c, d))``. - :param tuple_separator: the separator to split each tuple with + :param seq_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(( + ADMINS = SingleNestedTupleValue(( ('John', 'jcleese@site.com'), ('Eric', 'eidle@site.com'), )) @@ -294,6 +295,17 @@ Type values DJANGO_ADMINS=Terry,tjones@site.com;Graham,gchapman@site.com +.. class:: SingleNestedListValue(default, [seq_separator=';', separator=',', converter=None]) + + A :class:`~SingleNestedSequenceValue` subclass that handles single nested list values, + e.g. ``[[a, b], [c, d]]``. + + :param seq_separator: the separator to split each list with + :param separator: the separator to split the inner list contents with + :param converter: the optional converter callable to apply for each inner + list item + + See the :class:`~SingleNestedTupleValue` examples above. .. class:: SetValue diff --git a/tests/test_values.py b/tests/test_values.py index d7c2239..e1410b1 100644 --- a/tests/test_values.py +++ b/tests/test_values.py @@ -9,7 +9,8 @@ from mock import patch from configurations.values import (Value, BooleanValue, IntegerValue, FloatValue, DecimalValue, ListValue, - TupleValue, TupleOfTuplesValue, SetValue, + TupleValue, SingleNestedTupleValue, + SingleNestedListValue, SetValue, DictValue, URLValue, EmailValue, IPValue, RegexValue, PathValue, SecretValue, DatabaseURLValue, EmailURLValue, @@ -175,8 +176,39 @@ class ValueTests(TestCase): with env(DJANGO_TEST=''): self.assertEqual(value.setup('TEST'), ()) - def test_tuple_of_tuples_values_default(self): - value = TupleOfTuplesValue() + def test_single_nested_list_values_default(self): + value = SingleNestedListValue() + 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_single_nested_list_values_separator(self): + value = SingleNestedListValue(seq_separator=':') + with env(DJANGO_TEST='2,3:4,5'): + self.assertEqual(value.setup('TEST'), [['2', '3'], ['4', '5']]) + + def test_single_nested_list_values_converter(self): + value = SingleNestedListValue(converter=int) + with env(DJANGO_TEST='2,3;4,5'): + self.assertEqual(value.setup('TEST'), [[2, 3], [4, 5]]) + + def test_single_nested_list_values_converter_default(self): + value = SingleNestedListValue([['2', '3'], ['4', '5']], converter=int) + self.assertEqual(value.value, [[2, 3], [4, 5]]) + + def test_single_nested_tuple_values_default(self): + value = SingleNestedTupleValue() with env(DJANGO_TEST='2,3;4,5'): expected = (('2', '3'), ('4', '5')) self.assertEqual(value.setup('TEST'), expected) @@ -192,18 +224,18 @@ class ValueTests(TestCase): with env(DJANGO_TEST=''): self.assertEqual(value.setup('TEST'), ()) - def test_tuple_of_tuples_values_separator(self): - value = TupleOfTuplesValue(tuple_separator=':') + def test_single_nested_tuple_values_separator(self): + value = SingleNestedTupleValue(seq_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) + def test_single_nested_tuple_values_converter(self): + value = SingleNestedTupleValue(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) + def test_single_nested_tuple_values_converter_default(self): + value = SingleNestedTupleValue((('2', '3'), ('4', '5')), converter=int) self.assertEqual(value.value, ((2, 3), (4, 5))) def test_set_values_default(self):