diff --git a/news/6340.feature b/news/6340.feature new file mode 100644 index 00000000000..9afba8bf24a --- /dev/null +++ b/news/6340.feature @@ -0,0 +1 @@ +Add a new option ``--save-wheel-names `` to ``pip wheel`` that writes the names of the resulting wheels to the given filename. diff --git a/src/pip/_internal/commands/wheel.py b/src/pip/_internal/commands/wheel.py index 7230470b7d7..63a3f19a9c9 100644 --- a/src/pip/_internal/commands/wheel.py +++ b/src/pip/_internal/commands/wheel.py @@ -102,6 +102,16 @@ def __init__(self, *args, **kw): cmd_opts.add_option(cmdoptions.no_clean()) cmd_opts.add_option(cmdoptions.require_hashes()) + cmd_opts.add_option( + '--save-wheel-names', + dest='path_to_wheelnames', + action='store', + metavar='path', + help=("Store the filenames of the built or downloaded wheels " + "in a new file of given path. Filenames are separated " + "by new line and file ends with new line"), + ) + index_opts = cmdoptions.make_option_group( cmdoptions.index_group, self.parser, @@ -110,6 +120,28 @@ def __init__(self, *args, **kw): self.parser.insert_option_group(0, index_opts) self.parser.insert_option_group(0, cmd_opts) + def save_wheelnames( + self, + links_filenames, + path_to_wheelnames, + wheel_filenames, + ): + if path_to_wheelnames is None: + return + + entries_to_save = wheel_filenames + links_filenames + entries_to_save = [ + filename + '\n' for filename in entries_to_save + if filename.endswith('whl') + ] + try: + with open(path_to_wheelnames, 'w') as f: + f.writelines(entries_to_save) + except EnvironmentError as e: + logger.error('Cannot write to the given path: %s\n%s' % + (path_to_wheelnames, e)) + raise + def run(self, options, args): # type: (Values, List[Any]) -> None cmdoptions.check_install_build_global(options) @@ -163,10 +195,18 @@ def run(self, options, args): build_options=options.build_options or [], global_options=options.global_options or [], no_clean=options.no_clean, + path_to_wheelnames=options.path_to_wheelnames ) build_failures = wb.build( requirement_set.requirements.values(), ) + self.save_wheelnames( + [req.link.filename for req in + requirement_set.successfully_downloaded + if req.link is not None], + wb.path_to_wheelnames, + wb.wheel_filenames, + ) if len(build_failures) != 0: raise CommandError( "Failed to build one or more wheels" diff --git a/src/pip/_internal/wheel.py b/src/pip/_internal/wheel.py index d524701bdac..af4a4aa7e89 100644 --- a/src/pip/_internal/wheel.py +++ b/src/pip/_internal/wheel.py @@ -58,7 +58,7 @@ if MYPY_CHECK_RUNNING: from typing import ( Dict, List, Optional, Sequence, Mapping, Tuple, IO, Text, Any, - Iterable, Callable, Set, + Iterable, Callable, Set, Union, ) from pip._vendor.packaging.requirements import Requirement from pip._internal.req.req_install import InstallRequirement @@ -908,7 +908,8 @@ def __init__( build_options=None, # type: Optional[List[str]] global_options=None, # type: Optional[List[str]] check_binary_allowed=None, # type: Optional[BinaryAllowedPredicate] - no_clean=False # type: bool + no_clean=False, # type: bool + path_to_wheelnames=None, # type: Optional[Union[bytes, Text]] ): # type: (...) -> None if check_binary_allowed is None: @@ -924,6 +925,10 @@ def __init__( self.global_options = global_options or [] self.check_binary_allowed = check_binary_allowed self.no_clean = no_clean + # path where to save built names of built wheels + self.path_to_wheelnames = path_to_wheelnames + # file names of built wheel names + self.wheel_filenames = [] # type: List[Union[bytes, Text]] def _build_one(self, req, output_dir, python_tag=None): """Build one wheel. @@ -1134,6 +1139,9 @@ def build( ) if wheel_file: build_success.append(req) + self.wheel_filenames.append( + os.path.relpath(wheel_file, output_dir) + ) if should_unpack: # XXX: This is mildly duplicative with prepare_files, # but not close enough to pull out to a single common diff --git a/tests/functional/test_wheel.py b/tests/functional/test_wheel.py index 5ebc9ea4c21..92f2c9ef479 100644 --- a/tests/functional/test_wheel.py +++ b/tests/functional/test_wheel.py @@ -1,6 +1,7 @@ """'pip wheel' tests""" import os import re +import stat from os.path import exists import pytest @@ -255,3 +256,65 @@ def test_legacy_wheels_are_not_confused_with_other_files(script, tmpdir, data): wheel_file_name = 'simplewheel-1.0-py%s-none-any.whl' % pyversion[0] wheel_file_path = script.scratch / wheel_file_name assert wheel_file_path in result.files_created, result.stdout + + +def test_pip_option_save_wheel_name(script, data): + """Check if the option saves the filenames of built wheels + """ + script.pip( + 'wheel', '--no-index', '-f', data.find_links, + 'require_simple==1.0', + '--save-wheel-name', 'wheelnames', + ) + + wheel_file_names = [ + 'require_simple-1.0-py%s-none-any.whl' % pyversion[0], + 'simple-3.0-py%s-none-any.whl' % pyversion[0], + ] + wheelnames_path = script.scratch_path / 'wheelnames' + with open(wheelnames_path, 'r') as wheelnames_file: + wheelnames_entries = (wheelnames_file.read()).splitlines() + assert wheel_file_names == wheelnames_entries + + +def test_pip_option_save_wheel_name_Permission_error(script, data): + + temp_file = script.base_path / 'scratch' / 'wheelnames' + + wheel_file_names = [ + 'require_simple-1.0-py%s-none-any.whl' % pyversion[0], + 'simple-3.0-py%s-none-any.whl' % pyversion[0], + ] + + script.pip( + 'wheel', '--no-index', '-f', data.find_links, + 'require_simple==1.0', + '--save-wheel-name', 'wheelnames', + ) + os.chmod(temp_file, stat.S_IREAD) + result = script.pip( + 'wheel', '--no-index', '-f', data.find_links, + 'require_simple==1.0', + '--save-wheel-name', 'wheelnames', expect_error=True, + ) + os.chmod(temp_file, stat.S_IREAD | stat.S_IWRITE) + + assert "ERROR: Cannot write to the given path: wheelnames\n" \ + "[Errno 13] Permission denied: 'wheelnames'\n" in result.stderr + + with open(temp_file) as f: + result = f.read().splitlines() + # check that file stays same + assert result == wheel_file_names + + +def test_pip_option_save_wheel_name_error_during_build(script, data): + script.pip( + 'wheel', '--no-index', '--save-wheel-name', 'wheelnames', + '-f', data.find_links, 'wheelbroken==0.1', + expect_error=True, + ) + wheelnames_path = script.base_path / 'scratch' / 'wheelnames' + with open(wheelnames_path) as f: + wheelnames = f.read().splitlines() + assert wheelnames == [] diff --git a/tests/unit/test_wheel.py b/tests/unit/test_wheel.py index 2a824c7fd7b..254986e7f44 100644 --- a/tests/unit/test_wheel.py +++ b/tests/unit/test_wheel.py @@ -9,6 +9,7 @@ from pip._vendor.packaging.requirements import Requirement from pip._internal import pep425tags, wheel +from pip._internal.commands.wheel import WheelCommand from pip._internal.exceptions import InvalidWheelFilename, UnsupportedWheel from pip._internal.models.link import Link from pip._internal.req.req_install import InstallRequirement @@ -848,3 +849,30 @@ def test_rehash(self, tmpdir): h, length = wheel.rehash(self.test_file) assert length == str(self.test_file_len) assert h == self.test_file_hash_encoded + + +class TestWheelCommand(object): + + def test_save_wheelnames(self, tmpdir): + wheel_filenames = ['Flask-1.1.dev0-py2.py3-none-any.whl'] + links_filenames = [ + 'flask', + 'Werkzeug-0.15.4-py2.py3-none-any.whl', + 'Jinja2-2.10.1-py2.py3-none-any.whl', + 'itsdangerous-1.1.0-py2.py3-none-any.whl', + 'Click-7.0-py2.py3-none-any.whl' + ] + + expected = wheel_filenames + links_filenames[1:] + expected = [filename + '\n' for filename in expected] + temp_file = tmpdir.joinpath('wheelfiles') + + WheelCommand('name', 'summary').save_wheelnames( + links_filenames, + temp_file, + wheel_filenames + ) + + with open(temp_file, 'r') as f: + test_content = f.readlines() + assert test_content == expected