Skip to content

Add json formatting option to pip show #7967

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
wants to merge 9 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Empty file.
84 changes: 77 additions & 7 deletions src/pip/_internal/commands/show.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import json
import logging
import os
from email.parser import FeedParser
Expand Down Expand Up @@ -33,21 +34,41 @@ def add_options(self):
action='store_true',
default=False,
help='Show the full list of installed files for each package.')

self.cmd_opts.add_option(
'--format',
action='store',
dest='list_format',
default="header",
choices=('header', 'json'),
help="Select the output format among: header (default) or json",
)
self.parser.insert_option_group(0, self.cmd_opts)

def run(self, options, args):
# type: (Values, List[str]) -> int
if not args:
logger.warning('ERROR: Please provide a package name or names.')
return ERROR

query = args

# Get list of package infos and print them
results = search_packages_info(query)
if not print_results(
results, list_files=options.files, verbose=options.verbose):
return ERROR
return SUCCESS
return_status = False

if options.list_format == 'header':

return_status = print_results_default(results,
list_files=options.files,
verbose=options.verbose)

elif options.list_format == 'json':

return_status = print_results_json(results,
list_files=options.files,
verbose=options.verbose)

return SUCCESS if return_status else ERROR


def search_packages_info(query):
Expand Down Expand Up @@ -140,10 +161,10 @@ def get_requiring_packages(package_name):
yield package


def print_results(distributions, list_files=False, verbose=False):
def print_results_default(distributions, list_files=False, verbose=False):
# type: (Iterator[Dict[str, str]], bool, bool) -> bool
"""
Print the information from installed distributions found.
Print the information from installed distributions found in default format.
"""
results_printed = False
for i, dist in enumerate(distributions):
Expand Down Expand Up @@ -179,3 +200,52 @@ def print_results(distributions, list_files=False, verbose=False):
if "files" not in dist:
write_output("Cannot locate installed-files.txt")
return results_printed


def print_results_json(distributions, list_files=False, verbose=False):
# type: (Iterator[Dict[str, Any]], bool, bool) -> bool
"""
Build a dictionary with information from installed distributions
found in JSON format.
"""

results_printed = False
pkg_infos_list = []

for dist in distributions:
results_printed = True
pkg_info = {}

pkg_info["name"] = dist.get('name', '')
pkg_info["version"] = dist.get('version', '')

pkg_info["summary"] = dist.get('summary', '')
pkg_info["home-page"] = dist.get('home-page', '')
pkg_info["author"] = dist.get('author', '')
pkg_info["author-email"] = dist.get('author-email', '')
pkg_info["license"] = dist.get('license', '')
pkg_info["location"] = dist.get('location', '')
pkg_info["requires"] = dist.get('requires', [])
pkg_info["required-by"] = dist.get('required_by', [])

if verbose:

pkg_info["metadata-version"] = dist.get('metadata-version', '')
pkg_info["installer"] = dist.get('installer', '')
pkg_info["classifiers"] = dist.get('classifiers', [])
pkg_info["entry-points"] = \
[entry.strip() for entry in dist.get('entry_points', [])]\
if 'entry_points' in dist else []

if list_files:
if "files" not in dist:
pkg_info["files"] = None
else:
pkg_info['files'] = [line.strip()
for line in dist.get('files', [])]

pkg_infos_list.append(pkg_info)

write_output(json.dumps(pkg_infos_list, ensure_ascii=False))

return results_printed
155 changes: 154 additions & 1 deletion tests/functional/test_show.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import json
import os
import re

Expand All @@ -21,6 +22,18 @@ def test_basic_show(script):
assert 'Requires: ' in lines


def test_basic_show_json(script):
"""
Test end to end test for show command.
"""
result = script.pip('show', 'pip', '--format=json')
data = json.loads(result.stdout)[0]
assert len(data) == 10
assert data['name'] == 'pip'
assert data['version'] == '{}'.format(__version__)
assert {'location', 'requires'} <= set(data)


def test_show_with_files_not_found(script, data):
"""
Test for show command with installed files listing enabled and
Expand All @@ -39,6 +52,21 @@ def test_show_with_files_not_found(script, data):
assert 'Cannot locate installed-files.txt' in lines


def test_show_with_files_not_found_json(script, data):
"""
Test for show command with installed files listing enabled and
installed-files.txt not found.
"""
editable = data.packages.joinpath('SetupPyUTF8')
script.pip('install', '-e', editable)
result = script.pip('show', '-f', 'SetupPyUTF8', '--format=json')
data = json.loads(result.stdout)[0]
assert data['name'] == 'SetupPyUTF8'
assert data['version'] == '0.0.0'
assert {'location', 'requires'} <= set(data)
assert not data['files']


def test_show_with_files_from_wheel(script, data):
"""
Test that a wheel's files can be listed
Expand All @@ -52,6 +80,18 @@ def test_show_with_files_from_wheel(script, data):
assert re.search(r"Files:\n( .+\n)+", result.stdout)


def test_show_with_files_from_wheel_json(script, data):
"""
Test that a wheel's files can be listed
"""
wheel_file = data.packages.joinpath('simple.dist-0.1-py2.py3-none-any.whl')
script.pip('install', '--no-index', wheel_file)
result = script.pip('show', '-f', 'simple.dist', '--format=json')
data = json.loads(result.stdout)[0]
assert data['name'] == 'simple.dist'
assert data['files']


@pytest.mark.network
def test_show_with_all_files(script):
"""
Expand All @@ -64,6 +104,17 @@ def test_show_with_all_files(script):
assert re.search(r"Files:\n( .+\n)+", result.stdout)


@pytest.mark.network
def test_show_with_all_files_json(script):
"""
Test listing all files in the show command.
"""
script.pip('install', 'initools==0.2')
result = script.pip('show', '--files', 'initools', '--format=json')
data = json.loads(result.stdout)[0]
assert data['files']


def test_missing_argument(script):
"""
Test show command with no arguments.
Expand Down Expand Up @@ -108,6 +159,21 @@ def test_report_mixed_not_found(script):
assert 'Name: pip' in lines


def test_report_mixed_not_found_json(script):
"""
Test passing a mixture of found and not-found names.
"""
# We test passing non-canonicalized names.
result = script.pip(
'show', 'Abcd3', 'A-B-C', 'pip', '--format=json',
allow_stderr_warning=True
)
assert 'WARNING: Package(s) not found: A-B-C, Abcd3' in result.stderr
print(result.stdout)
data = json.loads(result.stdout)[0]
assert data['name'] == 'pip'


def test_search_any_case():
"""
Search for a package in any case.
Expand Down Expand Up @@ -138,6 +204,17 @@ def test_show_verbose_with_classifiers(script):
assert "Intended Audience :: Developers" in result.stdout


def test_show_verbose_with_classifiers_json(script):
"""
Test that classifiers can be listed
"""
result = script.pip('show', 'pip', '--verbose', '--format=json')
data = json.loads(result.stdout)[0]
assert data['name'] == 'pip'
assert data['classifiers']
assert "Intended Audience :: Developers" in data["classifiers"]


def test_show_verbose_installer(script, data):
"""
Test that the installer is shown (this currently needs a wheel install)
Expand All @@ -150,6 +227,18 @@ def test_show_verbose_installer(script, data):
assert 'Installer: pip' in lines


def test_show_verbose_installer_json(script, data):
"""
Test that the installer is shown (this currently needs a wheel install)
"""
wheel_file = data.packages.joinpath('simple.dist-0.1-py2.py3-none-any.whl')
script.pip('install', '--no-index', wheel_file)
result = script.pip('show', '--verbose', 'simple.dist', '--format=json')
data = json.loads(result.stdout)[0]
assert data['name'] == 'simple.dist'
assert data['installer'] == 'pip'


def test_show_verbose(script):
"""
Test end to end test for verbose show command.
Expand All @@ -162,6 +251,17 @@ def test_show_verbose(script):
assert 'Classifiers:' in lines


def test_show_verbose_json(script):
"""
Test end to end test for verbose show command.
"""
result = script.pip('show', '--verbose', 'pip', '--format=json')
data = json.loads(result.stdout)[0]

assert {'metadata-version', 'installer',
'entry-points', 'classifiers'} <= set(data)


def test_all_fields(script):
"""
Test that all the fields are present
Expand All @@ -175,6 +275,18 @@ def test_all_fields(script):
assert actual == expected


def test_all_fields_json(script):
"""
Test that all the fields are present
"""
result = script.pip('show', 'pip', '--format=json')
data = json.loads(result.stdout)[0]
expected = {'name', 'version', 'summary', 'home-page', 'author',
'author-email', 'license', 'location', 'requires',
'required-by'}
assert set(data) == expected


def test_pip_show_is_short(script):
"""
Test that pip show stays short
Expand All @@ -184,6 +296,15 @@ def test_pip_show_is_short(script):
assert len(lines) <= 10


def test_pip_show_is_short_json(script):
"""
Test that pip show stays short
"""
result = script.pip('show', 'pip', '--format=json')
data = json.loads(result.stdout)[0]
assert len(data) <= 10


def test_pip_show_divider(script, data):
"""
Expect a divider between packages
Expand Down Expand Up @@ -222,6 +343,22 @@ def test_show_required_by_packages_basic(script, data):
assert 'Required-by: requires-simple' in lines


def test_show_required_by_packages_basic_json(script, data):
"""
Test that installed packages that depend on this package are shown
"""
editable_path = os.path.join(data.src, 'requires_simple')
script.pip(
'install', '--no-index', '-f', data.find_links, editable_path
)

result = script.pip('show', 'simple', '--format=json')
data = json.loads(result.stdout)[0]

assert data['name'] == 'simple'
assert data['required-by'] == ['requires-simple']


def test_show_required_by_packages_capitalized(script, data):
"""
Test that the installed packages which depend on a package are shown
Expand All @@ -239,6 +376,23 @@ def test_show_required_by_packages_capitalized(script, data):
assert 'Required-by: Requires-Capitalized' in lines


def test_show_required_by_packages_capitalized_json(script, data):
"""
Test that the installed packages which depend on a package are shown
where the package has a capital letter
"""
editable_path = os.path.join(data.src, 'requires_capitalized')
script.pip(
'install', '--no-index', '-f', data.find_links, editable_path
)

result = script.pip('show', 'simple', '--format=json')
data = json.loads(result.stdout)[0]

assert data['name'] == 'simple'
assert data['required-by'] == ['Requires-Capitalized']


def test_show_required_by_packages_requiring_capitalized(script, data):
"""
Test that the installed packages which depend on a package are shown
Expand Down Expand Up @@ -292,7 +446,6 @@ def test_show_include_work_dir_pkg(script):
expect_stderr=True, cwd=pkg_path)

script.environ.update({'PYTHONPATH': pkg_path})

# Show should include package simple when run from package directory,
# when package directory is in PYTHONPATH
result = script.pip('show', 'simple', cwd=pkg_path)
Expand Down