Skip to content

Commit 5bbc73d

Browse files
committed
Add --format option to pip list
with legacy, columns, freeze and json formats
1 parent 5685e01 commit 5bbc73d

File tree

2 files changed

+119
-84
lines changed

2 files changed

+119
-84
lines changed

pip/commands/list.py

Lines changed: 89 additions & 71 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,15 @@
11
from __future__ import absolute_import
22

3+
import json
34
import logging
45
import warnings
56
try:
67
from itertools import zip_longest
78
except ImportError:
89
from itertools import izip_longest as zip_longest
910

11+
from pip._vendor import six
12+
1013
from pip.basecommand import Command
1114
from pip.exceptions import CommandError
1215
from pip.index import PackageFinder
@@ -15,7 +18,6 @@
1518
from pip.utils.deprecation import RemovedInPip10Warning
1619
from pip.cmdoptions import make_option_group, index_group
1720

18-
1921
logger = logging.getLogger(__name__)
2022

2123

@@ -72,19 +74,31 @@ def __init__(self, *args, **kw):
7274
"pip only finds stable versions."),
7375
)
7476

77+
cmd_opts.add_option(
78+
'--format',
79+
action='store',
80+
dest='list_format',
81+
choices=('legacy', 'columns', 'freeze', 'json'),
82+
default='legacy',
83+
help="Select the output format among: legacy (default), columns, "
84+
"freeze or json",
85+
)
86+
7587
# TODO: When we switch the default, set default=True here.
7688
cmd_opts.add_option(
7789
'--columns',
78-
action='store_true',
79-
dest='columns',
90+
action='store_const',
91+
const='columns',
92+
dest='list_format',
8093
# default=True,
8194
help="Align package names and versions into vertical columns.",
8295
)
8396

8497
cmd_opts.add_option(
8598
'--no-columns',
86-
action='store_false',
87-
dest='columns',
99+
action='store_const',
100+
const='legacy',
101+
dest='list_format',
88102
help=("Do not align package names and versions into "
89103
"vertical columns (old-style formatting)"),
90104
)
@@ -132,43 +146,33 @@ def run(self, options, args):
132146
RemovedInPip10Warning,
133147
)
134148

135-
# TODO: When we switch the default, remove
136-
# ``and options.columns is not None``
137-
if not options.columns and options.columns is not None:
138-
warnings.warn(
139-
"The --no-columns option will be removed in the future.",
140-
RemovedInPip10Warning,
141-
)
142-
143149
if options.outdated and options.uptodate:
144150
raise CommandError(
145151
"Options --outdated and --uptodate cannot be combined.")
146152

147153
if options.outdated:
148-
self.run_outdated(options)
154+
packages = self.get_outdated(options)
149155
elif options.uptodate:
150-
self.run_uptodate(options)
156+
packages = self.get_uptodate(options)
151157
else:
152-
self.run_listing(options)
158+
packages = self.get_listing(options)
159+
self.output_package_listing(packages, options)
153160

154-
def run_outdated(self, options):
161+
def get_outdated(self, options):
155162
latest_pkgs = []
156-
for dist, latest_version, typ in sorted(
163+
for dist in sorted(
157164
self.find_packages_latest_versions(options),
158-
key=lambda p: p[0].project_name.lower()):
159-
if latest_version > dist.parsed_version:
160-
latest_pkgs.append((dist, latest_version, typ))
161-
if (hasattr(options, "columns") and
162-
options.columns and
163-
len(latest_pkgs) > 0):
164-
data, header = format_for_columns(latest_pkgs, options)
165-
self.output_package_listing_columns(data, header)
166-
else:
167-
for dist, latest_version, typ in latest_pkgs:
168-
logger.info(
169-
'%s - Latest: %s [%s]',
170-
self.output_package(dist), latest_version, typ,
171-
)
165+
key=lambda dist: dist.project_name.lower()):
166+
if dist.latest_version > dist.parsed_version:
167+
latest_pkgs.append(dist)
168+
return latest_pkgs
169+
170+
def get_uptodate(self, options):
171+
uptodate = []
172+
for dist in self.find_packages_latest_versions(options):
173+
if dist.parsed_version == dist.latest_version:
174+
uptodate.append(dist)
175+
return uptodate
172176

173177
def find_packages_latest_versions(self, options):
174178
index_urls = [options.index_url] + options.extra_index_urls
@@ -212,17 +216,21 @@ def find_packages_latest_versions(self, options):
212216
typ = 'wheel'
213217
else:
214218
typ = 'sdist'
215-
yield dist, remote_version, typ
219+
# This is dirty but makes the rest of the code much cleaner
220+
dist.latest_version = remote_version
221+
dist.latest_filetype = typ
222+
yield dist
216223

217-
def run_listing(self, options):
218-
installed_packages = get_installed_distributions(
219-
local_only=options.local,
220-
user_only=options.user,
221-
editables_only=options.editable,
222-
)
223-
self.output_package_listing(installed_packages, options)
224+
def get_listing(self, options):
225+
packages = []
226+
for dist in get_installed_distributions(
227+
local_only=options.local,
228+
user_only=options.user,
229+
editables_only=options.editable):
230+
packages.append(dist)
231+
return packages
224232

225-
def output_package(self, dist):
233+
def output_legacy(self, dist):
226234
if dist_is_editable(dist):
227235
return '%s (%s, %s)' % (
228236
dist.project_name,
@@ -232,19 +240,42 @@ def output_package(self, dist):
232240
else:
233241
return '%s (%s)' % (dist.project_name, dist.version)
234242

235-
def output_package_listing(self, installed_packages, options=None):
236-
installed_packages = sorted(
237-
installed_packages,
243+
def output_legacy_latest(self, dist):
244+
return '%s - Latest: %s [%s]' % (
245+
self.output_legacy(dist),
246+
dist.latest_version,
247+
dist.latest_filetype,
248+
)
249+
250+
def output_package_listing(self, packages, options):
251+
packages = sorted(
252+
packages,
238253
key=lambda dist: dist.project_name.lower(),
239254
)
240-
if (hasattr(options, "columns") and
241-
options.columns and
242-
len(installed_packages) > 0):
243-
data, header = format_for_columns(installed_packages, options)
255+
if options.list_format == 'columns' and packages:
256+
data, header = format_for_columns(packages, options)
244257
self.output_package_listing_columns(data, header)
245-
else:
246-
for dist in installed_packages:
247-
logger.info(self.output_package(dist))
258+
elif options.list_format == 'freeze':
259+
for dist in packages:
260+
logger.info("%s==%s", dist.project_name, dist.version)
261+
elif options.list_format == 'json':
262+
data = []
263+
for dist in packages:
264+
info = {
265+
'name': dist.project_name,
266+
'version': six.text_type(dist.version),
267+
}
268+
if options.outdated:
269+
info['latest_version'] = six.text_type(dist.latest_version)
270+
info['latest_filetype'] = dist.latest_filetype
271+
data.append(info)
272+
logger.info(json.dumps(data))
273+
else: # legacy
274+
for dist in packages:
275+
if options.outdated:
276+
logger.info(self.output_legacy_latest(dist))
277+
else:
278+
logger.info(self.output_legacy(dist))
248279

249280
def output_package_listing_columns(self, data, header):
250281
# insert the header first: we need to know the size of column names
@@ -260,13 +291,6 @@ def output_package_listing_columns(self, data, header):
260291
for val in pkg_strings:
261292
logger.info(val)
262293

263-
def run_uptodate(self, options):
264-
uptodate = []
265-
for dist, version, typ in self.find_packages_latest_versions(options):
266-
if dist.parsed_version == version:
267-
uptodate.append(dist)
268-
self.output_package_listing(uptodate, options)
269-
270294

271295
def tabulate(vals):
272296
# From pfmoore on GitHub:
@@ -291,31 +315,25 @@ def format_for_columns(pkgs, options):
291315
Convert the package data into something usable
292316
by output_package_listing_columns.
293317
"""
294-
header = ["Package", "Version"]
295-
running_outdated = False
318+
running_outdated = options.outdated
296319
# Adjust the header for the `pip list --outdated` case.
297-
if isinstance(pkgs[0], (list, tuple)):
298-
running_outdated = True
320+
if running_outdated:
299321
header = ["Package", "Version", "Latest", "Type"]
322+
else:
323+
header = ["Package", "Version"]
300324

301325
data = []
302-
if any(dist_is_editable(x[0])
303-
if running_outdated
304-
else dist_is_editable(x)
305-
for x in pkgs):
326+
if any(dist_is_editable(x) for x in pkgs):
306327
header.append("Location")
307328

308329
for proj in pkgs:
309330
# if we're working on the 'outdated' list, separate out the
310331
# latest_version and type
311-
if running_outdated:
312-
proj, latest_version, typ = proj
313-
314332
row = [proj.project_name, proj.version]
315333

316334
if running_outdated:
317-
row.append(latest_version)
318-
row.append(typ)
335+
row.append(proj.latest_version)
336+
row.append(proj.latest_filetype)
319337

320338
if dist_is_editable(proj):
321339
row.append(proj.location)

tests/functional/test_list.py

Lines changed: 30 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,7 @@
1+
import json
12
import os
23
import pytest
34

4-
WARN_NOCOL = ("DEPRECATION: The --no-columns option will be "
5-
"removed in the future.")
6-
75

86
def test_list_command(script, data):
97
"""
@@ -45,7 +43,6 @@ def test_nocolumns_flag(script, data):
4543
'simple2==3.0',
4644
)
4745
result = script.pip('list', '--no-columns', expect_stderr=True)
48-
assert WARN_NOCOL in result.stderr, str(result)
4946
assert 'simple (1.0)' in result.stdout, str(result)
5047
assert 'simple2 (3.0)' in result.stdout, str(result)
5148

@@ -62,7 +59,6 @@ def test_columns_nocolumns(script, data):
6259
'list', '--columns', '--no-columns',
6360
expect_error=True,
6461
)
65-
assert WARN_NOCOL in result.stderr, str(result)
6662
assert 'simple (1.0)' in result.stdout, str(result)
6763
assert 'simple2 (3.0)' in result.stdout, str(result)
6864
assert 'simple 1.0' not in result.stdout, str(result)
@@ -118,7 +114,6 @@ def test_local_nocolumns_flag(script, data):
118114
"""
119115
script.pip('install', '-f', data.find_links, '--no-index', 'simple==1.0')
120116
result = script.pip('list', '--local', '--no-columns', expect_stderr=True)
121-
assert WARN_NOCOL in result.stderr, str(result)
122117
assert 'simple (1.0)' in result.stdout
123118

124119

@@ -162,7 +157,6 @@ def test_user_nocolumns_flag(script, data, virtualenv):
162157
script.pip('install', '-f', data.find_links, '--no-index',
163158
'--user', 'simple2==2.0')
164159
result = script.pip('list', '--user', '--no-columns', expect_stderr=True)
165-
assert WARN_NOCOL in result.stderr, str(result)
166160
assert 'simple (1.0)' not in result.stdout
167161
assert 'simple2 (2.0)' in result.stdout, str(result)
168162

@@ -234,7 +228,6 @@ def test_uptodate_nocolumns_flag(script, data):
234228
'list', '-f', data.find_links, '--no-index', '--uptodate',
235229
'--no-columns', expect_stderr=True,
236230
)
237-
assert WARN_NOCOL in result.stderr, str(result)
238231
assert 'simple (1.0)' not in result.stdout # 3.0 is latest
239232
assert 'pip-test-package (0.1.1,' in result.stdout # editables included
240233
assert 'simple2 (3.0)' in result.stdout, str(result)
@@ -319,7 +312,6 @@ def test_outdated_nocolumns_flag(script, data):
319312
'list', '-f', data.find_links, '--no-index', '--outdated',
320313
'--no-columns', expect_stderr=True,
321314
)
322-
assert WARN_NOCOL in result.stderr, str(result)
323315
assert 'simple (1.0) - Latest: 3.0 [sdist]' in result.stdout
324316
assert 'simplewheel (1.0) - Latest: 2.0 [wheel]' in result.stdout
325317
assert 'pip-test-package (0.1, ' in result.stdout
@@ -376,7 +368,6 @@ def test_editables_nocolumns_flag(script, data):
376368
result = script.pip(
377369
'list', '--editable', '--no-columns', expect_stderr=True,
378370
)
379-
assert WARN_NOCOL in result.stderr, str(result)
380371
assert 'simple (1.0)' not in result.stdout, str(result)
381372
assert os.path.join('src', 'pip-test-package') in result.stdout, (
382373
str(result)
@@ -442,7 +433,6 @@ def test_uptodate_editables_nocolumns_flag(script, data):
442433
'list', '-f', data.find_links, '--no-index', '--editable',
443434
'--uptodate', '--no-columns', expect_stderr=True,
444435
)
445-
assert WARN_NOCOL in result.stderr, str(result)
446436
assert 'simple (1.0)' not in result.stdout, str(result)
447437
assert os.path.join('src', 'pip-test-package') in result.stdout, (
448438
str(result)
@@ -510,7 +500,6 @@ def test_outdated_editables_nocolumns_flag(script, data):
510500
'--editable', '--outdated', '--no-columns',
511501
expect_stderr=True,
512502
)
513-
assert WARN_NOCOL in result.stderr, str(result)
514503
assert 'simple (1.0)' not in result.stdout, str(result)
515504
assert os.path.join('src', 'pip-test-package') in result.stdout, (
516505
str(result)
@@ -579,4 +568,32 @@ def test_outdated_pre_nocolumns(script, data):
579568
'--find-links', wheelhouse_path,
580569
'--outdated', '--pre', '--no-columns', expect_stderr=True
581570
)
582-
assert WARN_NOCOL in result.stderr, str(result)
571+
572+
573+
def test_list_freeze(script, data):
574+
"""
575+
Test freeze formating of list command
576+
577+
"""
578+
script.pip(
579+
'install', '-f', data.find_links, '--no-index', 'simple==1.0',
580+
'simple2==3.0',
581+
)
582+
result = script.pip('list', '--format=freeze')
583+
assert 'simple==1.0' in result.stdout, str(result)
584+
assert 'simple2==3.0' in result.stdout, str(result)
585+
586+
587+
def test_list_json(script, data):
588+
"""
589+
Test json formating of list command
590+
591+
"""
592+
script.pip(
593+
'install', '-f', data.find_links, '--no-index', 'simple==1.0',
594+
'simple2==3.0',
595+
)
596+
result = script.pip('list', '--format=json')
597+
data = json.loads(result.stdout)
598+
assert {'name': 'simple', 'version': '1.0'} in data
599+
assert {'name': 'simple2', 'version': '3.0'} in data

0 commit comments

Comments
 (0)