diff --git a/news/11217.bugfix.rst b/news/11217.bugfix.rst new file mode 100644 index 00000000000..b111df669a4 --- /dev/null +++ b/news/11217.bugfix.rst @@ -0,0 +1,4 @@ +Do not consider a ``.dist-info`` directory found inside a wheel-like zip file +as metadata for an installed distribution. A package in a wheel is (by +definition) not installed, and is not guaranteed to work due to how a wheel is +structured. diff --git a/src/pip/_internal/metadata/importlib/_envs.py b/src/pip/_internal/metadata/importlib/_envs.py index 25dbdeaeb77..d5fcfdbfef2 100644 --- a/src/pip/_internal/metadata/importlib/_envs.py +++ b/src/pip/_internal/metadata/importlib/_envs.py @@ -10,12 +10,24 @@ from pip._vendor.packaging.utils import NormalizedName, canonicalize_name from pip._internal.metadata.base import BaseDistribution, BaseEnvironment +from pip._internal.models.wheel import Wheel from pip._internal.utils.deprecation import deprecated +from pip._internal.utils.filetypes import WHEEL_EXTENSION from ._compat import BasePath, get_dist_name, get_info_location from ._dists import Distribution +def _looks_like_wheel(location: str) -> bool: + if not location.endswith(WHEEL_EXTENSION): + return False + if not os.path.isfile(location): + return False + if not Wheel.wheel_file_re.match(os.path.basename(location)): + return False + return zipfile.is_zipfile(location) + + class _DistributionFinder: """Finder to locate distributions. @@ -36,6 +48,11 @@ def __init__(self) -> None: def _find_impl(self, location: str) -> Iterator[FoundResult]: """Find distributions in a location.""" + # Skip looking inside a wheel. Since a package inside a wheel is not + # always valid (due to .data directories etc.), its .dist-info entry + # should not be considered an installed distribution. + if _looks_like_wheel(location): + return # To know exactly where we find a distribution, we have to feed in the # paths one by one, instead of dumping the list to importlib.metadata. for dist in importlib.metadata.distributions(path=[location]): diff --git a/tests/unit/metadata/test_metadata.py b/tests/unit/metadata/test_metadata.py index 0e8f3ffc1cc..f77178fb9c1 100644 --- a/tests/unit/metadata/test_metadata.py +++ b/tests/unit/metadata/test_metadata.py @@ -1,4 +1,5 @@ import logging +import os from pathlib import Path from typing import cast from unittest import mock @@ -9,6 +10,7 @@ from pip._internal.metadata import ( BaseDistribution, get_directory_distribution, + get_environment, get_wheel_distribution, ) from pip._internal.metadata.base import FilesystemWheel @@ -102,3 +104,28 @@ def test_metadata_dict(tmp_path: Path) -> None: metadata_dict = dist.metadata_dict assert metadata_dict["name"] == "pkga" assert metadata_dict["version"] == "1.0.1" + + +def test_no_dist_found_in_wheel(tmp_path: Path) -> None: + location = os.fspath(tmp_path.joinpath("pkg-1-py3-none-any.whl")) + make_wheel(name="pkg", version="1").save_to(location) + assert get_environment([location]).get_distribution("pkg") is None + + +def test_dist_found_in_directory_named_whl(tmp_path: Path) -> None: + dir_path = tmp_path.joinpath("pkg-1-py3-none-any.whl") + info_path = dir_path.joinpath("pkg-1.dist-info") + info_path.mkdir(parents=True) + info_path.joinpath("METADATA").write_text("Name: pkg") + location = os.fspath(dir_path) + dist = get_environment([location]).get_distribution("pkg") + assert dist is not None and dist.location is not None + assert Path(dist.location) == Path(location) + + +def test_dist_found_in_zip(tmp_path: Path) -> None: + location = os.fspath(tmp_path.joinpath("pkg.zip")) + make_wheel(name="pkg", version="1").save_to(location) + dist = get_environment([location]).get_distribution("pkg") + assert dist is not None and dist.location is not None + assert Path(dist.location) == Path(location)