Repository: conda/conda

Issue:
Conda breaks if it can't find the noarch channel
If the `noarch` URL of some custom channel doesn't exist, conda becomes completely broken. The error is:

```
Fetching package metadata ......
WARNING: The remote server could not find the noarch directory for the
requested channel with url: https://zigzah.com/static/conda-pkgs/noarch

It is possible you have given conda an invalid channel. Please double-check
your conda configuration using `conda config --show`.

If the requested url is in fact a valid conda channel, please request that the
channel administrator create `noarch/repodata.json` and associated
`noarch/repodata.json.bz2` files, even if `noarch/repodata.json` is empty.
An unexpected error has occurred.
Please consider posting the following information to the
conda GitHub issue tracker at:

    https://github.com/conda/conda/issues



Current conda install:

               platform : linux-64
          conda version : 4.3.9
       conda is private : False
      conda-env version : 4.3.9
    conda-build version : 2.0.12
         python version : 3.5.3.final.0
       requests version : 2.13.0
       root environment : /home/zah/anaconda3  (writable)
    default environment : /home/zah/anaconda3
       envs directories : /home/zah/anaconda3/envs
          package cache : /home/zah/anaconda3/pkgs
           channel URLs :
                          https://zigzah.com/static/conda-pkgs/linux-64
                          https://zigzah.com/static/conda-pkgs/noarch
                          https://conda.anaconda.org/conda-forge/linux-64
                          https://conda.anaconda.org/conda-forge/noarch
                          https://repo.continuum.io/pkgs/free/linux-64
                          https://repo.continuum.io/pkgs/free/noarch
                          https://repo.continuum.io/pkgs/r/linux-64
                          https://repo.continuum.io/pkgs/r/noarch
                          https://repo.continuum.io/pkgs/pro/linux-64
                          https://repo.continuum.io/pkgs/pro/noarch
                          https://conda.anaconda.org/r/linux-64
                          https://conda.anaconda.org/r/noarch
            config file : /home/zah/.condarc
           offline mode : False
             user-agent : conda/4.3.9 requests/2.13.0 CPython/3.5.3 Linux/4.8.0-32-generic debian/stretch/sid glibc/2.23
                UID:GID : 1000:1000



`$ /home/zah/anaconda3/bin/conda install validphys`




    Traceback (most recent call last):
      File "/home/zah/anaconda3/lib/python3.5/site-packages/conda/exceptions.py", line 616, in conda_exception_handler
        return_value = func(*args, **kwargs)
      File "/home/zah/anaconda3/lib/python3.5/site-packages/conda/cli/main.py", line 137, in _main
        exit_code = args.func(args, p)
      File "/home/zah/anaconda3/lib/python3.5/site-packages/conda/cli/main_install.py", line 80, in execute
        install(args, parser, 'install')
      File "/home/zah/anaconda3/lib/python3.5/site-packages/conda/cli/install.py", line 222, in install
        unknown=index_args['unknown'], prefix=prefix)
      File "/home/zah/anaconda3/lib/python3.5/site-packages/conda/core/index.py", line 124, in get_index
        index = fetch_index(channel_priority_map, use_cache=use_cache)
      File "/home/zah/anaconda3/lib/python3.5/site-packages/conda/core/index.py", line 532, in fetch_index
        repodatas = _collect_repodatas(use_cache, tasks)
      File "/home/zah/anaconda3/lib/python3.5/site-packages/conda/core/index.py", line 521, in _collect_repodatas
        repodatas = _collect_repodatas_serial(use_cache, tasks)
      File "/home/zah/anaconda3/lib/python3.5/site-packages/conda/core/index.py", line 494, in _collect_repodatas_serial
        for url, schan, pri in tasks]
      File "/home/zah/anaconda3/lib/python3.5/site-packages/conda/core/index.py", line 494, in <listcomp>
        for url, schan, pri in tasks]
      File "/home/zah/anaconda3/lib/python3.5/site-packages/conda/core/index.py", line 143, in func
        res = f(*args, **kwargs)
      File "/home/zah/anaconda3/lib/python3.5/site-packages/conda/core/index.py", line 483, in fetch_repodata
        process_repodata(repodata, url, schannel, priority)
      File "/home/zah/anaconda3/lib/python3.5/site-packages/conda/core/index.py", line 410, in process_repodata
        opackages = repodata.setdefault('packages', {})
    AttributeError: 'NoneType' object has no attribute 'setdefault'
```


Discussion:

Resolving patch:
diff --git a/conda/core/index.py b/conda/core/index.py
--- a/conda/core/index.py
+++ b/conda/core/index.py
@@ -9,7 +9,7 @@
 from logging import DEBUG, getLogger
 from mmap import ACCESS_READ, mmap
 from os import makedirs
-from os.path import getmtime, isfile, join, split as path_split
+from os.path import getmtime, isfile, join, split as path_split, dirname
 import pickle
 import re
 from textwrap import dedent
@@ -234,12 +234,13 @@ def maybe_decompress(filename, resp_content):
                     $ mkdir noarch
                     $ echo '{}' > noarch/repodata.json
                     $ bzip2 -k noarch/repodata.json
-                    """ % url)
+                    """) % dirname(url)
                     stderrlog.warn(help_message)
                     return None
                 else:
                     help_message = dals("""
-                    The remote server could not find the channel you requested.
+                    The remote server could not find the noarch directory for the
+                    requested channel with url: %s
 
                     As of conda 4.3, a valid channel must contain a `noarch/repodata.json` and
                     associated `noarch/repodata.json.bz2` file, even if `noarch/repodata.json` is
@@ -252,7 +253,7 @@ def maybe_decompress(filename, resp_content):
                     You will need to adjust your conda configuration to proceed.
                     Use `conda config --show` to view your configuration's current state.
                     Further configuration help can be found at <%s>.
-                    """ % join_url(CONDA_HOMEPAGE_URL, 'docs/config.html'))
+                    """) % (dirname(url), join_url(CONDA_HOMEPAGE_URL, 'docs/config.html'))
 
         elif status_code == 403:
             if not url.endswith('/noarch'):
@@ -272,12 +273,13 @@ def maybe_decompress(filename, resp_content):
                     $ mkdir noarch
                     $ echo '{}' > noarch/repodata.json
                     $ bzip2 -k noarch/repodata.json
-                    """ % url)
+                    """) % dirname(url)
                     stderrlog.warn(help_message)
                     return None
                 else:
                     help_message = dals("""
-                    The channel you requested is not available on the remote server.
+                    The remote server could not find the noarch directory for the
+                    requested channel with url: %s
 
                     As of conda 4.3, a valid channel must contain a `noarch/repodata.json` and
                     associated `noarch/repodata.json.bz2` file, even if `noarch/repodata.json` is
@@ -290,7 +292,7 @@ def maybe_decompress(filename, resp_content):
                     You will need to adjust your conda configuration to proceed.
                     Use `conda config --show` to view your configuration's current state.
                     Further configuration help can be found at <%s>.
-                    """ % join_url(CONDA_HOMEPAGE_URL, 'docs/config.html'))
+                    """) % (dirname(url), join_url(CONDA_HOMEPAGE_URL, 'docs/config.html'))
 
         elif status_code == 401:
             channel = Channel(url)
@@ -491,9 +493,12 @@ def fetch_repodata(url, schannel, priority,
         touch(cache_path)
         return read_local_repodata(cache_path, url, schannel, priority,
                                    mod_etag_headers.get('_etag'), mod_etag_headers.get('_mod'))
+    if repodata is None:
+        return None
 
     with open(cache_path, 'w') as fo:
         json.dump(repodata, fo, indent=2, sort_keys=True, cls=EntityEncoder)
+
     process_repodata(repodata, url, schannel, priority)
     write_pickled_repodata(cache_path, repodata)
     return repodata
@@ -548,7 +553,7 @@ def fetch_index(channel_urls, use_cache=False, index=None):
     #   this is sorta a lie; actually more primitve types
 
     if index is None:
-        index = dict()
+        index = {}
     for _, repodata in repodatas:
         if repodata:
             index.update(repodata.get('packages', {}))


Test patch:
diff --git a/tests/test_create.py b/tests/test_create.py
--- a/tests/test_create.py
+++ b/tests/test_create.py
@@ -191,6 +191,12 @@ def test_install_python2(self):
             assert exists(join(prefix, PYTHON_BINARY))
             assert_package_is_installed(prefix, 'python-2')
 
+            # regression test for #4513
+            run_command(Commands.CONFIG, prefix, "--add channels https://repo.continuum.io/pkgs/not-a-channel")
+            stdout, stderr = run_command(Commands.SEARCH, prefix, "python --json")
+            packages = json.loads(stdout)
+            assert len(packages) > 1
+
     @pytest.mark.timeout(900)
     def test_create_install_update_remove(self):
         with make_temp_env("python=3.5") as prefix: