Repository: pypa/pip

Issue:
Is there any desire for a "pretty print" output of "pip list"?
- Pip version: 8.1.1
- Python version: Python 3.4.3 x64
- Operating System: Windows 7
### Description:

Is there any desire for a "pretty print" output of `pip list`?

I'm willing and able to do the work for this, but I'd rather not bother if it's not something people are interested in.
### What I've run:

N/A
### Comments:

When running `pip list`, the output looks like so:

```
$ pip list
Sphinx (1.2)
spyder (2.3.8)
SQLAlchemy (0.9.0)
statsmodels (0.5.0)
stdlib-list (0.2.1)

$ pip list -o
Sphinx (1.2) - Latest: 1.4.1 [wheel]
spyder (2.3.8) - Latest: 2.3.9 [sdist]
SQLAlchemy (0.9.0) - Latest: 1.0.12 [sdist]
statsmodels (0.5.0) - Latest: 0.6.1 [sdist]
```

I thought that it might be nice to have the option to arrange the items into columns:

```
$ pip list --pretty
Package           Current
Sphinx            1.2
spyder            2.3.8
SQLAlchemy        0.9.0
statsmodels       0.5.0
stdlib-list       0.2.1

$ pip list -o --pretty
Package           Current      Latest     Type
Sphinx            1.2          1.4.1      [wheel]
spyder            2.3.8        2.3.9      [sdist]
SQLAlchemy        0.9.0        1.0.12     [sdist]
statsmodels       0.5.0        0.6.1      [sdist]
stdlib-list       0.2.1        0.3.3      [wheel]
```
### Implementation

In order to implement this, I would modify `list.py`, specifically the `output_package` and `run_outdated` methods of `ListCommand`. The modifications would be to add format specifiers to the return values (tentative):
- output_package: `'%-40s %-15s %7s'`
- run_outdated: `'%-35s %-15s %s'`

I would of course add the ``--pretty` option to `ListCommand.__init()__` and update documentation to reflect the change. To maintain backwards compatibility, the --pretty option would default to false, so there will be no change in behavior for default commands.
### Potential issues
- Given that I'll continue to use %-style formatting, and that the option will default to off, I don't foresee any major issues.
- There may be some truncation of package names, versions, or paths.
  - It's hard to guess how long a package name or version will be, so I plan on maxing out the terminal width (79 chars) and giving more space for the version (as I think that's more important than the name).

> My original plan was to use `.format()`, but that would have resulted in the following issues:
> - Adding this will be backwards-incompatible with python version prior to 2.6, as `.format` was not introduced until then.
> - The `.format` function is slightly slower than %-style formatting. It also does not have the nifty logging lazy-eval that %-formatting does.



Discussion:
Some comments:
1. The output without `--pretty` should remain as it is, because scripts probably rely on it (your first example with headings used the command `pip list`, I suspect you meant `pip list --pretty`).
2. I'm not massively in favour of the headings, as it makes it harder to process the output with a script (and the new column format _is_ easier than the current format to process with a script). But they are nice for a "human readable" layout.
3. There's no need for `[...]` round the type in the column format.
4. I'm 100% against truncating anything. Ideally, you should have your format adapt to the longest entry, but if you don't want to go to that much effort you should at least let too-long entries fully display at the cost of misaligning the columns on that line. Also, there's no need to take the terminal width into account - just make the lines as long as they need to be and they'll wrap if the terminal is too narrow - the same "never lose information" principle applies here as with column widths.
5. If you _don't_ do adaptive column widths, you'll be likely to have an ugly display as you will either leave too much space for potentially long values, or be prone to long values breaking the column layout.

As for the option name, maybe add `--columns` and `-no-headers` options. The first selects the columnar layout, and the second suppresses column headers (for script use).

BTW, a simple adaptive tabulation function isn't that hard:

``` python
def tabulate(vals):
    assert len(vals) > 0

    sizes = [0] * len(vals[0])
    for row in vals:
        sizes = [max(s, len(str(c))) for s, c in zip(sizes, row)]

    result = []
    for row in vals:
        display = " ".join([str(c).ljust(s) for s, c in zip(sizes, row)])
        result.append(display)

    return result
```


Resolving patch:
diff --git a/pip/commands/list.py b/pip/commands/list.py
--- a/pip/commands/list.py
+++ b/pip/commands/list.py
@@ -2,6 +2,10 @@
 
 import logging
 import warnings
+try:
+    from itertools import zip_longest
+except ImportError:
+    from itertools import izip_longest as zip_longest
 
 from pip.basecommand import Command
 from pip.exceptions import CommandError
@@ -68,6 +72,23 @@ def __init__(self, *args, **kw):
                   "pip only finds stable versions."),
         )
 
+        # TODO: When we switch the default, set default=True here.
+        cmd_opts.add_option(
+            '--columns',
+            action='store_true',
+            dest='columns',
+            # default=True,
+            help="Align package names and versions into vertical columns.",
+        )
+
+        cmd_opts.add_option(
+            '--no-columns',
+            action='store_false',
+            dest='columns',
+            help=("Do not align package names and versions into "
+                  "vertical columns (old-style formatting)"),
+        )
+
         index_opts = make_option_group(index_group, self.parser)
 
         self.parser.insert_option_group(0, index_opts)
@@ -110,6 +131,15 @@ def run(self, options, args):
                 "no longer has any effect.",
                 RemovedInPip10Warning,
             )
+
+        # TODO: When we switch the default, remove
+        #       ``and options.columns is not None``
+        if not options.columns and options.columns is not None:
+            warnings.warn(
+                "The --no-columns option will be removed in the future.",
+                RemovedInPip10Warning,
+            )
+
         if options.outdated and options.uptodate:
             raise CommandError(
                 "Options --outdated and --uptodate cannot be combined.")
@@ -122,10 +152,19 @@ def run(self, options, args):
             self.run_listing(options)
 
     def run_outdated(self, options):
+        latest_pkgs = []
         for dist, latest_version, typ in sorted(
                 self.find_packages_latest_versions(options),
                 key=lambda p: p[0].project_name.lower()):
             if latest_version > dist.parsed_version:
+                latest_pkgs.append((dist, latest_version, typ))
+        if (hasattr(options, "columns") and
+                options.columns and
+                len(latest_pkgs) > 0):
+            data, header = format_for_columns(latest_pkgs, options)
+            self.output_package_listing_columns(data, header)
+        else:
+            for dist, latest_version, typ in latest_pkgs:
                 logger.info(
                     '%s - Latest: %s [%s]',
                     self.output_package(dist), latest_version, typ,
@@ -181,7 +220,7 @@ def run_listing(self, options):
             user_only=options.user,
             editables_only=options.editable,
         )
-        self.output_package_listing(installed_packages)
+        self.output_package_listing(installed_packages, options)
 
     def output_package(self, dist):
         if dist_is_editable(dist):
@@ -193,17 +232,94 @@ def output_package(self, dist):
         else:
             return '%s (%s)' % (dist.project_name, dist.version)
 
-    def output_package_listing(self, installed_packages):
+    def output_package_listing(self, installed_packages, options=None):
         installed_packages = sorted(
             installed_packages,
             key=lambda dist: dist.project_name.lower(),
         )
-        for dist in installed_packages:
-            logger.info(self.output_package(dist))
+        if (hasattr(options, "columns") and
+                options.columns and
+                len(installed_packages) > 0):
+            data, header = format_for_columns(installed_packages, options)
+            self.output_package_listing_columns(data, header)
+        else:
+            for dist in installed_packages:
+                logger.info(self.output_package(dist))
+
+    def output_package_listing_columns(self, data, header):
+        # insert the header first: we need to know the size of column names
+        if len(data) > 0:
+            data.insert(0, header)
+
+        pkg_strings, sizes = tabulate(data)
+
+        # Create and add a separator.
+        if len(data) > 0:
+            pkg_strings.insert(1, " ".join(map(lambda x: '-' * x, sizes)))
+
+        for val in pkg_strings:
+            logger.info(val)
 
     def run_uptodate(self, options):
         uptodate = []
         for dist, version, typ in self.find_packages_latest_versions(options):
             if dist.parsed_version == version:
                 uptodate.append(dist)
-        self.output_package_listing(uptodate)
+        self.output_package_listing(uptodate, options)
+
+
+def tabulate(vals):
+    # From pfmoore on GitHub:
+    # https://github.com/pypa/pip/issues/3651#issuecomment-216932564
+    assert len(vals) > 0
+
+    sizes = [0] * max(len(x) for x in vals)
+    for row in vals:
+        sizes = [max(s, len(str(c))) for s, c in zip_longest(sizes, row)]
+
+    result = []
+    for row in vals:
+        display = " ".join([str(c).ljust(s) if c is not None else ''
+                            for s, c in zip_longest(sizes, row)])
+        result.append(display)
+
+    return result, sizes
+
+
+def format_for_columns(pkgs, options):
+    """
+    Convert the package data into something usable
+    by output_package_listing_columns.
+    """
+    header = ["Package", "Version"]
+    running_outdated = False
+    # Adjust the header for the `pip list --outdated` case.
+    if isinstance(pkgs[0], (list, tuple)):
+        running_outdated = True
+        header = ["Package", "Version", "Latest", "Type"]
+
+    data = []
+    if any(dist_is_editable(x[0])
+           if running_outdated
+           else dist_is_editable(x)
+           for x in pkgs):
+        header.append("Location")
+
+    for proj in pkgs:
+        # if we're working on the 'outdated' list, separate out the
+        # latest_version and type
+        if running_outdated:
+            proj, latest_version, typ = proj
+
+        row = [proj.project_name, proj.version]
+
+        if running_outdated:
+            row.append(latest_version)
+            row.append(typ)
+
+        if dist_is_editable(proj):
+            row.append(proj.location)
+
+        data.append(row)
+
+    return data, header


Test patch:
diff --git a/tests/functional/test_list.py b/tests/functional/test_list.py
--- a/tests/functional/test_list.py
+++ b/tests/functional/test_list.py
@@ -1,6 +1,9 @@
 import os
 import pytest
 
+WARN_NOCOL = ("DEPRECATION: The --no-columns option will be "
+              "removed in the future.")
+
 
 def test_list_command(script, data):
     """
@@ -16,6 +19,75 @@ def test_list_command(script, data):
     assert 'simple2 (3.0)' in result.stdout, str(result)
 
 
+def test_columns_flag(script, data):
+    """
+    Test the list command with the '--columns' option
+    """
+    script.pip(
+        'install', '-f', data.find_links, '--no-index', 'simple==1.0',
+        'simple2==3.0',
+    )
+    result = script.pip('list', '--columns')
+    assert 'Package' in result.stdout, str(result)
+    assert 'Version' in result.stdout, str(result)
+    assert 'simple (1.0)' not in result.stdout, str(result)
+    assert 'simple     1.0' in result.stdout, str(result)
+    assert 'simple2    3.0' in result.stdout, str(result)
+
+
+def test_nocolumns_flag(script, data):
+    """
+    Test that --no-columns raises the deprecation warning and still outputs
+    the old-style formatting.
+    """
+    script.pip(
+        'install', '-f', data.find_links, '--no-index', 'simple==1.0',
+        'simple2==3.0',
+    )
+    result = script.pip('list', '--no-columns', expect_stderr=True)
+    assert WARN_NOCOL in result.stderr, str(result)
+    assert 'simple (1.0)' in result.stdout, str(result)
+    assert 'simple2 (3.0)' in result.stdout, str(result)
+
+
+def test_columns_nocolumns(script, data):
+    """
+    Test that --no-columns has priority in --columns --no-columns.
+    """
+    script.pip(
+        'install', '-f', data.find_links, '--no-index', 'simple==1.0',
+        'simple2==3.0',
+    )
+    result = script.pip(
+        'list', '--columns', '--no-columns',
+        expect_error=True,
+    )
+    assert WARN_NOCOL in result.stderr, str(result)
+    assert 'simple (1.0)' in result.stdout, str(result)
+    assert 'simple2 (3.0)' in result.stdout, str(result)
+    assert 'simple     1.0' not in result.stdout, str(result)
+    assert 'simple2    3.0' not in result.stdout, str(result)
+
+
+def test_nocolumns_columns(script, data):
+    """
+    Test that --columns has priority in --no-columns --columns.
+    """
+    script.pip(
+        'install', '-f', data.find_links, '--no-index', 'simple==1.0',
+        'simple2==3.0',
+    )
+    result = script.pip(
+        'list', '--no-columns', '--columns',
+    )
+    assert 'Package' in result.stdout, str(result)
+    assert 'Version' in result.stdout, str(result)
+    assert 'simple (1.0)' not in result.stdout, str(result)
+    assert 'simple2 (3.0)' not in result.stdout, str(result)
+    assert 'simple     1.0' in result.stdout, str(result)
+    assert 'simple2    3.0' in result.stdout, str(result)
+
+
 def test_local_flag(script, data):
     """
     Test the behavior of --local flag in the list command
@@ -26,6 +98,30 @@ def test_local_flag(script, data):
     assert 'simple (1.0)' in result.stdout
 
 
+def test_local_columns_flag(script, data):
+    """
+    Test the behavior of --local --columns flags in the list command
+
+    """
+    script.pip('install', '-f', data.find_links, '--no-index', 'simple==1.0')
+    result = script.pip('list', '--local', '--columns')
+    assert 'Package' in result.stdout
+    assert 'Version' in result.stdout
+    assert 'simple (1.0)' not in result.stdout
+    assert 'simple     1.0' in result.stdout, str(result)
+
+
+def test_local_nocolumns_flag(script, data):
+    """
+    Test the behavior of --local --no-columns flags in the list
+    command.
+    """
+    script.pip('install', '-f', data.find_links, '--no-index', 'simple==1.0')
+    result = script.pip('list', '--local', '--no-columns', expect_stderr=True)
+    assert WARN_NOCOL in result.stderr, str(result)
+    assert 'simple (1.0)' in result.stdout
+
+
 def test_user_flag(script, data, virtualenv):
     """
     Test the behavior of --user flag in the list command
@@ -40,6 +136,37 @@ def test_user_flag(script, data, virtualenv):
     assert 'simple2 (2.0)' in result.stdout
 
 
+def test_user_columns_flag(script, data, virtualenv):
+    """
+    Test the behavior of --user --columns flags in the list command
+
+    """
+    virtualenv.system_site_packages = True
+    script.pip('install', '-f', data.find_links, '--no-index', 'simple==1.0')
+    script.pip('install', '-f', data.find_links, '--no-index',
+               '--user', 'simple2==2.0')
+    result = script.pip('list', '--user', '--columns')
+    assert 'Package' in result.stdout
+    assert 'Version' in result.stdout
+    assert 'simple2 (2.0)' not in result.stdout
+    assert 'simple2 2.0' in result.stdout, str(result)
+
+
+def test_user_nocolumns_flag(script, data, virtualenv):
+    """
+    Test the behavior of --user flag in the list command
+
+    """
+    virtualenv.system_site_packages = True
+    script.pip('install', '-f', data.find_links, '--no-index', 'simple==1.0')
+    script.pip('install', '-f', data.find_links, '--no-index',
+               '--user', 'simple2==2.0')
+    result = script.pip('list', '--user', '--no-columns', expect_stderr=True)
+    assert WARN_NOCOL in result.stderr, str(result)
+    assert 'simple (1.0)' not in result.stdout
+    assert 'simple2 (2.0)' in result.stdout, str(result)
+
+
 @pytest.mark.network
 def test_uptodate_flag(script, data):
     """
@@ -63,6 +190,56 @@ def test_uptodate_flag(script, data):
     assert 'simple2 (3.0)' in result.stdout, str(result)
 
 
+@pytest.mark.network
+def test_uptodate_columns_flag(script, data):
+    """
+    Test the behavior of --uptodate --columns flag in the list command
+
+    """
+    script.pip(
+        'install', '-f', data.find_links, '--no-index', 'simple==1.0',
+        'simple2==3.0',
+    )
+    script.pip(
+        'install', '-e',
+        'git+https://github.com/pypa/pip-test-package.git#egg=pip-test-package'
+    )
+    result = script.pip(
+        'list', '-f', data.find_links, '--no-index', '--uptodate',
+        '--columns',
+    )
+    assert 'Package' in result.stdout
+    assert 'Version' in result.stdout
+    assert 'Location' in result.stdout      # editables included
+    assert 'pip-test-package (0.1.1,' not in result.stdout
+    assert 'pip-test-package 0.1.1' in result.stdout, str(result)
+    assert 'simple2          3.0' in result.stdout, str(result)
+
+
+@pytest.mark.network
+def test_uptodate_nocolumns_flag(script, data):
+    """
+    Test the behavior of --uptodate --no-columns flag in the list command
+
+    """
+    script.pip(
+        'install', '-f', data.find_links, '--no-index', 'simple==1.0',
+        'simple2==3.0',
+    )
+    script.pip(
+        'install', '-e',
+        'git+https://github.com/pypa/pip-test-package.git#egg=pip-test-package'
+    )
+    result = script.pip(
+        'list', '-f', data.find_links, '--no-index', '--uptodate',
+        '--no-columns', expect_stderr=True,
+    )
+    assert WARN_NOCOL in result.stderr, str(result)
+    assert 'simple (1.0)' not in result.stdout  # 3.0 is latest
+    assert 'pip-test-package (0.1.1,' in result.stdout  # editables included
+    assert 'simple2 (3.0)' in result.stdout, str(result)
+
+
 @pytest.mark.network
 def test_outdated_flag(script, data):
     """
@@ -89,6 +266,67 @@ def test_outdated_flag(script, data):
     assert 'simple2' not in result.stdout, str(result)  # 3.0 is latest
 
 
+@pytest.mark.network
+def test_outdated_columns_flag(script, data):
+    """
+    Test the behavior of --outdated --columns flag in the list command
+
+    """
+    script.pip(
+        'install', '-f', data.find_links, '--no-index', 'simple==1.0',
+        'simple2==3.0', 'simplewheel==1.0',
+    )
+    script.pip(
+        'install', '-e',
+        'git+https://github.com/pypa/pip-test-package.git'
+        '@0.1#egg=pip-test-package'
+    )
+    result = script.pip(
+        'list', '-f', data.find_links, '--no-index', '--outdated',
+        '--columns',
+    )
+    assert 'Package' in result.stdout
+    assert 'Version' in result.stdout
+    assert 'Latest' in result.stdout
+    assert 'Type' in result.stdout
+    assert 'simple (1.0) - Latest: 3.0 [sdist]' not in result.stdout
+    assert 'simplewheel (1.0) - Latest: 2.0 [wheel]' not in result.stdout
+    assert 'simple           1.0     3.0    sdist' in result.stdout, (
+        str(result)
+    )
+    assert 'simplewheel      1.0     2.0    wheel' in result.stdout, (
+        str(result)
+    )
+    assert 'simple2' not in result.stdout, str(result)  # 3.0 is latest
+
+
+@pytest.mark.network
+def test_outdated_nocolumns_flag(script, data):
+    """
+    Test the behavior of --outdated --no-columns flag in the list command
+
+    """
+    script.pip(
+        'install', '-f', data.find_links, '--no-index', 'simple==1.0',
+        'simple2==3.0', 'simplewheel==1.0',
+    )
+    script.pip(
+        'install', '-e',
+        'git+https://github.com/pypa/pip-test-package.git'
+        '@0.1#egg=pip-test-package'
+    )
+    result = script.pip(
+        'list', '-f', data.find_links, '--no-index', '--outdated',
+        '--no-columns', expect_stderr=True,
+    )
+    assert WARN_NOCOL in result.stderr, str(result)
+    assert 'simple (1.0) - Latest: 3.0 [sdist]' in result.stdout
+    assert 'simplewheel (1.0) - Latest: 2.0 [wheel]' in result.stdout
+    assert 'pip-test-package (0.1, ' in result.stdout
+    assert ' Latest: 0.1.1 [sdist]' in result.stdout
+    assert 'simple2' not in result.stdout, str(result)  # 3.0 is latest
+
+
 @pytest.mark.network
 def test_editables_flag(script, data):
     """
@@ -106,6 +344,45 @@ def test_editables_flag(script, data):
     )
 
 
+@pytest.mark.network
+def test_editables_columns_flag(script, data):
+    """
+    Test the behavior of --editables flag in the list command
+    """
+    script.pip('install', '-f', data.find_links, '--no-index', 'simple==1.0')
+    result = script.pip(
+        'install', '-e',
+        'git+https://github.com/pypa/pip-test-package.git#egg=pip-test-package'
+    )
+    result = script.pip('list', '--editable', '--columns')
+    assert 'Package' in result.stdout
+    assert 'Version' in result.stdout
+    assert 'Location' in result.stdout
+    assert os.path.join('src', 'pip-test-package') in result.stdout, (
+        str(result)
+    )
+
+
+@pytest.mark.network
+def test_editables_nocolumns_flag(script, data):
+    """
+    Test the behavior of --editables flag in the list command
+    """
+    script.pip('install', '-f', data.find_links, '--no-index', 'simple==1.0')
+    script.pip(
+        'install', '-e',
+        'git+https://github.com/pypa/pip-test-package.git#egg=pip-test-package'
+    )
+    result = script.pip(
+        'list', '--editable', '--no-columns', expect_stderr=True,
+    )
+    assert WARN_NOCOL in result.stderr, str(result)
+    assert 'simple (1.0)' not in result.stdout, str(result)
+    assert os.path.join('src', 'pip-test-package') in result.stdout, (
+        str(result)
+    )
+
+
 @pytest.mark.network
 def test_uptodate_editables_flag(script, data):
     """
@@ -127,6 +404,51 @@ def test_uptodate_editables_flag(script, data):
     )
 
 
+@pytest.mark.network
+def test_uptodate_editables_columns_flag(script, data):
+    """
+    test the behavior of --editable --uptodate --columns flag in the
+    list command
+    """
+    script.pip('install', '-f', data.find_links, '--no-index', 'simple==1.0')
+    result = script.pip(
+        'install', '-e',
+        'git+https://github.com/pypa/pip-test-package.git#egg=pip-test-package'
+    )
+    result = script.pip(
+        'list', '-f', data.find_links, '--no-index',
+        '--editable', '--uptodate', '--columns',
+    )
+    assert 'Package' in result.stdout
+    assert 'Version' in result.stdout
+    assert 'Location' in result.stdout
+    assert os.path.join('src', 'pip-test-package') in result.stdout, (
+        str(result)
+    )
+
+
+@pytest.mark.network
+def test_uptodate_editables_nocolumns_flag(script, data):
+    """
+    test the behavior of --editable --uptodate --columns --no-columns flag
+    in the list command
+    """
+    script.pip('install', '-f', data.find_links, '--no-index', 'simple==1.0')
+    script.pip(
+        'install', '-e',
+        'git+https://github.com/pypa/pip-test-package.git#egg=pip-test-package'
+    )
+    result = script.pip(
+        'list', '-f', data.find_links, '--no-index', '--editable',
+        '--uptodate', '--no-columns', expect_stderr=True,
+    )
+    assert WARN_NOCOL in result.stderr, str(result)
+    assert 'simple (1.0)' not in result.stdout, str(result)
+    assert os.path.join('src', 'pip-test-package') in result.stdout, (
+        str(result)
+    )
+
+
 @pytest.mark.network
 def test_outdated_editables_flag(script, data):
     """
@@ -149,6 +471,52 @@ def test_outdated_editables_flag(script, data):
     )
 
 
+@pytest.mark.network
+def test_outdated_editables_columns_flag(script, data):
+    """
+    test the behavior of --editable --outdated flag in the list command
+    """
+    script.pip('install', '-f', data.find_links, '--no-index', 'simple==1.0')
+    result = script.pip(
+        'install', '-e',
+        'git+https://github.com/pypa/pip-test-package.git'
+        '@0.1#egg=pip-test-package'
+    )
+    result = script.pip(
+        'list', '-f', data.find_links, '--no-index',
+        '--editable', '--outdated', '--columns',
+    )
+    assert 'Package' in result.stdout
+    assert 'Version' in result.stdout
+    assert 'Location' in result.stdout
+    assert os.path.join('src', 'pip-test-package') in result.stdout, (
+        str(result)
+    )
+
+
+@pytest.mark.network
+def test_outdated_editables_nocolumns_flag(script, data):
+    """
+    test the behavior of --editable --outdated flag in the list command
+    """
+    script.pip('install', '-f', data.find_links, '--no-index', 'simple==1.0')
+    script.pip(
+        'install', '-e',
+        'git+https://github.com/pypa/pip-test-package.git'
+        '@0.1#egg=pip-test-package'
+    )
+    result = script.pip(
+        'list', '-f', data.find_links, '--no-index',
+        '--editable', '--outdated', '--no-columns',
+        expect_stderr=True,
+    )
+    assert WARN_NOCOL in result.stderr, str(result)
+    assert 'simple (1.0)' not in result.stdout, str(result)
+    assert os.path.join('src', 'pip-test-package') in result.stdout, (
+        str(result)
+    )
+
+
 def test_outdated_pre(script, data):
     script.pip('install', '-f', data.find_links, '--no-index', 'simple==1.0')
 
@@ -166,3 +534,49 @@ def test_outdated_pre(script, data):
                             '--find-links', wheelhouse_path,
                             '--outdated', '--pre')
     assert 'simple (1.0) - Latest: 2.0.dev0 [wheel]' in result_pre.stdout
+
+
+def test_outdated_pre_columns(script, data):
+    """ Test of interaction behavior of --pre and --columns """
+    script.pip('install', '-f', data.find_links, '--no-index', 'simple==1.0')
+
+    # Let's build a fake wheelhouse
+    script.scratch_path.join("wheelhouse").mkdir()
+    wheelhouse_path = script.scratch_path / 'wheelhouse'
+    wheelhouse_path.join('simple-1.1-py2.py3-none-any.whl').write('')
+    wheelhouse_path.join('simple-2.0.dev0-py2.py3-none-any.whl').write('')
+    result = script.pip('list', '--no-index', '--find-links', wheelhouse_path)
+    assert 'simple (1.0)' in result.stdout
+    result = script.pip('list', '--no-index', '--find-links', wheelhouse_path,
+                        '--outdated')
+    assert 'simple (1.0) - Latest: 1.1 [wheel]' in result.stdout
+    result_pre = script.pip(
+        'list', '--no-index', '--find-links', wheelhouse_path,
+        '--outdated', '--pre', '--columns',
+    )
+    assert 'Package' in result_pre.stdout
+    assert 'Version' in result_pre.stdout
+    assert 'Latest' in result_pre.stdout
+    assert 'Type' in result_pre.stdout
+
+
+def test_outdated_pre_nocolumns(script, data):
+    """ Test of interaction behavior of --pre and --no-columns """
+    script.pip('install', '-f', data.find_links, '--no-index', 'simple==1.0')
+
+    # Let's build a fake wheelhouse
+    script.scratch_path.join("wheelhouse").mkdir()
+    wheelhouse_path = script.scratch_path / 'wheelhouse'
+    wheelhouse_path.join('simple-1.1-py2.py3-none-any.whl').write('')
+    wheelhouse_path.join('simple-2.0.dev0-py2.py3-none-any.whl').write('')
+    result = script.pip('list', '--no-index', '--find-links', wheelhouse_path)
+    assert 'simple (1.0)' in result.stdout
+    result = script.pip('list', '--no-index', '--find-links', wheelhouse_path,
+                        '--outdated')
+    assert 'simple (1.0) - Latest: 1.1 [wheel]' in result.stdout
+    result = script.pip(
+        'list', '--no-index',
+        '--find-links', wheelhouse_path,
+        '--outdated', '--pre', '--no-columns', expect_stderr=True
+    )
+    assert WARN_NOCOL in result.stderr, str(result)