diff --git a/news/7487.removal b/news/7487.removal new file mode 100644 index 000000000..aa89bf42a --- /dev/null +++ b/news/7487.removal @@ -0,0 +1,2 @@ +Wheel processing no longer permits wheels containing more than one top-level +.dist-info directory. diff --git a/src/pip/_internal/operations/install/wheel.py b/src/pip/_internal/operations/install/wheel.py index 627cc4a32..4b10a9c00 100644 --- a/src/pip/_internal/operations/install/wheel.py +++ b/src/pip/_internal/operations/install/wheel.py @@ -315,8 +315,10 @@ def install_unpacked_wheel( :param pycompile: Whether to byte-compile installed Python files :param warn_script_location: Whether to check that scripts are installed into a directory on PATH - :raises UnsupportedWheel: when the directory holds an unpacked wheel with - incompatible Wheel-Version + :raises UnsupportedWheel: + * when the directory holds an unpacked wheel with incompatible + Wheel-Version + * when the .dist-info dir does not match the wheel """ # TODO: Investigate and break this up. # TODO: Look into moving this into a dedicated class for representing an @@ -381,8 +383,7 @@ def install_unpacked_wheel( continue elif ( is_base and - s.endswith('.dist-info') and - canonicalize_name(s).startswith(canonicalize_name(name)) + s.endswith('.dist-info') ): assert not info_dir, ( 'Multiple .dist-info directories: {}, '.format( @@ -445,6 +446,15 @@ def install_unpacked_wheel( req_description ) + info_dir_name = canonicalize_name(os.path.basename(info_dir[0])) + canonical_name = canonicalize_name(name) + if not info_dir_name.startswith(canonical_name): + raise UnsupportedWheel( + "{} .dist-info directory {!r} does not start with {!r}".format( + req_description, os.path.basename(info_dir[0]), canonical_name + ) + ) + # Get the defined entry points ep_file = os.path.join(info_dir[0], 'entry_points.txt') console, gui = get_entrypoints(ep_file) diff --git a/tests/functional/test_install_wheel.py b/tests/functional/test_install_wheel.py index e218d6e7f..4c1a92854 100644 --- a/tests/functional/test_install_wheel.py +++ b/tests/functional/test_install_wheel.py @@ -1,9 +1,11 @@ import distutils import glob import os +import shutil import pytest +from tests.lib import create_basic_wheel_for_package from tests.lib.path import Path @@ -432,3 +434,41 @@ def test_wheel_install_with_no_cache_dir(script, tmpdir, data): package = data.packages.joinpath("simple.dist-0.1-py2.py3-none-any.whl") result = script.pip('install', '--no-cache-dir', '--no-index', package) result.assert_installed('simpledist', editable=False) + + +def test_wheel_install_fails_with_extra_dist_info(script): + package = create_basic_wheel_for_package( + script, + "simple", + "0.1.0", + extra_files={ + "unrelated-2.0.0.dist-info/WHEEL": "Wheel-Version: 1.0", + "unrelated-2.0.0.dist-info/METADATA": ( + "Name: unrelated\nVersion: 2.0.0\n" + ), + }, + ) + result = script.pip( + "install", "--no-cache-dir", "--no-index", package, expect_error=True + ) + assert "Multiple .dist-info directories" in result.stderr + + +def test_wheel_install_fails_with_unrelated_dist_info(script): + package = create_basic_wheel_for_package(script, "simple", "0.1.0") + new_name = "unrelated-2.0.0-py2.py3-none-any.whl" + new_package = os.path.join(os.path.dirname(package), new_name) + shutil.move(package, new_package) + + result = script.pip( + "install", + "--no-cache-dir", + "--no-index", + new_package, + expect_error=True, + ) + + assert ( + "'simple-0.1.0.dist-info' does not start with 'unrelated'" + in result.stderr + ) diff --git a/tests/lib/__init__.py b/tests/lib/__init__.py index 07cc0ed5f..af80078df 100644 --- a/tests/lib/__init__.py +++ b/tests/lib/__init__.py @@ -921,8 +921,9 @@ def create_test_package_with_setup(script, **setup_kwargs): return pkg_path -def create_basic_wheel_for_package(script, name, version, - depends=None, extras=None): +def create_basic_wheel_for_package( + script, name, version, depends=None, extras=None, extra_files=None +): if depends is None: depends = [] if extras is None: @@ -966,6 +967,9 @@ def create_basic_wheel_for_package(script, name, version, "{dist_info}/RECORD": "" } + if extra_files: + files.update(extra_files) + # Some useful shorthands archive_name = "{name}-{version}-py2.py3-none-any.whl".format( name=name, version=version