Repository: conda/conda

Issue:
add subdirs configuration parameter
As a beta and undocumented feature for future use, add a configurable subdirs parameter.


Discussion:

Resolving patch:
diff --git a/conda/base/context.py b/conda/base/context.py
--- a/conda/base/context.py
+++ b/conda/base/context.py
@@ -93,6 +93,7 @@ class Context(Configuration):
                                    string_delimiter=os.pathsep)
     _pkgs_dirs = SequenceParameter(string_types, aliases=('pkgs_dirs',))
     _subdir = PrimitiveParameter('', aliases=('subdir',))
+    _subdirs = SequenceParameter(string_types, aliases=('subdirs',))
 
     local_repodata_ttl = PrimitiveParameter(1, element_type=(bool, int))
     # number of seconds to cache repodata locally
@@ -241,6 +242,10 @@ def subdir(self):
         else:
             return '%s-%d' % (self.platform, self.bits)
 
+    @property
+    def subdirs(self):
+        return self._subdirs if self._subdirs else (self.subdir, 'noarch')
+
     @property
     def bits(self):
         if self.force_32bit:
@@ -456,6 +461,7 @@ def list_parameters(self):
             'root_dir',
             'skip_safety_checks',
             'subdir',
+            'subdirs',
 # https://conda.io/docs/config.html#disable-updating-of-dependencies-update-dependencies # NOQA
 # I don't think this documentation is correct any longer. # NOQA
             'update_dependencies',
diff --git a/conda/core/index.py b/conda/core/index.py
--- a/conda/core/index.py
+++ b/conda/core/index.py
@@ -88,7 +88,8 @@ def get_index(channel_urls=(), prepend=True, platform=None,
     if context.offline and unknown is None:
         unknown = True
 
-    channel_priority_map = prioritize_channels(channel_urls, platform=platform)
+    subdirs = (platform, 'noarch') if platform is not None else context.subdirs
+    channel_priority_map = prioritize_channels(channel_urls, subdirs=subdirs)
     index = fetch_index(channel_priority_map, use_cache=use_cache)
 
     if prefix or unknown:
@@ -114,7 +115,7 @@ def fetch_index(channel_urls, use_cache=False, index=None):
 
     if index is None:
         index = {}
-    for _, repodata in repodatas:
+    for _, repodata in reversed(repodatas):
         if repodata:
             index.update(repodata.get('packages', {}))
 
diff --git a/conda/models/channel.py b/conda/models/channel.py
--- a/conda/models/channel.py
+++ b/conda/models/channel.py
@@ -7,7 +7,7 @@
 from ..base.constants import (DEFAULTS_CHANNEL_NAME, DEFAULT_CHANNELS_UNIX, DEFAULT_CHANNELS_WIN,
                               MAX_CHANNEL_PRIORITY, UNKNOWN_CHANNEL)
 from ..base.context import context
-from ..common.compat import ensure_text_type, iteritems, odict, with_metaclass
+from ..common.compat import ensure_text_type, isiterable, iteritems, odict, with_metaclass
 from ..common.path import is_path, win_path_backout
 from ..common.url import (Url, has_scheme, is_url, join_url, path_to_url,
                           split_conda_url_easy_parts, split_scheme_auth_token, urlparse)
@@ -268,9 +268,14 @@ def canonical_name(self):
         else:
             return join_url(self.location, self.name).lstrip('/')
 
-    def urls(self, with_credentials=False, platform=None):
+    def urls(self, with_credentials=False, subdirs=None):
+        if subdirs is None:
+            subdirs = context.subdirs
+
+        assert isiterable(subdirs), subdirs  # subdirs must be a non-string iterable
+
         if self.canonical_name == UNKNOWN_CHANNEL:
-            return Channel(DEFAULTS_CHANNEL_NAME).urls(with_credentials, platform)
+            return Channel(DEFAULTS_CHANNEL_NAME).urls(with_credentials, subdirs)
 
         base = [self.location]
         if with_credentials and self.token:
@@ -279,8 +284,14 @@ def urls(self, with_credentials=False, platform=None):
         base = join_url(*base)
 
         def _platforms():
-            p = platform or self.platform or context.subdir
-            return (p, 'noarch') if p != 'noarch' else ('noarch',)
+            if self.platform:
+                yield self.platform
+                if self.platform != 'noarch':
+                    yield 'noarch'
+            else:
+                for subdir in subdirs:
+                    yield subdir
+
         bases = (join_url(base, p) for p in _platforms())
 
         if with_credentials and self.auth:
@@ -301,7 +312,8 @@ def url(self, with_credentials=False):
             if self.package_filename:
                 base.append(self.package_filename)
         else:
-            base.append(context.subdir)
+            first_non_noarch = next((s for s in context.subdirs if s != 'noarch'), 'noarch')
+            base.append(first_non_noarch)
 
         base = join_url(*base)
 
@@ -373,15 +385,16 @@ def channel_location(self):
     def canonical_name(self):
         return self.name
 
-    def urls(self, with_credentials=False, platform=None):
-        if platform and platform != context.subdir and self.name == 'defaults':
-            # necessary shenanigan because different platforms have different default channels
-            urls = DEFAULT_CHANNELS_WIN if 'win' in platform else DEFAULT_CHANNELS_UNIX
-            ca = context.channel_alias
-            _channels = tuple(Channel.make_simple_channel(ca, v) for v in urls)
-        else:
-            _channels = self._channels
-        return list(chain.from_iterable(c.urls(with_credentials, platform) for c in _channels))
+    def urls(self, with_credentials=False, subdirs=None):
+        _channels = self._channels
+        if self.name == 'defaults':
+            platform = next((s for s in reversed(subdirs or context.subdirs) if s != 'noarch'), '')
+            if platform != context.subdir:
+                # necessary shenanigan because different platforms have different default channels
+                urls = DEFAULT_CHANNELS_WIN if 'win' in platform else DEFAULT_CHANNELS_UNIX
+                ca = context.channel_alias
+                _channels = tuple(Channel.make_simple_channel(ca, v) for v in urls)
+        return list(chain.from_iterable(c.urls(with_credentials, subdirs) for c in _channels))
 
     @property
     def base_url(self):
@@ -391,17 +404,19 @@ def url(self, with_credentials=False):
         return None
 
 
-def prioritize_channels(channels, with_credentials=True, platform=None):
+def prioritize_channels(channels, with_credentials=True, subdirs=None):
     # prioritize_channels returns and OrderedDict with platform-specific channel
     #   urls as the key, and a tuple of canonical channel name and channel priority
     #   number as the value
     # ('https://conda.anaconda.org/conda-forge/osx-64/', ('conda-forge', 1))
     result = odict()
-    for q, chn in enumerate(channels):
+    q = -1  # channel priority counter
+    for chn in channels:
         channel = Channel(chn)
-        for url in channel.urls(with_credentials, platform):
+        for url in channel.urls(with_credentials, subdirs):
             if url in result:
                 continue
+            q += 1
             result[url] = channel.canonical_name, min(q, MAX_CHANNEL_PRIORITY - 1)
     return result
 


Test patch:
diff --git a/tests/base/test_context.py b/tests/base/test_context.py
--- a/tests/base/test_context.py
+++ b/tests/base/test_context.py
@@ -24,7 +24,7 @@
 from unittest import TestCase
 
 
-class ContextTests(TestCase):
+class ContextCustomRcTests(TestCase):
 
     def setUp(self):
         string = dals("""
@@ -171,3 +171,13 @@ def test_describe_all(self):
         from pprint import pprint
         for name in paramter_names:
             pprint(context.describe_parameter(name))
+
+
+class ContextDefaultRcTests(TestCase):
+
+    def test_subdirs(self):
+        assert context.subdirs == (context.subdir, 'noarch')
+
+        subdirs = ('linux-highest', 'linux-64', 'noarch')
+        with env_var('CONDA_SUBDIRS', ','.join(subdirs), reset_context):
+            assert context.subdirs == subdirs
diff --git a/tests/models/test_channel.py b/tests/models/test_channel.py
--- a/tests/models/test_channel.py
+++ b/tests/models/test_channel.py
@@ -5,7 +5,7 @@
 import os
 from tempfile import gettempdir
 
-from conda.base.constants import APP_NAME
+from conda.base.constants import APP_NAME, DEFAULT_CHANNELS, DEFAULT_CHANNELS_UNIX
 from conda.common.io import env_var
 
 from conda._vendor.auxlib.ish import dals
@@ -789,11 +789,46 @@ def test_env_var_file_urls(self):
             prioritized = prioritize_channels(new_context.channels)
             assert prioritized == OrderedDict((
                 ("file://network_share/shared_folder/path/conda/%s" % context.subdir, ("file://network_share/shared_folder/path/conda", 0)),
-                ("file://network_share/shared_folder/path/conda/noarch", ("file://network_share/shared_folder/path/conda", 0)),
-                ("https://some.url/ch_name/%s" % context.subdir, ("https://some.url/ch_name", 1)),
-                ("https://some.url/ch_name/noarch", ("https://some.url/ch_name", 1)),
-                ("file:///some/place/on/my/machine/%s" % context.subdir, ("file:///some/place/on/my/machine", 2)),
-                ("file:///some/place/on/my/machine/noarch", ("file:///some/place/on/my/machine", 2)),
+                ("file://network_share/shared_folder/path/conda/noarch", ("file://network_share/shared_folder/path/conda", 1)),
+                ("https://some.url/ch_name/%s" % context.subdir, ("https://some.url/ch_name", 2)),
+                ("https://some.url/ch_name/noarch", ("https://some.url/ch_name", 3)),
+                ("file:///some/place/on/my/machine/%s" % context.subdir, ("file:///some/place/on/my/machine", 4)),
+                ("file:///some/place/on/my/machine/noarch", ("file:///some/place/on/my/machine", 5)),
+            ))
+
+    def test_subdirs(self):
+        subdirs = ('linux-highest', 'linux-64', 'noarch')
+
+        def _channel_urls(channels=None):
+            for channel in channels or DEFAULT_CHANNELS_UNIX:
+                channel = Channel(channel)
+                for subdir in subdirs:
+                    yield join_url(channel.base_url, subdir)
+
+        with env_var('CONDA_SUBDIRS', ','.join(subdirs), reset_context):
+            c = Channel('defaults')
+            assert c.urls() == list(_channel_urls())
+
+            c = Channel('conda-forge')
+            assert c.urls() == list(_channel_urls(('conda-forge',)))
+
+            channels = ('bioconda', 'conda-forge')
+            prioritized = prioritize_channels(channels)
+            assert prioritized == OrderedDict((
+                ("https://conda.anaconda.org/bioconda/linux-highest", ("bioconda", 0)),
+                ("https://conda.anaconda.org/bioconda/linux-64", ("bioconda", 1)),
+                ("https://conda.anaconda.org/bioconda/noarch", ("bioconda", 2)),
+                ("https://conda.anaconda.org/conda-forge/linux-highest", ("conda-forge", 3)),
+                ("https://conda.anaconda.org/conda-forge/linux-64", ("conda-forge", 4)),
+                ("https://conda.anaconda.org/conda-forge/noarch", ("conda-forge", 5)),
+            ))
+
+            prioritized = prioritize_channels(channels, subdirs=('linux-again', 'noarch'))
+            assert prioritized == OrderedDict((
+                ("https://conda.anaconda.org/bioconda/linux-again", ("bioconda", 0)),
+                ("https://conda.anaconda.org/bioconda/noarch", ("bioconda", 1)),
+                ("https://conda.anaconda.org/conda-forge/linux-again", ("conda-forge", 2)),
+                ("https://conda.anaconda.org/conda-forge/noarch", ("conda-forge", 3)),
             ))