Repository: conda/conda

Issue:
Make the conda install table easier to read
The table of what packages will be installed and removed is hard to read. For one thing, it's hard to tell easily what packages are not removed but just upgraded or downgraded.   Also, the "link" terminology is confusing.

A suggestion by @jklowden:

```
$ conda update conda 
Updating Anaconda environment at /usr/local/anaconda  

The following packages will be downloaded:

    conda-2.2.3-py27_0.tar.bz2 
        [http://repo.continuum.io/pkgs/free/osx-64/]

The following packages will be upgraded:

    Old version               Replace with 
    ------------------------- ------------------------- 
    conda-1.4.4               conda-2.2.3

```

> or, if you really want the build (I don't, it's not meaningful to the user)

```
    package      Old version        New version 
    ------------ ------------------ ------------------  
    conda        1.4.4, py27_0      2.2.3, py27_0
```

I think the build is meaningful as it tells you what Python version is being used. It also tells you if you are using mkl. And also some people might use the build string to put other information which may be useful to users. 

<!---
@huboard:{"order":3.3142282405143226e-49,"custom_state":""}
-->



Discussion:

Resolving patch:
diff --git a/conda/cli/main_remove.py b/conda/cli/main_remove.py
--- a/conda/cli/main_remove.py
+++ b/conda/cli/main_remove.py
@@ -63,6 +63,7 @@ def execute(args, parser):
     from conda.api import get_index
     from conda.cli import pscheck
     from conda.install import rm_rf, linked
+    from conda import config
 
     if not (args.all or args.package_names):
         sys.exit('Error: no package names supplied,\n'
@@ -71,12 +72,11 @@ def execute(args, parser):
     prefix = common.get_prefix(args)
     common.check_write('remove', prefix)
 
-    index = None
+    common.ensure_override_channels_requires_channel(args)
+    channel_urls = args.channel or ()
+    index = get_index(channel_urls=channel_urls,
+                      prepend=not args.override_channels)
     if args.features:
-        common.ensure_override_channels_requires_channel(args)
-        channel_urls = args.channel or ()
-        index = get_index(channel_urls=channel_urls,
-                          prepend=not args.override_channels)
         features = set(args.package_names)
         actions = plan.remove_features_actions(prefix, index, features)
 
diff --git a/conda/plan.py b/conda/plan.py
--- a/conda/plan.py
+++ b/conda/plan.py
@@ -20,7 +20,7 @@
 from conda import install
 from conda.fetch import fetch_pkg
 from conda.history import History
-from conda.resolve import MatchSpec, Resolve
+from conda.resolve import MatchSpec, Resolve, Package
 from conda.utils import md5_file, human_bytes
 
 log = getLogger(__name__)
@@ -60,7 +60,7 @@ def split_linkarg(arg):
         linktype = install.LINK_HARD
     return dist, pkgs_dir, int(linktype)
 
-def display_actions(actions, index=None):
+def display_actions(actions, index):
     if actions.get(FETCH):
         print("\nThe following packages will be downloaded:\n")
 
@@ -79,19 +79,113 @@ def display_actions(actions, index=None):
             print(" " * 43 + "Total: %14s" %
                   human_bytes(sum(index[dist + '.tar.bz2']['size']
                                   for dist in actions[FETCH])))
-    if actions.get(UNLINK):
-        print("\nThe following packages will be UN-linked:\n")
-        print_dists([
-                (dist, None)
-                for dist in actions[UNLINK]])
-    if actions.get(LINK):
-        print("\nThe following packages will be linked:\n")
-        lst = []
-        for arg in actions[LINK]:
-            dist, pkgs_dir, lt = split_linkarg(arg)
-            extra = '   %s' % install.link_name_map.get(lt)
-            lst.append((dist, extra))
-        print_dists(lst)
+
+    # package -> [oldver-oldbuild, newver-newbuild]
+    packages = defaultdict(lambda: list(('', '')))
+    features = defaultdict(lambda: list(('', '')))
+
+    # This assumes each package will appear in LINK no more than once.
+    Packages = {}
+    linktypes = {}
+    for arg in actions.get(LINK, []):
+        dist, pkgs_dir, lt =  split_linkarg(arg)
+        pkg, ver, build = dist.rsplit('-', 2)
+        packages[pkg][1] = ver + '-' + build
+        Packages[dist] = Package(dist + '.tar.bz2', index[dist + '.tar.bz2'])
+        linktypes[pkg] = lt
+        features[pkg][1] = index[dist + '.tar.bz2'].get('features', '')
+    for arg in actions.get(UNLINK, []):
+        dist, pkgs_dir, lt =  split_linkarg(arg)
+        pkg, ver, build = dist.rsplit('-', 2)
+        packages[pkg][0] = ver + '-' + build
+        Packages[dist] = Package(dist + '.tar.bz2', index[dist + '.tar.bz2'])
+        features[pkg][0] = index[dist + '.tar.bz2'].get('features', '')
+
+    #             Put a minimum length here---.    .--For the :
+    #                                         v    v
+    maxpkg = max(len(max(packages or [''], key=len)), 0) + 1
+    maxoldver = len(max(packages.values() or [['']], key=lambda i: len(i[0]))[0])
+    maxnewver = len(max(packages.values() or [['', '']], key=lambda i: len(i[1]))[1])
+    maxoldfeatures = len(max(features.values() or [['']], key=lambda i: len(i[0]))[0])
+    maxnewfeatures = len(max(features.values() or [['', '']], key=lambda i: len(i[1]))[1])
+    maxoldchannel = len(max([config.canonical_channel_name(Packages[pkg + '-' +
+        packages[pkg][0]].channel) for pkg in packages if packages[pkg][0]] or
+        [''], key=len))
+    maxnewchannel = len(max([config.canonical_channel_name(Packages[pkg + '-' +
+        packages[pkg][1]].channel) for pkg in packages if packages[pkg][1]] or
+        [''], key=len))
+    new = {pkg for pkg in packages if not packages[pkg][0]}
+    removed = {pkg for pkg in packages if not packages[pkg][1]}
+    updated = set()
+    downgraded = set()
+    oldfmt = {}
+    newfmt = {}
+    for pkg in packages:
+        # That's right. I'm using old-style string formatting to generate a
+        # string with new-style string formatting.
+        oldfmt[pkg] = '{pkg:<%s} {vers[0]:<%s}' % (maxpkg, maxoldver)
+        if config.show_channel_urls:
+            oldfmt[pkg] += ' {channel[0]:<%s}' % maxoldchannel
+        if packages[pkg][0]:
+            newfmt[pkg] = '{vers[1]:<%s}' % maxnewver
+        else:
+            newfmt[pkg] = '{pkg:<%s} {vers[1]:<%s}' % (maxpkg, maxnewver)
+        if config.show_channel_urls:
+            newfmt[pkg] += ' {channel[1]:<%s}' % maxnewchannel
+        # TODO: Should we also care about the old package's link type?
+        if pkg in linktypes and linktypes[pkg] != install.LINK_HARD:
+            newfmt[pkg] += ' (%s)' % install.link_name_map[linktypes[pkg]]
+
+        if features[pkg][0]:
+            oldfmt[pkg] += ' [{features[0]:<%s}]' % maxoldfeatures
+        if features[pkg][1]:
+            newfmt[pkg] += ' [{features[1]:<%s}]' % maxnewfeatures
+
+        if pkg in new or pkg in removed:
+            continue
+        P0 = Packages[pkg + '-' + packages[pkg][0]]
+        P1 = Packages[pkg + '-' + packages[pkg][1]]
+        try:
+            # <= here means that unchanged packages will be put in updated
+            newer = (P0.name, P0.norm_version, P0.build_number) <= (P1.name, P1.norm_version, P1.build_number)
+        except TypeError:
+            newer = (P0.name, P0.version, P0.build_number) <= (P1.name, P1.version, P1.build_number)
+        if newer:
+            updated.add(pkg)
+        else:
+            downgraded.add(pkg)
+
+    arrow = ' --> '
+    lead = ' '*4
+
+    def format(s, pkg):
+        channel = ['', '']
+        for i in range(2):
+            if packages[pkg][i]:
+                channel[i] = config.canonical_channel_name(Packages[pkg + '-' + packages[pkg][i]].channel)
+        return lead + s.format(pkg=pkg+':', vers=packages[pkg],
+            channel=channel, features=features[pkg])
+
+    if new:
+        print("\nThe following NEW packages will be INSTALLED:\n")
+    for pkg in sorted(new):
+        print(format(newfmt[pkg], pkg))
+
+    if removed:
+        print("\nThe following packages will be REMOVED:\n")
+    for pkg in sorted(removed):
+        print(format(oldfmt[pkg], pkg))
+
+    if updated:
+        print("\nThe following packages will be UPDATED:\n")
+    for pkg in sorted(updated):
+        print(format(oldfmt[pkg] + arrow + newfmt[pkg], pkg))
+
+    if downgraded:
+        print("\nThe following packages will be DOWNGRADED:\n")
+    for pkg in sorted(downgraded):
+        print(format(oldfmt[pkg] + arrow + newfmt[pkg], pkg))
+
     print()
 
 # the order matters here, don't change it


Test patch:
diff --git a/tests/helpers.py b/tests/helpers.py
--- a/tests/helpers.py
+++ b/tests/helpers.py
@@ -5,6 +5,8 @@
 import sys
 import os
 
+from contextlib import contextmanager
+
 def raises(exception, func, string=None):
     try:
         a = func()
@@ -35,3 +37,31 @@ def run_conda_command(*args):
     stdout, stderr = p.communicate()
     return (stdout.decode('utf-8').replace('\r\n', '\n'),
         stderr.decode('utf-8').replace('\r\n', '\n'))
+
+class CapturedText(object):
+    pass
+
+@contextmanager
+def captured():
+    """
+    Context manager to capture the printed output of the code in the with block
+
+    Bind the context manager to a variable using `as` and the result will be
+    in the stdout property.
+
+    >>> from tests.helpers import capture
+    >>> with captured() as c:
+    ...     print('hello world!')
+    ...
+    >>> c.stdout
+    'hello world!\n'
+    """
+    from conda.compat import StringIO
+    import sys
+
+    stdout = sys.stdout
+    sys.stdout = file = StringIO()
+    c = CapturedText()
+    yield c
+    c.stdout = file.getvalue()
+    sys.stdout = stdout
diff --git a/tests/test_plan.py b/tests/test_plan.py
--- a/tests/test_plan.py
+++ b/tests/test_plan.py
@@ -1,15 +1,20 @@
 import json
 import unittest
 from os.path import dirname, join
+from collections import defaultdict
 
 from conda.config import default_python, pkgs_dirs
+import conda.config
 from conda.install import LINK_HARD
 import conda.plan as plan
+from conda.plan import display_actions
 from conda.resolve import Resolve
 
+from tests.helpers import captured
 
 with open(join(dirname(__file__), 'index.json')) as fi:
-    r = Resolve(json.load(fi))
+    index = json.load(fi)
+    r = Resolve(index)
 
 def solve(specs):
     return [fn[:-8] for fn in r.solve(specs)]
@@ -77,3 +82,685 @@ def test_4(self):
             (['anaconda', 'python 3*'], []),
             ]:
             self.check(specs, added)
+
+def test_display_actions():
+    conda.config.show_channel_urls = False
+    actions = defaultdict(list, {"FETCH": ['sympy-0.7.2-py27_0',
+        "numpy-1.7.1-py27_0"]})
+    # The older test index doesn't have the size metadata
+    index['sympy-0.7.2-py27_0.tar.bz2']['size'] = 4374752
+    index["numpy-1.7.1-py27_0.tar.bz2"]['size'] = 5994338
+
+    with captured() as c:
+        display_actions(actions, index)
+
+    assert c.stdout == """
+The following packages will be downloaded:
+
+    package                    |            build
+    ---------------------------|-----------------
+    sympy-0.7.2                |           py27_0         4.2 MB
+    numpy-1.7.1                |           py27_0         5.7 MB
+    ------------------------------------------------------------
+                                           Total:         9.9 MB
+
+"""
+
+    actions = defaultdict(list, {'PREFIX':
+    '/Users/aaronmeurer/anaconda/envs/test', 'SYMLINK_CONDA':
+    ['/Users/aaronmeurer/anaconda'], 'LINK': ['python-3.3.2-0', 'readline-6.2-0 /Users/aaronmeurer/anaconda/pkgs 1', 'sqlite-3.7.13-0 /Users/aaronmeurer/anaconda/pkgs 1', 'tk-8.5.13-0 /Users/aaronmeurer/anaconda/pkgs 1', 'zlib-1.2.7-0 /Users/aaronmeurer/anaconda/pkgs 1']})
+
+    with captured() as c:
+        display_actions(actions, index)
+
+    assert c.stdout == """
+The following NEW packages will be INSTALLED:
+
+    python:   3.3.2-0 \n\
+    readline: 6.2-0   \n\
+    sqlite:   3.7.13-0
+    tk:       8.5.13-0
+    zlib:     1.2.7-0 \n\
+
+"""
+
+    actions['UNLINK'] = actions['LINK']
+    actions['LINK'] = []
+
+    with captured() as c:
+        display_actions(actions, index)
+
+
+    assert c.stdout == """
+The following packages will be REMOVED:
+
+    python:   3.3.2-0 \n\
+    readline: 6.2-0   \n\
+    sqlite:   3.7.13-0
+    tk:       8.5.13-0
+    zlib:     1.2.7-0 \n\
+
+"""
+
+
+    actions = defaultdict(list, {'LINK': ['cython-0.19.1-py33_0'], 'UNLINK':
+    ['cython-0.19-py33_0']})
+
+    with captured() as c:
+        display_actions(actions, index)
+
+    assert c.stdout == """
+The following packages will be UPDATED:
+
+    cython: 0.19-py33_0 --> 0.19.1-py33_0
+
+"""
+
+    actions['LINK'], actions['UNLINK'] = actions['UNLINK'], actions['LINK']
+
+    with captured() as c:
+        display_actions(actions, index)
+
+    assert c.stdout == """
+The following packages will be DOWNGRADED:
+
+    cython: 0.19.1-py33_0 --> 0.19-py33_0
+
+"""
+
+    actions = defaultdict(list, {'LINK': ['cython-0.19.1-py33_0',
+        'dateutil-1.5-py33_0', 'numpy-1.7.1-py33_0'], 'UNLINK':
+        ['cython-0.19-py33_0', 'dateutil-2.1-py33_1', 'pip-1.3.1-py33_1']})
+
+    with captured() as c:
+        display_actions(actions, index)
+
+    assert c.stdout == """
+The following NEW packages will be INSTALLED:
+
+    numpy:    1.7.1-py33_0 \n\
+
+The following packages will be REMOVED:
+
+    pip:      1.3.1-py33_1
+
+The following packages will be UPDATED:
+
+    cython:   0.19-py33_0  --> 0.19.1-py33_0
+
+The following packages will be DOWNGRADED:
+
+    dateutil: 2.1-py33_1   --> 1.5-py33_0   \n\
+
+"""
+
+
+    actions = defaultdict(list, {'LINK': ['cython-0.19.1-py33_0',
+        'dateutil-2.1-py33_1'], 'UNLINK':  ['cython-0.19-py33_0',
+            'dateutil-1.5-py33_0']})
+
+    with captured() as c:
+        display_actions(actions, index)
+
+    assert c.stdout == """
+The following packages will be UPDATED:
+
+    cython:   0.19-py33_0 --> 0.19.1-py33_0
+    dateutil: 1.5-py33_0  --> 2.1-py33_1   \n\
+
+"""
+
+    actions['LINK'], actions['UNLINK'] = actions['UNLINK'], actions['LINK']
+
+
+    with captured() as c:
+        display_actions(actions, index)
+
+    assert c.stdout == """
+The following packages will be DOWNGRADED:
+
+    cython:   0.19.1-py33_0 --> 0.19-py33_0
+    dateutil: 2.1-py33_1    --> 1.5-py33_0 \n\
+
+"""
+
+
+
+def test_display_actions_show_channel_urls():
+    conda.config.show_channel_urls = True
+    actions = defaultdict(list, {"FETCH": ['sympy-0.7.2-py27_0',
+        "numpy-1.7.1-py27_0"]})
+    # The older test index doesn't have the size metadata
+    index['sympy-0.7.2-py27_0.tar.bz2']['size'] = 4374752
+    index["numpy-1.7.1-py27_0.tar.bz2"]['size'] = 5994338
+
+    with captured() as c:
+        display_actions(actions, index)
+
+    assert c.stdout == """
+The following packages will be downloaded:
+
+    package                    |            build
+    ---------------------------|-----------------
+    sympy-0.7.2                |           py27_0         4.2 MB  <unknown>
+    numpy-1.7.1                |           py27_0         5.7 MB  <unknown>
+    ------------------------------------------------------------
+                                           Total:         9.9 MB
+
+"""
+
+
+    actions = defaultdict(list, {'PREFIX':
+    '/Users/aaronmeurer/anaconda/envs/test', 'SYMLINK_CONDA':
+    ['/Users/aaronmeurer/anaconda'], 'LINK': ['python-3.3.2-0', 'readline-6.2-0 /Users/aaronmeurer/anaconda/pkgs 1', 'sqlite-3.7.13-0 /Users/aaronmeurer/anaconda/pkgs 1', 'tk-8.5.13-0 /Users/aaronmeurer/anaconda/pkgs 1', 'zlib-1.2.7-0 /Users/aaronmeurer/anaconda/pkgs 1']})
+
+    with captured() as c:
+        display_actions(actions, index)
+
+    assert c.stdout == """
+The following NEW packages will be INSTALLED:
+
+    python:   3.3.2-0  <unknown>
+    readline: 6.2-0    <unknown>
+    sqlite:   3.7.13-0 <unknown>
+    tk:       8.5.13-0 <unknown>
+    zlib:     1.2.7-0  <unknown>
+
+"""
+
+    actions['UNLINK'] = actions['LINK']
+    actions['LINK'] = []
+
+    with captured() as c:
+        display_actions(actions, index)
+
+
+    assert c.stdout == """
+The following packages will be REMOVED:
+
+    python:   3.3.2-0  <unknown>
+    readline: 6.2-0    <unknown>
+    sqlite:   3.7.13-0 <unknown>
+    tk:       8.5.13-0 <unknown>
+    zlib:     1.2.7-0  <unknown>
+
+"""
+
+
+    actions = defaultdict(list, {'LINK': ['cython-0.19.1-py33_0'], 'UNLINK':
+    ['cython-0.19-py33_0']})
+
+    with captured() as c:
+        display_actions(actions, index)
+
+    assert c.stdout == """
+The following packages will be UPDATED:
+
+    cython: 0.19-py33_0 <unknown> --> 0.19.1-py33_0 <unknown>
+
+"""
+
+    actions['LINK'], actions['UNLINK'] = actions['UNLINK'], actions['LINK']
+
+
+    with captured() as c:
+        display_actions(actions, index)
+
+    assert c.stdout == """
+The following packages will be DOWNGRADED:
+
+    cython: 0.19.1-py33_0 <unknown> --> 0.19-py33_0 <unknown>
+
+"""
+
+
+    actions = defaultdict(list, {'LINK': ['cython-0.19.1-py33_0',
+        'dateutil-1.5-py33_0', 'numpy-1.7.1-py33_0'], 'UNLINK':
+        ['cython-0.19-py33_0', 'dateutil-2.1-py33_1', 'pip-1.3.1-py33_1']})
+
+    with captured() as c:
+        display_actions(actions, index)
+
+    assert c.stdout == """
+The following NEW packages will be INSTALLED:
+
+    numpy:    1.7.1-py33_0  <unknown>
+
+The following packages will be REMOVED:
+
+    pip:      1.3.1-py33_1 <unknown>
+
+The following packages will be UPDATED:
+
+    cython:   0.19-py33_0  <unknown> --> 0.19.1-py33_0 <unknown>
+
+The following packages will be DOWNGRADED:
+
+    dateutil: 2.1-py33_1   <unknown> --> 1.5-py33_0    <unknown>
+
+"""
+
+
+    actions = defaultdict(list, {'LINK': ['cython-0.19.1-py33_0',
+        'dateutil-2.1-py33_1'], 'UNLINK':  ['cython-0.19-py33_0',
+            'dateutil-1.5-py33_0']})
+
+    with captured() as c:
+        display_actions(actions, index)
+
+    assert c.stdout == """
+The following packages will be UPDATED:
+
+    cython:   0.19-py33_0 <unknown> --> 0.19.1-py33_0 <unknown>
+    dateutil: 1.5-py33_0  <unknown> --> 2.1-py33_1    <unknown>
+
+"""
+
+    actions['LINK'], actions['UNLINK'] = actions['UNLINK'], actions['LINK']
+
+    with captured() as c:
+        display_actions(actions, index)
+
+    assert c.stdout == """
+The following packages will be DOWNGRADED:
+
+    cython:   0.19.1-py33_0 <unknown> --> 0.19-py33_0 <unknown>
+    dateutil: 2.1-py33_1    <unknown> --> 1.5-py33_0  <unknown>
+
+"""
+
+    actions['LINK'], actions['UNLINK'] = actions['UNLINK'], actions['LINK']
+
+    index['cython-0.19.1-py33_0.tar.bz2']['channel'] = 'my_channel'
+    index['dateutil-1.5-py33_0.tar.bz2']['channel'] = 'my_channel'
+
+    with captured() as c:
+        display_actions(actions, index)
+
+    assert c.stdout == """
+The following packages will be UPDATED:
+
+    cython:   0.19-py33_0 <unknown>  --> 0.19.1-py33_0 my_channel
+    dateutil: 1.5-py33_0  my_channel --> 2.1-py33_1    <unknown> \n\
+
+"""
+
+    actions['LINK'], actions['UNLINK'] = actions['UNLINK'], actions['LINK']
+
+
+    with captured() as c:
+        display_actions(actions, index)
+
+    assert c.stdout == """
+The following packages will be DOWNGRADED:
+
+    cython:   0.19.1-py33_0 my_channel --> 0.19-py33_0 <unknown> \n\
+    dateutil: 2.1-py33_1    <unknown>  --> 1.5-py33_0  my_channel
+
+"""
+
+
+def test_display_actions_link_type():
+    conda.config.show_channel_urls = False
+
+    actions = defaultdict(list, {'LINK': ['cython-0.19.1-py33_0 /Users/aaronmeurer/anaconda/pkgs 2', 'dateutil-1.5-py33_0 /Users/aaronmeurer/anaconda/pkgs 2',
+    'numpy-1.7.1-py33_0 /Users/aaronmeurer/anaconda/pkgs 2', 'python-3.3.2-0 /Users/aaronmeurer/anaconda/pkgs 2', 'readline-6.2-0 /Users/aaronmeurer/anaconda/pkgs 2', 'sqlite-3.7.13-0 /Users/aaronmeurer/anaconda/pkgs 2', 'tk-8.5.13-0 /Users/aaronmeurer/anaconda/pkgs 2', 'zlib-1.2.7-0 /Users/aaronmeurer/anaconda/pkgs 2']})
+
+    with captured() as c:
+        display_actions(actions, index)
+
+    assert c.stdout == """
+The following NEW packages will be INSTALLED:
+
+    cython:   0.19.1-py33_0 (soft-link)
+    dateutil: 1.5-py33_0    (soft-link)
+    numpy:    1.7.1-py33_0  (soft-link)
+    python:   3.3.2-0       (soft-link)
+    readline: 6.2-0         (soft-link)
+    sqlite:   3.7.13-0      (soft-link)
+    tk:       8.5.13-0      (soft-link)
+    zlib:     1.2.7-0       (soft-link)
+
+"""
+
+    actions = defaultdict(list, {'LINK': ['cython-0.19.1-py33_0 /Users/aaronmeurer/anaconda/pkgs 2',
+        'dateutil-2.1-py33_1 /Users/aaronmeurer/anaconda/pkgs 2'], 'UNLINK':  ['cython-0.19-py33_0',
+            'dateutil-1.5-py33_0']})
+
+    with captured() as c:
+        display_actions(actions, index)
+
+    assert c.stdout == """
+The following packages will be UPDATED:
+
+    cython:   0.19-py33_0 --> 0.19.1-py33_0 (soft-link)
+    dateutil: 1.5-py33_0  --> 2.1-py33_1    (soft-link)
+
+"""
+
+    actions = defaultdict(list, {'LINK': ['cython-0.19-py33_0 /Users/aaronmeurer/anaconda/pkgs 2',
+        'dateutil-1.5-py33_0 /Users/aaronmeurer/anaconda/pkgs 2'], 'UNLINK':  ['cython-0.19.1-py33_0',
+            'dateutil-2.1-py33_1']})
+
+    with captured() as c:
+        display_actions(actions, index)
+
+    assert c.stdout == """
+The following packages will be DOWNGRADED:
+
+    cython:   0.19.1-py33_0 --> 0.19-py33_0 (soft-link)
+    dateutil: 2.1-py33_1    --> 1.5-py33_0  (soft-link)
+
+"""
+
+    actions = defaultdict(list, {'LINK': ['cython-0.19.1-py33_0 /Users/aaronmeurer/anaconda/pkgs 1', 'dateutil-1.5-py33_0 /Users/aaronmeurer/anaconda/pkgs 1',
+    'numpy-1.7.1-py33_0 /Users/aaronmeurer/anaconda/pkgs 1', 'python-3.3.2-0 /Users/aaronmeurer/anaconda/pkgs 1', 'readline-6.2-0 /Users/aaronmeurer/anaconda/pkgs 1', 'sqlite-3.7.13-0 /Users/aaronmeurer/anaconda/pkgs 1', 'tk-8.5.13-0 /Users/aaronmeurer/anaconda/pkgs 1', 'zlib-1.2.7-0 /Users/aaronmeurer/anaconda/pkgs 1']})
+
+    with captured() as c:
+        display_actions(actions, index)
+
+    assert c.stdout == """
+The following NEW packages will be INSTALLED:
+
+    cython:   0.19.1-py33_0
+    dateutil: 1.5-py33_0   \n\
+    numpy:    1.7.1-py33_0 \n\
+    python:   3.3.2-0      \n\
+    readline: 6.2-0        \n\
+    sqlite:   3.7.13-0     \n\
+    tk:       8.5.13-0     \n\
+    zlib:     1.2.7-0      \n\
+
+"""
+
+    actions = defaultdict(list, {'LINK': ['cython-0.19.1-py33_0 /Users/aaronmeurer/anaconda/pkgs 1',
+        'dateutil-2.1-py33_1 /Users/aaronmeurer/anaconda/pkgs 1'], 'UNLINK':  ['cython-0.19-py33_0',
+            'dateutil-1.5-py33_0']})
+
+    with captured() as c:
+        display_actions(actions, index)
+
+    assert c.stdout == """
+The following packages will be UPDATED:
+
+    cython:   0.19-py33_0 --> 0.19.1-py33_0
+    dateutil: 1.5-py33_0  --> 2.1-py33_1   \n\
+
+"""
+
+    actions = defaultdict(list, {'LINK': ['cython-0.19-py33_0 /Users/aaronmeurer/anaconda/pkgs 1',
+        'dateutil-1.5-py33_0 /Users/aaronmeurer/anaconda/pkgs 1'], 'UNLINK':  ['cython-0.19.1-py33_0',
+            'dateutil-2.1-py33_1']})
+
+    with captured() as c:
+        display_actions(actions, index)
+
+    assert c.stdout == """
+The following packages will be DOWNGRADED:
+
+    cython:   0.19.1-py33_0 --> 0.19-py33_0
+    dateutil: 2.1-py33_1    --> 1.5-py33_0 \n\
+
+"""
+
+    actions = defaultdict(list, {'LINK': ['cython-0.19.1-py33_0 /Users/aaronmeurer/anaconda/pkgs 3', 'dateutil-1.5-py33_0 /Users/aaronmeurer/anaconda/pkgs 3',
+    'numpy-1.7.1-py33_0 /Users/aaronmeurer/anaconda/pkgs 3', 'python-3.3.2-0 /Users/aaronmeurer/anaconda/pkgs 3', 'readline-6.2-0 /Users/aaronmeurer/anaconda/pkgs 3', 'sqlite-3.7.13-0 /Users/aaronmeurer/anaconda/pkgs 3', 'tk-8.5.13-0 /Users/aaronmeurer/anaconda/pkgs 3', 'zlib-1.2.7-0 /Users/aaronmeurer/anaconda/pkgs 3']})
+
+    with captured() as c:
+        display_actions(actions, index)
+
+    assert c.stdout == """
+The following NEW packages will be INSTALLED:
+
+    cython:   0.19.1-py33_0 (copy)
+    dateutil: 1.5-py33_0    (copy)
+    numpy:    1.7.1-py33_0  (copy)
+    python:   3.3.2-0       (copy)
+    readline: 6.2-0         (copy)
+    sqlite:   3.7.13-0      (copy)
+    tk:       8.5.13-0      (copy)
+    zlib:     1.2.7-0       (copy)
+
+"""
+
+    actions = defaultdict(list, {'LINK': ['cython-0.19.1-py33_0 /Users/aaronmeurer/anaconda/pkgs 3',
+        'dateutil-2.1-py33_1 /Users/aaronmeurer/anaconda/pkgs 3'], 'UNLINK':  ['cython-0.19-py33_0',
+            'dateutil-1.5-py33_0']})
+
+    with captured() as c:
+        display_actions(actions, index)
+
+    assert c.stdout == """
+The following packages will be UPDATED:
+
+    cython:   0.19-py33_0 --> 0.19.1-py33_0 (copy)
+    dateutil: 1.5-py33_0  --> 2.1-py33_1    (copy)
+
+"""
+
+    actions = defaultdict(list, {'LINK': ['cython-0.19-py33_0 /Users/aaronmeurer/anaconda/pkgs 3',
+        'dateutil-1.5-py33_0 /Users/aaronmeurer/anaconda/pkgs 3'], 'UNLINK':  ['cython-0.19.1-py33_0',
+            'dateutil-2.1-py33_1']})
+
+    with captured() as c:
+        display_actions(actions, index)
+
+    assert c.stdout == """
+The following packages will be DOWNGRADED:
+
+    cython:   0.19.1-py33_0 --> 0.19-py33_0 (copy)
+    dateutil: 2.1-py33_1    --> 1.5-py33_0  (copy)
+
+"""
+
+    conda.config.show_channel_urls = True
+
+    index['cython-0.19.1-py33_0.tar.bz2']['channel'] = 'my_channel'
+    index['dateutil-1.5-py33_0.tar.bz2']['channel'] = 'my_channel'
+
+    actions = defaultdict(list, {'LINK': ['cython-0.19.1-py33_0 /Users/aaronmeurer/anaconda/pkgs 3', 'dateutil-1.5-py33_0 /Users/aaronmeurer/anaconda/pkgs 3',
+    'numpy-1.7.1-py33_0 /Users/aaronmeurer/anaconda/pkgs 3', 'python-3.3.2-0 /Users/aaronmeurer/anaconda/pkgs 3', 'readline-6.2-0 /Users/aaronmeurer/anaconda/pkgs 3', 'sqlite-3.7.13-0 /Users/aaronmeurer/anaconda/pkgs 3', 'tk-8.5.13-0 /Users/aaronmeurer/anaconda/pkgs 3', 'zlib-1.2.7-0 /Users/aaronmeurer/anaconda/pkgs 3']})
+
+    with captured() as c:
+        display_actions(actions, index)
+
+    assert c.stdout == """
+The following NEW packages will be INSTALLED:
+
+    cython:   0.19.1-py33_0 my_channel (copy)
+    dateutil: 1.5-py33_0    my_channel (copy)
+    numpy:    1.7.1-py33_0  <unknown>  (copy)
+    python:   3.3.2-0       <unknown>  (copy)
+    readline: 6.2-0         <unknown>  (copy)
+    sqlite:   3.7.13-0      <unknown>  (copy)
+    tk:       8.5.13-0      <unknown>  (copy)
+    zlib:     1.2.7-0       <unknown>  (copy)
+
+"""
+
+    actions = defaultdict(list, {'LINK': ['cython-0.19.1-py33_0 /Users/aaronmeurer/anaconda/pkgs 3',
+        'dateutil-2.1-py33_1 /Users/aaronmeurer/anaconda/pkgs 3'], 'UNLINK':  ['cython-0.19-py33_0',
+            'dateutil-1.5-py33_0']})
+
+    with captured() as c:
+        display_actions(actions, index)
+
+    assert c.stdout == """
+The following packages will be UPDATED:
+
+    cython:   0.19-py33_0 <unknown>  --> 0.19.1-py33_0 my_channel (copy)
+    dateutil: 1.5-py33_0  my_channel --> 2.1-py33_1    <unknown>  (copy)
+
+"""
+
+    actions = defaultdict(list, {'LINK': ['cython-0.19-py33_0 /Users/aaronmeurer/anaconda/pkgs 3',
+        'dateutil-1.5-py33_0 /Users/aaronmeurer/anaconda/pkgs 3'], 'UNLINK':  ['cython-0.19.1-py33_0',
+            'dateutil-2.1-py33_1']})
+
+    with captured() as c:
+        display_actions(actions, index)
+
+    assert c.stdout == """
+The following packages will be DOWNGRADED:
+
+    cython:   0.19.1-py33_0 my_channel --> 0.19-py33_0 <unknown>  (copy)
+    dateutil: 2.1-py33_1    <unknown>  --> 1.5-py33_0  my_channel (copy)
+
+"""
+
+def test_display_actions_features():
+    conda.config.show_channel_urls = False
+
+    actions = defaultdict(list, {'LINK': ['numpy-1.7.1-py33_p0', 'cython-0.19-py33_0']})
+
+    with captured() as c:
+        display_actions(actions, index)
+
+    assert c.stdout == """
+The following NEW packages will be INSTALLED:
+
+    cython: 0.19-py33_0  \n\
+    numpy:  1.7.1-py33_p0 [mkl]
+
+"""
+
+    actions = defaultdict(list, {'UNLINK': ['numpy-1.7.1-py33_p0', 'cython-0.19-py33_0']})
+
+    with captured() as c:
+        display_actions(actions, index)
+
+    assert c.stdout == """
+The following packages will be REMOVED:
+
+    cython: 0.19-py33_0  \n\
+    numpy:  1.7.1-py33_p0 [mkl]
+
+"""
+
+    actions = defaultdict(list, {'UNLINK': ['numpy-1.7.1-py33_p0'], 'LINK': ['numpy-1.7.0-py33_p0']})
+
+    with captured() as c:
+        display_actions(actions, index)
+
+    assert c.stdout == """
+The following packages will be DOWNGRADED:
+
+    numpy: 1.7.1-py33_p0 [mkl] --> 1.7.0-py33_p0 [mkl]
+
+"""
+
+    actions = defaultdict(list, {'LINK': ['numpy-1.7.1-py33_p0'], 'UNLINK': ['numpy-1.7.0-py33_p0']})
+
+    with captured() as c:
+        display_actions(actions, index)
+
+    assert c.stdout == """
+The following packages will be UPDATED:
+
+    numpy: 1.7.0-py33_p0 [mkl] --> 1.7.1-py33_p0 [mkl]
+
+"""
+
+    actions = defaultdict(list, {'LINK': ['numpy-1.7.1-py33_p0'], 'UNLINK': ['numpy-1.7.1-py33_0']})
+
+    with captured() as c:
+        display_actions(actions, index)
+
+    # NB: Packages whose version do not changed are put in UPDATED
+    assert c.stdout == """
+The following packages will be UPDATED:
+
+    numpy: 1.7.1-py33_0 --> 1.7.1-py33_p0 [mkl]
+
+"""
+
+    actions = defaultdict(list, {'UNLINK': ['numpy-1.7.1-py33_p0'], 'LINK': ['numpy-1.7.1-py33_0']})
+
+    with captured() as c:
+        display_actions(actions, index)
+
+    assert c.stdout == """
+The following packages will be UPDATED:
+
+    numpy: 1.7.1-py33_p0 [mkl] --> 1.7.1-py33_0
+
+"""
+
+    conda.config.show_channel_urls = True
+
+    actions = defaultdict(list, {'LINK': ['numpy-1.7.1-py33_p0', 'cython-0.19-py33_0']})
+
+    with captured() as c:
+        display_actions(actions, index)
+
+    assert c.stdout == """
+The following NEW packages will be INSTALLED:
+
+    cython: 0.19-py33_0   <unknown>
+    numpy:  1.7.1-py33_p0 <unknown> [mkl]
+
+"""
+
+
+    actions = defaultdict(list, {'UNLINK': ['numpy-1.7.1-py33_p0', 'cython-0.19-py33_0']})
+
+    with captured() as c:
+        display_actions(actions, index)
+
+    assert c.stdout == """
+The following packages will be REMOVED:
+
+    cython: 0.19-py33_0   <unknown>
+    numpy:  1.7.1-py33_p0 <unknown> [mkl]
+
+"""
+
+    actions = defaultdict(list, {'UNLINK': ['numpy-1.7.1-py33_p0'], 'LINK': ['numpy-1.7.0-py33_p0']})
+
+    with captured() as c:
+        display_actions(actions, index)
+
+    assert c.stdout == """
+The following packages will be DOWNGRADED:
+
+    numpy: 1.7.1-py33_p0 <unknown> [mkl] --> 1.7.0-py33_p0 <unknown> [mkl]
+
+"""
+
+    actions = defaultdict(list, {'LINK': ['numpy-1.7.1-py33_p0'], 'UNLINK': ['numpy-1.7.0-py33_p0']})
+
+    with captured() as c:
+        display_actions(actions, index)
+
+    assert c.stdout == """
+The following packages will be UPDATED:
+
+    numpy: 1.7.0-py33_p0 <unknown> [mkl] --> 1.7.1-py33_p0 <unknown> [mkl]
+
+"""
+
+
+    actions = defaultdict(list, {'LINK': ['numpy-1.7.1-py33_p0'], 'UNLINK': ['numpy-1.7.1-py33_0']})
+
+    with captured() as c:
+        display_actions(actions, index)
+
+    # NB: Packages whose version do not changed are put in UPDATED
+    assert c.stdout == """
+The following packages will be UPDATED:
+
+    numpy: 1.7.1-py33_0 <unknown> --> 1.7.1-py33_p0 <unknown> [mkl]
+
+"""
+
+    actions = defaultdict(list, {'UNLINK': ['numpy-1.7.1-py33_p0'], 'LINK': ['numpy-1.7.1-py33_0']})
+
+    with captured() as c:
+        display_actions(actions, index)
+
+    assert c.stdout == """
+The following packages will be UPDATED:
+
+    numpy: 1.7.1-py33_p0 <unknown> [mkl] --> 1.7.1-py33_0 <unknown>
+
+"""