Repository: conda/conda

Issue:
regression bug: https://github.com/conda/conda/issues/3235 appears to have resurrected itself in another place
The bug was that conda handled the channel "http://conda-01" incorrectly. Here's the stack trace in conda 4.2.11:

```
    Traceback (most recent call last):
      File "C:\Miniconda3\lib\site-packages\conda\exceptions.py", line 479, in c
onda_exception_handler
        return_value = func(*args, **kwargs)
      File "C:\Miniconda3\lib\site-packages\conda\cli\main.py", line 145, in _ma
in
        exit_code = args.func(args, p)
      File "C:\Miniconda3\lib\site-packages\conda\cli\main_update.py", line 65,
in execute
        install(args, parser, 'update')
      File "C:\Miniconda3\lib\site-packages\conda\cli\install.py", line 308, in
install
        update_deps=context.update_dependencies)
      File "C:\Miniconda3\lib\site-packages\conda\plan.py", line 526, in install
_actions
        force=force, always_copy=always_copy)
      File "C:\Miniconda3\lib\site-packages\conda\plan.py", line 308, in ensure_
linked_actions
        fetched_in = is_fetched(dist)
      File "C:\Miniconda3\lib\site-packages\conda\install.py", line 727, in is_f
etched
        for fn in package_cache().get(dist, {}).get('files', ()):
      File "C:\Miniconda3\lib\site-packages\conda\install.py", line 675, in pack
age_cache
        add_cached_package(pdir, url)
      File "C:\Miniconda3\lib\site-packages\conda\install.py", line 633, in add_
cached_package
        schannel = Channel(url).canonical_name
      File "C:\Miniconda3\lib\site-packages\conda\models\channel.py", line 161,
in __call__
        c = Channel.from_value(value)
      File "C:\Miniconda3\lib\site-packages\conda\models\channel.py", line 211,
in from_value
        return Channel.from_url(value)
      File "C:\Miniconda3\lib\site-packages\conda\models\channel.py", line 196,
in from_url
        return parse_conda_channel_url(url)
      File "C:\Miniconda3\lib\site-packages\conda\models\channel.py", line 132,
in parse_conda_channel_url
        configured_token) = _read_channel_configuration(scheme, host, port, path
)
      File "C:\Miniconda3\lib\site-packages\conda\models\channel.py", line 122,
in _read_channel_configuration
        return (Url(host=host, port=port).url.rstrip('/'), path.strip('/') or No
ne,
    AttributeError: 'NoneType' object has no attribute 'strip'
```

regression bug: https://github.com/conda/conda/issues/3235 appears to have resurrected itself in another place
The bug was that conda handled the channel "http://conda-01" incorrectly. Here's the stack trace in conda 4.2.11:

```
    Traceback (most recent call last):
      File "C:\Miniconda3\lib\site-packages\conda\exceptions.py", line 479, in c
onda_exception_handler
        return_value = func(*args, **kwargs)
      File "C:\Miniconda3\lib\site-packages\conda\cli\main.py", line 145, in _ma
in
        exit_code = args.func(args, p)
      File "C:\Miniconda3\lib\site-packages\conda\cli\main_update.py", line 65,
in execute
        install(args, parser, 'update')
      File "C:\Miniconda3\lib\site-packages\conda\cli\install.py", line 308, in
install
        update_deps=context.update_dependencies)
      File "C:\Miniconda3\lib\site-packages\conda\plan.py", line 526, in install
_actions
        force=force, always_copy=always_copy)
      File "C:\Miniconda3\lib\site-packages\conda\plan.py", line 308, in ensure_
linked_actions
        fetched_in = is_fetched(dist)
      File "C:\Miniconda3\lib\site-packages\conda\install.py", line 727, in is_f
etched
        for fn in package_cache().get(dist, {}).get('files', ()):
      File "C:\Miniconda3\lib\site-packages\conda\install.py", line 675, in pack
age_cache
        add_cached_package(pdir, url)
      File "C:\Miniconda3\lib\site-packages\conda\install.py", line 633, in add_
cached_package
        schannel = Channel(url).canonical_name
      File "C:\Miniconda3\lib\site-packages\conda\models\channel.py", line 161,
in __call__
        c = Channel.from_value(value)
      File "C:\Miniconda3\lib\site-packages\conda\models\channel.py", line 211,
in from_value
        return Channel.from_url(value)
      File "C:\Miniconda3\lib\site-packages\conda\models\channel.py", line 196,
in from_url
        return parse_conda_channel_url(url)
      File "C:\Miniconda3\lib\site-packages\conda\models\channel.py", line 132,
in parse_conda_channel_url
        configured_token) = _read_channel_configuration(scheme, host, port, path
)
      File "C:\Miniconda3\lib\site-packages\conda\models\channel.py", line 122,
in _read_channel_configuration
        return (Url(host=host, port=port).url.rstrip('/'), path.strip('/') or No
ne,
    AttributeError: 'NoneType' object has no attribute 'strip'
```



Discussion:
Adding some print statements gives the following specific variable values from split_conda_url_easy_parts in the innermost calls:

```
url=http://conda-01/win-64/qt5-5.6.1.1-msvc2015rel_2.tar.bz2
host=conda-01
port=None
path=None
query=None
scheme=http
```

Yeah I know where the bug is.  4.2.11 introduced the requirement that every channel have a "name."  At least one directory level beyond the host and port.  I guess I didn't remember #3235, and was hoping everyone had at least one directory layer.

We need this to allow channels to have locations.  That is, we need to uniquely identify channels, but they also need to be able to move locations.  Each channel needs a deterministic name.  4.2.11 does that, but it assumes at least one directory level beyond the network location.

Cool, I guess the fix is to allow an empty string as the unique name, replacing None?

Maybe. I'm not sure just yet.  Thinking...

Need to chat with @mcg1969 about this. But I'm out of the office today.

@mwiebe How invasive would it be to your infrastructure to add one directory level?  Even if I hack a fix here, I think you're going to want to do this in the future.

We're progressively moving toward making "channel" a first-class-citizen in a package specification.  You'll be able to specify channel along with package on the command line.  Likely something similar to

```
conda install channel_name/package_name
```

Note there I have `channel_name`, because channels fundamentally need to move locations with proper configuration.  The no-channel-name/empty-channel-name case just really doesn't work here.

I'm still thinking on solutions though...

Either way this is definitely a bug.  Whether we ultimately find a way to handle no-name channels cleanly, or just need to alert that the specified channel is invalid.

It's definitely something we can do, just in a phased way. This bug basically trickled down to the "reinstall miniconda" fix.

In the system you're moving towards, how is a channel on anaconda.org distinguished from a channel on someone's local server? If we make http://conda-01/stable the location where we put packages once we're happy with them for development environments, how should that interact with the wholly separate anaconda/"defaults" set of channels?

There's very robust configuration for all of that.  New in 4.2.  The docs are behind; still working on those.

But, to answer your exact question, all you would do is add

```
custom_channels:
  stable: http://conda-01
```

in your condarc file.  (There are now multiple places for condarc configs, including in activated env directories.)

I'm seeing this also. I reference a channel of the form 'http://conda.ourserver.com' that I don't have control over right now. Any work around?

```
           platform : linux-64
      conda version : 4.2.11
   conda is private : False
  conda-env version : 4.2.11
conda-build version : 1.18.2
     python version : 2.7.11.final.0
   requests version : 2.11.1
```

Best workaround I've come up with is to wipe out all the contents of the `<anaconda>/pkgs` directory, then downgrade to 4.2.9.

Adding some print statements gives the following specific variable values from split_conda_url_easy_parts in the innermost calls:

```
url=http://conda-01/win-64/qt5-5.6.1.1-msvc2015rel_2.tar.bz2
host=conda-01
port=None
path=None
query=None
scheme=http
```

Yeah I know where the bug is.  4.2.11 introduced the requirement that every channel have a "name."  At least one directory level beyond the host and port.  I guess I didn't remember #3235, and was hoping everyone had at least one directory layer.

We need this to allow channels to have locations.  That is, we need to uniquely identify channels, but they also need to be able to move locations.  Each channel needs a deterministic name.  4.2.11 does that, but it assumes at least one directory level beyond the network location.

Cool, I guess the fix is to allow an empty string as the unique name, replacing None?

Maybe. I'm not sure just yet.  Thinking...

Need to chat with @mcg1969 about this. But I'm out of the office today.

@mwiebe How invasive would it be to your infrastructure to add one directory level?  Even if I hack a fix here, I think you're going to want to do this in the future.

We're progressively moving toward making "channel" a first-class-citizen in a package specification.  You'll be able to specify channel along with package on the command line.  Likely something similar to

```
conda install channel_name/package_name
```

Note there I have `channel_name`, because channels fundamentally need to move locations with proper configuration.  The no-channel-name/empty-channel-name case just really doesn't work here.

I'm still thinking on solutions though...

Either way this is definitely a bug.  Whether we ultimately find a way to handle no-name channels cleanly, or just need to alert that the specified channel is invalid.

It's definitely something we can do, just in a phased way. This bug basically trickled down to the "reinstall miniconda" fix.

In the system you're moving towards, how is a channel on anaconda.org distinguished from a channel on someone's local server? If we make http://conda-01/stable the location where we put packages once we're happy with them for development environments, how should that interact with the wholly separate anaconda/"defaults" set of channels?

There's very robust configuration for all of that.  New in 4.2.  The docs are behind; still working on those.

But, to answer your exact question, all you would do is add

```
custom_channels:
  stable: http://conda-01
```

in your condarc file.  (There are now multiple places for condarc configs, including in activated env directories.)

I'm seeing this also. I reference a channel of the form 'http://conda.ourserver.com' that I don't have control over right now. Any work around?

```
           platform : linux-64
      conda version : 4.2.11
   conda is private : False
  conda-env version : 4.2.11
conda-build version : 1.18.2
     python version : 2.7.11.final.0
   requests version : 2.11.1
```

Best workaround I've come up with is to wipe out all the contents of the `<anaconda>/pkgs` directory, then downgrade to 4.2.9.


Resolving patch:
diff --git a/conda/models/channel.py b/conda/models/channel.py
--- a/conda/models/channel.py
+++ b/conda/models/channel.py
@@ -73,9 +73,14 @@ def _get_channel_for_name_helper(name):
 def _read_channel_configuration(scheme, host, port, path):
     # return location, name, scheme, auth, token
 
-    test_url = Url(host=host, port=port, path=path).url.rstrip('/')
+    path = path and path.rstrip('/')
+    test_url = Url(host=host, port=port, path=path).url
 
-    # Step 1. migrated_custom_channels matches
+    # Step 1. No path given; channel name is None
+    if not path:
+        return Url(host=host, port=port).url.rstrip('/'), None, scheme or None, None, None
+
+    # Step 2. migrated_custom_channels matches
     for name, location in sorted(context.migrated_custom_channels.items(), reverse=True,
                                  key=lambda x: len(x[0])):
         location, _scheme, _auth, _token = split_scheme_auth_token(location)
@@ -86,14 +91,14 @@ def _read_channel_configuration(scheme, host, port, path):
             channel = _get_channel_for_name(channel_name)
             return channel.location, channel_name, channel.scheme, channel.auth, channel.token
 
-    # Step 2. migrated_channel_aliases matches
+    # Step 3. migrated_channel_aliases matches
     for migrated_alias in context.migrated_channel_aliases:
         if test_url.startswith(migrated_alias.location):
             name = test_url.replace(migrated_alias.location, '', 1).strip('/')
             ca = context.channel_alias
             return ca.location, name, ca.scheme, ca.auth, ca.token
 
-    # Step 3. custom_channels matches
+    # Step 4. custom_channels matches
     for name, channel in sorted(context.custom_channels.items(), reverse=True,
                                 key=lambda x: len(x[0])):
         that_test_url = join_url(channel.location, channel.name)
@@ -102,13 +107,13 @@ def _read_channel_configuration(scheme, host, port, path):
             return (channel.location, join_url(channel.name, subname), scheme,
                     channel.auth, channel.token)
 
-    # Step 4. channel_alias match
+    # Step 5. channel_alias match
     ca = context.channel_alias
     if ca.location and test_url.startswith(ca.location):
         name = test_url.replace(ca.location, '', 1).strip('/') or None
         return ca.location, name, scheme, ca.auth, ca.token
 
-    # Step 5. not-otherwise-specified file://-type urls
+    # Step 6. not-otherwise-specified file://-type urls
     if host is None:
         # this should probably only happen with a file:// type url
         assert port is None
@@ -118,7 +123,7 @@ def _read_channel_configuration(scheme, host, port, path):
         _scheme, _auth, _token = 'file', None, None
         return location, name, _scheme, _auth, _token
 
-    # Step 6. fall through to host:port as channel_location and path as channel_name
+    # Step 7. fall through to host:port as channel_location and path as channel_name
     return (Url(host=host, port=port).url.rstrip('/'), path.strip('/') or None,
             scheme or None, None, None)
 
@@ -250,7 +255,7 @@ def canonical_name(self):
                     return multiname
 
         for that_name in context.custom_channels:
-            if tokenized_startswith(self.name.split('/'), that_name.split('/')):
+            if self.name and tokenized_startswith(self.name.split('/'), that_name.split('/')):
                 return self.name
 
         if any(c.location == self.location
@@ -259,7 +264,7 @@ def canonical_name(self):
 
         # fall back to the equivalent of self.base_url
         # re-defining here because base_url for MultiChannel is None
-        return "%s://%s/%s" % (self.scheme, self.location, self.name)
+        return "%s://%s" % (self.scheme, join_url(self.location, self.name))
 
     def urls(self, with_credentials=False, platform=None):
         base = [self.location]


Test patch:
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,6 +5,7 @@
 from conda.base.context import context, reset_context
 from conda.common.compat import odict
 from conda.common.configuration import YamlRawParameter
+from conda.common.url import join_url
 from conda.common.yaml import yaml_load
 from conda.models.channel import Channel
 from conda.utils import on_win
@@ -85,6 +86,22 @@ def test_url_channel_w_platform(self):
             'https://repo.continuum.io/pkgs/free/noarch',
         ]
 
+    def test_bare_channel(self):
+        url = "http://conda-01"
+        channel = Channel(url)
+        assert channel.scheme == "http"
+        assert channel.location == "conda-01"
+        assert channel.platform is None
+        assert channel.canonical_name == url
+        assert channel.name is None
+
+        assert channel.base_url == url
+        assert channel.url() == join_url(url, context.subdir)
+        assert channel.urls() == [
+            join_url(url, context.subdir),
+            join_url(url, 'noarch'),
+        ]
+
 
 class AnacondaServerChannelTests(TestCase):