diff --git a/src/pip/_internal/operations/prepare.py b/src/pip/_internal/operations/prepare.py index df1016ebbd1..053e0020c90 100644 --- a/src/pip/_internal/operations/prepare.py +++ b/src/pip/_internal/operations/prepare.py @@ -25,6 +25,7 @@ ) from pip._internal.index.package_finder import PackageFinder from pip._internal.metadata import BaseDistribution +from pip._internal.models.direct_url import ArchiveInfo from pip._internal.models.link import Link from pip._internal.models.wheel import Wheel from pip._internal.network.download import BatchDownloader, Downloader @@ -35,9 +36,18 @@ from pip._internal.network.session import PipSession from pip._internal.operations.build.build_tracker import BuildTracker from pip._internal.req.req_install import InstallRequirement +from pip._internal.utils.direct_url_helpers import ( + direct_url_for_editable, + direct_url_from_link, +) from pip._internal.utils.hashes import Hashes, MissingHashes from pip._internal.utils.logging import indent_log -from pip._internal.utils.misc import display_path, hide_url, is_installable_dir +from pip._internal.utils.misc import ( + display_path, + hash_file, + hide_url, + is_installable_dir, +) from pip._internal.utils.temp_dir import TempDirectory from pip._internal.utils.unpacking import unpack_file from pip._internal.vcs import vcs @@ -489,10 +499,26 @@ def _prepare_linked_requirement( hashes.check_against_path(file_path) local_file = File(file_path, content_type=None) + # If download_info is set, we got it from the wheel cache. + if req.download_info is None: + # Editables don't go through this function (see + # prepare_editable_requirement). + assert not req.editable + req.download_info = direct_url_from_link(link, req.source_dir) + # For use in later processing, # preserve the file path on the requirement. if local_file: req.local_file_path = local_file.path + # Make sure we have a hash in download_info. If we got it as part of the + # URL, it will have been verified and we can rely on it. Otherwise we + # compute it from the downloaded file. + if ( + isinstance(req.download_info.info, ArchiveInfo) + and not req.download_info.info.hash + ): + hash = hash_file(local_file.path)[0].hexdigest() + req.download_info.info.hash = f"sha256={hash}" dist = _get_prepared_distribution( req, @@ -547,6 +573,8 @@ def prepare_editable_requirement( ) req.ensure_has_source_dir(self.src_dir) req.update_editable() + assert req.source_dir + req.download_info = direct_url_for_editable(req.unpacked_source_directory) dist = _get_prepared_distribution( req, diff --git a/src/pip/_internal/req/req_install.py b/src/pip/_internal/req/req_install.py index b40d9e251f8..e01da2d69ef 100644 --- a/src/pip/_internal/req/req_install.py +++ b/src/pip/_internal/req/req_install.py @@ -26,6 +26,7 @@ get_default_environment, get_directory_distribution, ) +from pip._internal.models.direct_url import DirectUrl from pip._internal.models.link import Link from pip._internal.operations.build.metadata import generate_metadata from pip._internal.operations.build.metadata_editable import generate_editable_metadata @@ -112,6 +113,10 @@ def __init__( self.link = self.original_link = link self.original_link_is_in_wheel_cache = False + # Information about the location of the artifact that was downloaded . This + # property is guaranteed to be set in resolver results. + self.download_info: Optional[DirectUrl] = None + # Path to any downloaded or already-existing package. self.local_file_path: Optional[str] = None if self.link and self.link.is_file: @@ -762,6 +767,7 @@ def install( if self.is_wheel: assert self.local_file_path direct_url = None + # TODO this can be refactored to direct_url = self.download_info if self.editable: direct_url = direct_url_for_editable(self.unpacked_source_directory) elif self.original_link: diff --git a/tests/unit/test_req.py b/tests/unit/test_req.py index ac68fa4df57..3c4ded6f26d 100644 --- a/tests/unit/test_req.py +++ b/tests/unit/test_req.py @@ -22,6 +22,7 @@ ) from pip._internal.index.package_finder import PackageFinder from pip._internal.metadata import select_backend +from pip._internal.models.direct_url import ArchiveInfo, DirInfo, VcsInfo from pip._internal.network.session import PipSession from pip._internal.operations.build.build_tracker import get_build_tracker from pip._internal.operations.prepare import RequirementPreparer @@ -342,6 +343,115 @@ def test_hashed_deps_on_require_hashes(self) -> None: ) ) + def test_download_info_find_links(self, data: TestData) -> None: + """Test that download_info is set for requirements via find_links.""" + finder = make_test_finder(find_links=[data.find_links]) + with self._basic_resolver(finder) as resolver: + ireq = get_processed_req_from_line("simple") + reqset = resolver.resolve([ireq], True) + assert len(reqset.all_requirements) == 1 + req = reqset.all_requirements[0] + assert req.download_info + assert isinstance(req.download_info.info, ArchiveInfo) + assert req.download_info.info.hash + + @pytest.mark.network + def test_download_info_index_url(self) -> None: + """Test that download_info is set for requirements via index.""" + finder = make_test_finder(index_urls=["https://pypi.org/simple"]) + with self._basic_resolver(finder) as resolver: + ireq = get_processed_req_from_line("initools") + reqset = resolver.resolve([ireq], True) + assert len(reqset.all_requirements) == 1 + req = reqset.all_requirements[0] + assert req.download_info + assert isinstance(req.download_info.info, ArchiveInfo) + assert req.download_info.info.hash + + @pytest.mark.network + def test_download_info_web_archive(self) -> None: + """Test that download_info is set for requirements from a web archive.""" + finder = make_test_finder() + with self._basic_resolver(finder) as resolver: + ireq = get_processed_req_from_line( + "pip-test-package @ " + "https://github.com/pypa/pip-test-package/tarball/0.1.1" + ) + reqset = resolver.resolve([ireq], True) + assert len(reqset.all_requirements) == 1 + req = reqset.all_requirements[0] + assert req.download_info + assert ( + req.download_info.url + == "https://github.com/pypa/pip-test-package/tarball/0.1.1" + ) + assert isinstance(req.download_info.info, ArchiveInfo) + assert ( + req.download_info.info.hash == "sha256=" + "ad977496000576e1b6c41f6449a9897087ce9da6db4f15b603fe8372af4bf3c6" + ) + + def test_download_info_local_wheel(self, data: TestData) -> None: + """Test that download_info is set for requirements from a local wheel.""" + finder = make_test_finder() + with self._basic_resolver(finder) as resolver: + ireq = get_processed_req_from_line( + f"{data.packages}/simplewheel-1.0-py2.py3-none-any.whl" + ) + reqset = resolver.resolve([ireq], True) + assert len(reqset.all_requirements) == 1 + req = reqset.all_requirements[0] + assert req.download_info + assert req.download_info.url.startswith("file://") + assert isinstance(req.download_info.info, ArchiveInfo) + assert ( + req.download_info.info.hash == "sha256=" + "e63aa139caee941ec7f33f057a5b987708c2128238357cf905429846a2008718" + ) + + def test_download_info_local_dir(self, data: TestData) -> None: + """Test that download_info is set for requirements from a local dir.""" + finder = make_test_finder() + with self._basic_resolver(finder) as resolver: + ireq_url = path_to_url(data.packages / "FSPkg") + ireq = get_processed_req_from_line(f"FSPkg @ {ireq_url}") + reqset = resolver.resolve([ireq], True) + assert len(reqset.all_requirements) == 1 + req = reqset.all_requirements[0] + assert req.download_info + assert req.download_info.url.startswith("file://") + assert isinstance(req.download_info.info, DirInfo) + + def test_download_info_local_editable_dir(self, data: TestData) -> None: + """Test that download_info is set for requirements from a local editable dir.""" + finder = make_test_finder() + with self._basic_resolver(finder) as resolver: + ireq_url = path_to_url(data.packages / "FSPkg") + ireq = get_processed_req_from_line(f"-e {ireq_url}#egg=FSPkg") + reqset = resolver.resolve([ireq], True) + assert len(reqset.all_requirements) == 1 + req = reqset.all_requirements[0] + assert req.download_info + assert req.download_info.url.startswith("file://") + assert isinstance(req.download_info.info, DirInfo) + assert req.download_info.info.editable + + @pytest.mark.network + def test_download_info_vcs(self) -> None: + """Test that download_info is set for requirements from git.""" + finder = make_test_finder() + with self._basic_resolver(finder) as resolver: + ireq = get_processed_req_from_line( + "pip-test-package @ git+https://github.com/pypa/pip-test-package" + ) + reqset = resolver.resolve([ireq], True) + assert len(reqset.all_requirements) == 1 + req = reqset.all_requirements[0] + assert req.download_info + assert isinstance(req.download_info.info, VcsInfo) + assert req.download_info.url == "https://github.com/pypa/pip-test-package" + assert req.download_info.info.vcs == "git" + class TestInstallRequirement: def setup(self) -> None: