Repository: conda/conda

Issue:
CONDA_CHANNELS environment variable doesn't work
fixed with #3390



Discussion:

Resolving patch:
diff --git a/conda/common/configuration.py b/conda/common/configuration.py
--- a/conda/common/configuration.py
+++ b/conda/common/configuration.py
@@ -188,7 +188,7 @@ def __repr__(self):
         return text_type(vars(self))
 
     @abstractmethod
-    def value(self, parameter_type):
+    def value(self, parameter_obj):
         raise NotImplementedError()
 
     @abstractmethod
@@ -196,7 +196,7 @@ def keyflag(self):
         raise NotImplementedError()
 
     @abstractmethod
-    def valueflags(self, parameter_type):
+    def valueflags(self, parameter_obj):
         raise NotImplementedError()
 
     @classmethod
@@ -209,14 +209,29 @@ def make_raw_parameters(cls, source, from_map):
 class EnvRawParameter(RawParameter):
     source = 'envvars'
 
-    def value(self, parameter_type):
-        return self.__important_split_value[0].strip()
+    def value(self, parameter_obj):
+        if hasattr(parameter_obj, 'string_delimiter'):
+            string_delimiter = getattr(parameter_obj, 'string_delimiter')
+            # TODO: add stripping of !important, !top, and !bottom
+            raw_value = self._raw_value
+            if string_delimiter in raw_value:
+                value = raw_value.split(string_delimiter)
+            else:
+                value = [raw_value]
+            return tuple(v.strip() for v in value)
+        else:
+            return self.__important_split_value[0].strip()
 
     def keyflag(self):
         return ParameterFlag.final if len(self.__important_split_value) >= 2 else None
 
-    def valueflags(self, parameter_type):
-        return None
+    def valueflags(self, parameter_obj):
+        if hasattr(parameter_obj, 'string_delimiter'):
+            string_delimiter = getattr(parameter_obj, 'string_delimiter')
+            # TODO: add stripping of !important, !top, and !bottom
+            return tuple('' for _ in self._raw_value.split(string_delimiter))
+        else:
+            return self.__important_split_value[0].strip()
 
     @property
     def __important_split_value(self):
@@ -233,13 +248,13 @@ def make_raw_parameters(cls, appname):
 class ArgParseRawParameter(RawParameter):
     source = 'cmd_line'
 
-    def value(self, parameter_type):
+    def value(self, parameter_obj):
         return make_immutable(self._raw_value)
 
     def keyflag(self):
         return None
 
-    def valueflags(self, parameter_type):
+    def valueflags(self, parameter_obj):
         return None
 
     @classmethod
@@ -255,18 +270,18 @@ def __init__(self, source, key, raw_value, keycomment):
         self._keycomment = keycomment
         super(YamlRawParameter, self).__init__(source, key, raw_value)
 
-    def value(self, parameter_type):
-        self.__process(parameter_type)
+    def value(self, parameter_obj):
+        self.__process(parameter_obj)
         return self._value
 
     def keyflag(self):
         return ParameterFlag.from_string(self._keycomment)
 
-    def valueflags(self, parameter_type):
-        self.__process(parameter_type)
+    def valueflags(self, parameter_obj):
+        self.__process(parameter_obj)
         return self._valueflags
 
-    def __process(self, parameter_type):
+    def __process(self, parameter_obj):
         if hasattr(self, '_value'):
             return
         elif isinstance(self._raw_value, CommentedSeq):
@@ -511,18 +526,17 @@ def __init__(self, default, aliases=(), validation=None, parameter_type=None):
     def _merge(self, matches):
         important_match = first(matches, self._match_key_is_important, default=None)
         if important_match is not None:
-            return important_match.value(self.__class__)
+            return important_match.value(self)
 
         last_match = last(matches, lambda x: x is not None, default=None)
         if last_match is not None:
-            return last_match.value(self.__class__)
+            return last_match.value(self)
         raise ThisShouldNeverHappenError()  # pragma: no cover
 
-    @classmethod
-    def repr_raw(cls, raw_parameter):
+    def repr_raw(self, raw_parameter):
         return "%s: %s%s" % (raw_parameter.key,
-                             cls._str_format_value(raw_parameter.value(cls)),
-                             cls._str_format_flag(raw_parameter.keyflag()))
+                             self._str_format_value(raw_parameter.value(self)),
+                             self._str_format_flag(raw_parameter.keyflag()))
 
 
 class SequenceParameter(Parameter):
@@ -531,7 +545,8 @@ class SequenceParameter(Parameter):
     """
     _type = tuple
 
-    def __init__(self, element_type, default=(), aliases=(), validation=None):
+    def __init__(self, element_type, default=(), aliases=(), validation=None,
+                 string_delimiter=','):
         """
         Args:
             element_type (type or Iterable[type]): The generic type of each element in
@@ -543,6 +558,7 @@ def __init__(self, element_type, default=(), aliases=(), validation=None):
 
         """
         self._element_type = element_type
+        self.string_delimiter = string_delimiter
         super(SequenceParameter, self).__init__(default, aliases, validation)
 
     def collect_errors(self, instance, value, source="<<merged>>"):
@@ -562,21 +578,21 @@ def _merge(self, matches):
 
         # get individual lines from important_matches that were marked important
         # these will be prepended to the final result
-        def get_marked_lines(match, marker):
+        def get_marked_lines(match, marker, parameter_obj):
             return tuple(line
-                         for line, flag in zip(match.value(self.__class__),
-                                               match.valueflags(self.__class__))
+                         for line, flag in zip(match.value(parameter_obj),
+                                               match.valueflags(parameter_obj))
                          if flag is marker)
-        top_lines = concat(get_marked_lines(m, ParameterFlag.top) for m in relevant_matches)
+        top_lines = concat(get_marked_lines(m, ParameterFlag.top, self) for m in relevant_matches)
 
         # also get lines that were marked as bottom, but reverse the match order so that lines
         # coming earlier will ultimately be last
-        bottom_lines = concat(get_marked_lines(m, ParameterFlag.bottom) for m in
+        bottom_lines = concat(get_marked_lines(m, ParameterFlag.bottom, self) for m in
                               reversed(relevant_matches))
 
         # now, concat all lines, while reversing the matches
         #   reverse because elements closer to the end of search path take precedence
-        all_lines = concat(m.value(self.__class__) for m in reversed(relevant_matches))
+        all_lines = concat(m.value(self) for m in reversed(relevant_matches))
 
         # stack top_lines + all_lines, then de-dupe
         top_deduped = tuple(unique(concatv(top_lines, all_lines)))
@@ -586,19 +602,17 @@ def get_marked_lines(match, marker):
         # NOTE: for a line value marked both top and bottom, the bottom marker will win out
         #       for the top marker to win out, we'd need one additional de-dupe step
         bottom_deduped = unique(concatv(reversed(tuple(bottom_lines)), reversed(top_deduped)))
-
         # just reverse, and we're good to go
         return tuple(reversed(tuple(bottom_deduped)))
 
-    @classmethod
-    def repr_raw(cls, raw_parameter):
+    def repr_raw(self, raw_parameter):
         lines = list()
         lines.append("%s:%s" % (raw_parameter.key,
-                                cls._str_format_flag(raw_parameter.keyflag())))
-        for q, value in enumerate(raw_parameter.value(cls)):
-            valueflag = raw_parameter.valueflags(cls)[q]
-            lines.append("  - %s%s" % (cls._str_format_value(value),
-                                       cls._str_format_flag(valueflag)))
+                                self._str_format_flag(raw_parameter.keyflag())))
+        for q, value in enumerate(raw_parameter.value(self)):
+            valueflag = raw_parameter.valueflags(self)[q]
+            lines.append("  - %s%s" % (self._str_format_value(value),
+                                       self._str_format_flag(valueflag)))
         return '\n'.join(lines)
 
 
@@ -635,25 +649,24 @@ def _merge(self, matches):
 
         # mapkeys with important matches
         def key_is_important(match, key):
-            return match.valueflags(self.__class__).get(key) is ParameterFlag.final
+            return match.valueflags(self).get(key) is ParameterFlag.final
         important_maps = tuple(dict((k, v)
-                                    for k, v in iteritems(match.value(self.__class__))
+                                    for k, v in iteritems(match.value(self))
                                     if key_is_important(match, k))
                                for match in relevant_matches)
         # dump all matches in a dict
         # then overwrite with important matches
-        return merge(concatv((m.value(self.__class__) for m in relevant_matches),
+        return merge(concatv((m.value(self) for m in relevant_matches),
                              reversed(important_maps)))
 
-    @classmethod
-    def repr_raw(cls, raw_parameter):
+    def repr_raw(self, raw_parameter):
         lines = list()
         lines.append("%s:%s" % (raw_parameter.key,
-                                cls._str_format_flag(raw_parameter.keyflag())))
-        for valuekey, value in iteritems(raw_parameter.value(cls)):
-            valueflag = raw_parameter.valueflags(cls).get(valuekey)
-            lines.append("  %s: %s%s" % (valuekey, cls._str_format_value(value),
-                                         cls._str_format_flag(valueflag)))
+                                self._str_format_flag(raw_parameter.keyflag())))
+        for valuekey, value in iteritems(raw_parameter.value(self)):
+            valueflag = raw_parameter.valueflags(self).get(valuekey)
+            lines.append("  %s: %s%s" % (valuekey, self._str_format_value(value),
+                                         self._str_format_flag(valueflag)))
         return '\n'.join(lines)
 
 
@@ -717,7 +730,7 @@ def check_source(self, source):
 
             if match is not None:
                 try:
-                    typed_value = typify_data_structure(match.value(parameter.__class__),
+                    typed_value = typify_data_structure(match.value(parameter),
                                                         parameter._element_type)
                 except TypeCoercionError as e:
                     validation_errors.append(CustomValidationError(match.key, e.value,


Test patch:
diff --git a/tests/common/test_configuration.py b/tests/common/test_configuration.py
--- a/tests/common/test_configuration.py
+++ b/tests/common/test_configuration.py
@@ -128,7 +128,7 @@
 }
 
 
-class TestConfiguration(Configuration):
+class SampleConfiguration(Configuration):
     always_yes = PrimitiveParameter(False, aliases=('always_yes_altname1', 'yes',
                                                     'always_yes_altname2'))
     changeps1 = PrimitiveParameter(True)
@@ -148,13 +148,13 @@ def load_from_string_data(*seq):
 class ConfigurationTests(TestCase):
 
     def test_simple_merges_and_caching(self):
-        config = TestConfiguration()._add_raw_data(load_from_string_data('file1', 'file2'))
+        config = SampleConfiguration()._add_raw_data(load_from_string_data('file1', 'file2'))
         assert config.changeps1 is False
         assert config.always_yes is True
         assert config.channels == ('porky', 'bugs', 'elmer', 'daffy', 'tweety')
         assert config.proxy_servers == {'http': 'marv', 'https': 'sam', 's3': 'pepé'}
 
-        config = TestConfiguration()._add_raw_data(load_from_string_data('file2', 'file1'))
+        config = SampleConfiguration()._add_raw_data(load_from_string_data('file2', 'file1'))
         assert len(config._cache) == 0
         assert config.changeps1 is False
         assert len(config._cache) == 1
@@ -166,7 +166,7 @@ def test_simple_merges_and_caching(self):
         assert config.proxy_servers == {'http': 'taz', 'https': 'sly', 's3': 'pepé'}
 
     def test_default_values(self):
-        config = TestConfiguration()
+        config = SampleConfiguration()
         assert config.channels == ()
         assert config.always_yes is False
         assert config.proxy_servers == {}
@@ -183,8 +183,7 @@ def make_key(appname, key):
         try:
             environ.update(test_dict)
             assert 'MYAPP_ALWAYS_YES' in environ
-            raw_data = load_from_string_data('file1', 'file2')
-            config = TestConfiguration(app_name=appname)
+            config = SampleConfiguration(app_name=appname)
             assert config.changeps1 is False
             assert config.always_yes is True
         finally:
@@ -201,13 +200,42 @@ def make_key(appname, key):
         try:
             environ.update(test_dict)
             assert 'MYAPP_YES' in environ
-            raw_data = load_from_string_data('file1', 'file2')
-            config = TestConfiguration()._add_env_vars(appname)
+            config = SampleConfiguration()._add_env_vars(appname)
             assert config.always_yes is True
             assert config.changeps1 is False
         finally:
             [environ.pop(key) for key in test_dict]
 
+    def test_env_var_config_split_sequence(self):
+        def make_key(appname, key):
+            return "{0}_{1}".format(appname.upper(), key.upper())
+        appname = "myapp"
+        test_dict = {}
+        test_dict[make_key(appname, 'channels')] = 'channel1,channel2'
+
+        try:
+            environ.update(test_dict)
+            assert 'MYAPP_CHANNELS' in environ
+            config = SampleConfiguration()._add_env_vars(appname)
+            assert config.channels == ('channel1', 'channel2')
+        finally:
+            [environ.pop(key) for key in test_dict]
+
+    def test_env_var_config_no_split_sequence(self):
+        def make_key(appname, key):
+            return "{0}_{1}".format(appname.upper(), key.upper())
+        appname = "myapp"
+        test_dict = {}
+        test_dict[make_key(appname, 'channels')] = 'channel1'
+
+        try:
+            environ.update(test_dict)
+            assert 'MYAPP_CHANNELS' in environ
+            config = SampleConfiguration()._add_env_vars(appname)
+            assert config.channels == ('channel1',)
+        finally:
+            [environ.pop(key) for key in test_dict]
+
     def test_load_raw_configs(self):
         try:
             tempdir = mkdtemp()
@@ -232,7 +260,7 @@ def test_load_raw_configs(self):
             assert raw_data[f1]['always_yes'].value(None) == "no"
             assert raw_data[f2]['proxy_servers'].value(None)['http'] == "marv"
 
-            config = TestConfiguration(search_path)
+            config = SampleConfiguration(search_path)
             assert config.channels == ('wile', 'porky', 'bugs', 'elmer', 'daffy',
                                        'tweety', 'foghorn')
 
@@ -241,105 +269,105 @@ def test_load_raw_configs(self):
 
     def test_important_primitive_map_merges(self):
         raw_data = load_from_string_data('file1', 'file3', 'file2')
-        config = TestConfiguration()._add_raw_data(raw_data)
+        config = SampleConfiguration()._add_raw_data(raw_data)
         assert config.changeps1 is False
         assert config.always_yes is True
         assert config.channels == ('wile', 'porky', 'bugs', 'elmer', 'daffy', 'foghorn', 'tweety')
         assert config.proxy_servers == {'http': 'foghorn', 'https': 'sam', 's3': 'porky'}
 
         raw_data = load_from_string_data('file3', 'file2', 'file1')
-        config = TestConfiguration()._add_raw_data(raw_data)
+        config = SampleConfiguration()._add_raw_data(raw_data)
         assert config.changeps1 is False
         assert config.always_yes is True
         assert config.channels == ('wile', 'bugs', 'daffy', 'tweety', 'porky', 'elmer', 'foghorn')
         assert config.proxy_servers == {'http': 'foghorn', 'https': 'sly', 's3': 'pepé'}
 
         raw_data = load_from_string_data('file4', 'file3', 'file1')
-        config = TestConfiguration()._add_raw_data(raw_data)
+        config = SampleConfiguration()._add_raw_data(raw_data)
         assert config.changeps1 is False
         assert config.always_yes is True
         assert config.proxy_servers == {'https': 'daffy', 'http': 'bugs'}
 
         raw_data = load_from_string_data('file1', 'file4', 'file3', 'file2')
-        config = TestConfiguration()._add_raw_data(raw_data)
+        config = SampleConfiguration()._add_raw_data(raw_data)
         assert config.changeps1 is False
         assert config.always_yes is True
         assert config.proxy_servers == {'http': 'bugs', 'https': 'daffy', 's3': 'pepé'}
 
         raw_data = load_from_string_data('file1', 'file2', 'file3', 'file4')
-        config = TestConfiguration()._add_raw_data(raw_data)
+        config = SampleConfiguration()._add_raw_data(raw_data)
         assert config.changeps1 is False
         assert config.always_yes is True
         assert config.proxy_servers == {'https': 'daffy', 'http': 'foghorn', 's3': 'porky'}
 
         raw_data = load_from_string_data('file3', 'file1')
-        config = TestConfiguration()._add_raw_data(raw_data)
+        config = SampleConfiguration()._add_raw_data(raw_data)
         assert config.changeps1 is True
         assert config.always_yes is True
         assert config.proxy_servers == {'https': 'sly', 'http': 'foghorn', 's3': 'pepé'}
 
         raw_data = load_from_string_data('file4', 'file3')
-        config = TestConfiguration()._add_raw_data(raw_data)
+        config = SampleConfiguration()._add_raw_data(raw_data)
         assert config.changeps1 is False
         assert config.always_yes is True
         assert config.proxy_servers == {'http': 'bugs', 'https': 'daffy'}
 
     def test_list_merges(self):
         raw_data = load_from_string_data('file5', 'file3')
-        config = TestConfiguration()._add_raw_data(raw_data)
+        config = SampleConfiguration()._add_raw_data(raw_data)
         assert config.channels == ('marv', 'wile', 'daffy', 'foghorn', 'pepé', 'sam')
 
         raw_data = load_from_string_data('file6', 'file5', 'file4', 'file3')
-        config = TestConfiguration()._add_raw_data(raw_data)
+        config = SampleConfiguration()._add_raw_data(raw_data)
         assert config.channels == ('pepé', 'sam', 'elmer', 'bugs', 'marv')
 
         raw_data = load_from_string_data('file3', 'file4', 'file5', 'file6')
-        config = TestConfiguration()._add_raw_data(raw_data)
+        config = SampleConfiguration()._add_raw_data(raw_data)
         assert config.channels == ('wile', 'pepé', 'marv', 'sam', 'daffy', 'foghorn')
 
         raw_data = load_from_string_data('file6', 'file3', 'file4', 'file5')
-        config = TestConfiguration()._add_raw_data(raw_data)
+        config = SampleConfiguration()._add_raw_data(raw_data)
         assert config.channels == ('wile', 'pepé', 'sam', 'daffy', 'foghorn',
                                    'elmer', 'bugs', 'marv')
 
         raw_data = load_from_string_data('file7', 'file8', 'file9')
-        config = TestConfiguration()._add_raw_data(raw_data)
+        config = SampleConfiguration()._add_raw_data(raw_data)
         assert config.channels == ('sam', 'marv', 'wile', 'foghorn', 'daffy', 'pepé')
 
         raw_data = load_from_string_data('file7', 'file9', 'file8')
-        config = TestConfiguration()._add_raw_data(raw_data)
+        config = SampleConfiguration()._add_raw_data(raw_data)
         assert config.channels == ('sam', 'marv', 'pepé', 'wile', 'foghorn', 'daffy')
 
         raw_data = load_from_string_data('file8', 'file7', 'file9')
-        config = TestConfiguration()._add_raw_data(raw_data)
+        config = SampleConfiguration()._add_raw_data(raw_data)
         assert config.channels == ('marv', 'sam', 'wile', 'foghorn', 'daffy', 'pepé')
 
         raw_data = load_from_string_data('file8', 'file9', 'file7')
-        config = TestConfiguration()._add_raw_data(raw_data)
+        config = SampleConfiguration()._add_raw_data(raw_data)
         assert config.channels == ('marv', 'sam', 'wile', 'daffy', 'pepé')
 
         raw_data = load_from_string_data('file9', 'file7', 'file8')
-        config = TestConfiguration()._add_raw_data(raw_data)
+        config = SampleConfiguration()._add_raw_data(raw_data)
         assert config.channels == ('marv', 'sam', 'pepé', 'daffy')
 
         raw_data = load_from_string_data('file9', 'file8', 'file7')
-        config = TestConfiguration()._add_raw_data(raw_data)
+        config = SampleConfiguration()._add_raw_data(raw_data)
         assert config.channels == ('marv', 'sam', 'pepé', 'daffy')
 
     def test_validation(self):
-        config = TestConfiguration()._add_raw_data(load_from_string_data('bad_boolean'))
+        config = SampleConfiguration()._add_raw_data(load_from_string_data('bad_boolean'))
         raises(ValidationError, lambda: config.always_yes)
 
-        config = TestConfiguration()._add_raw_data(load_from_string_data('too_many_aliases'))
+        config = SampleConfiguration()._add_raw_data(load_from_string_data('too_many_aliases'))
         raises(ValidationError, lambda: config.always_yes)
 
-        config = TestConfiguration()._add_raw_data(load_from_string_data('not_an_int'))
+        config = SampleConfiguration()._add_raw_data(load_from_string_data('not_an_int'))
         raises(ValidationError, lambda: config.always_an_int)
 
-        config = TestConfiguration()._add_raw_data(load_from_string_data('bad_boolean_map'))
+        config = SampleConfiguration()._add_raw_data(load_from_string_data('bad_boolean_map'))
         raises(ValidationError, lambda: config.boolean_map)
 
-        config = TestConfiguration()._add_raw_data(load_from_string_data('good_boolean_map'))
+        config = SampleConfiguration()._add_raw_data(load_from_string_data('good_boolean_map'))
         assert config.boolean_map['a_true'] is True
         assert config.boolean_map['a_yes'] is True
         assert config.boolean_map['a_1'] is True
@@ -351,10 +379,10 @@ def test_parameter(self):
         assert ParameterFlag.from_name('top') is ParameterFlag.top
 
     def test_validate_all(self):
-        config = TestConfiguration()._add_raw_data(load_from_string_data('file1'))
+        config = SampleConfiguration()._add_raw_data(load_from_string_data('file1'))
         config.validate_all()
 
-        config = TestConfiguration()._add_raw_data(load_from_string_data('bad_boolean_map'))
+        config = SampleConfiguration()._add_raw_data(load_from_string_data('bad_boolean_map'))
         raises(ValidationError, config.validate_all)
 
     def test_cross_parameter_validation(self):
diff --git a/tests/conda_env/support/notebook.ipynb b/tests/conda_env/support/notebook.ipynb
--- a/tests/conda_env/support/notebook.ipynb
+++ b/tests/conda_env/support/notebook.ipynb
@@ -1 +1 @@
-{"nbformat": 3, "nbformat_minor": 0, "worksheets": [{"cells": []}], "metadata": {"name": "", "signature": ""}}
\ No newline at end of file
+{"nbformat": 3, "worksheets": [{"cells": []}], "metadata": {"signature": "", "name": ""}, "nbformat_minor": 0}
\ No newline at end of file