diff --git a/news/8654.bugfix b/news/8654.bugfix new file mode 100644 index 00000000000..ec0df7a903e --- /dev/null +++ b/news/8654.bugfix @@ -0,0 +1,2 @@ +Trace a better error message on installation failure due to invalid ``.data`` +files in wheels. diff --git a/src/pip/_internal/operations/install/wheel.py b/src/pip/_internal/operations/install/wheel.py index 8f73a88b074..681fc0aa8ef 100644 --- a/src/pip/_internal/operations/install/wheel.py +++ b/src/pip/_internal/operations/install/wheel.py @@ -583,8 +583,28 @@ def data_scheme_file_maker(zip_file, scheme): def make_data_scheme_file(record_path): # type: (RecordPath) -> File normed_path = os.path.normpath(record_path) - _, scheme_key, dest_subpath = normed_path.split(os.path.sep, 2) - scheme_path = scheme_paths[scheme_key] + try: + _, scheme_key, dest_subpath = normed_path.split(os.path.sep, 2) + except ValueError: + message = ( + "Unexpected file in {}: {!r}. .data directory contents" + " should be named like: '/'." + ).format(wheel_path, record_path) + raise InstallationError(message) + + try: + scheme_path = scheme_paths[scheme_key] + except KeyError: + valid_scheme_keys = ", ".join(sorted(scheme_paths)) + message = ( + "Unknown scheme key used in {}: {} (for file {!r}). .data" + " directory contents should be in subdirectories named" + " with a valid scheme key ({})" + ).format( + wheel_path, scheme_key, record_path, valid_scheme_keys + ) + raise InstallationError(message) + dest_path = os.path.join(scheme_path, dest_subpath) assert_no_path_traversal(scheme_path, dest_path) return ZipBackedFile(record_path, dest_path, zip_file) diff --git a/tests/functional/test_install_wheel.py b/tests/functional/test_install_wheel.py index c53f13ca415..ad4e749676f 100644 --- a/tests/functional/test_install_wheel.py +++ b/tests/functional/test_install_wheel.py @@ -681,3 +681,36 @@ def test_correct_package_name_while_creating_wheel_bug(script, package_name): package = create_basic_wheel_for_package(script, package_name, '1.0') wheel_name = os.path.basename(package) assert wheel_name == 'simple_package-1.0-py2.py3-none-any.whl' + + +@pytest.mark.parametrize("name", ["purelib", "abc"]) +def test_wheel_with_file_in_data_dir_has_reasonable_error( + script, tmpdir, name +): + """Normally we expect entities in the .data directory to be in a + subdirectory, but if they are not then we should show a reasonable error + message that includes the path. + """ + wheel_path = make_wheel( + "simple", "0.1.0", extra_data_files={name: "hello world"} + ).save_to_dir(tmpdir) + + result = script.pip( + "install", "--no-index", str(wheel_path), expect_error=True + ) + assert "simple-0.1.0.data/{}".format(name) in result.stderr + + +def test_wheel_with_unknown_subdir_in_data_dir_has_reasonable_error( + script, tmpdir +): + wheel_path = make_wheel( + "simple", + "0.1.0", + extra_data_files={"unknown/hello.txt": "hello world"} + ).save_to_dir(tmpdir) + + result = script.pip( + "install", "--no-index", str(wheel_path), expect_error=True + ) + assert "simple-0.1.0.data/unknown/hello.txt" in result.stderr