diff options
author | Stéphane Bidoul <stephane.bidoul@gmail.com> | 2023-04-14 08:02:57 +0200 |
---|---|---|
committer | GitHub <noreply@github.com> | 2023-04-14 08:02:57 +0200 |
commit | f7787f8798712e475ebbf71f5487f92158f043a9 (patch) | |
tree | 2d84dd527b2e9324aa8358a4356cffae6ce3bf11 | |
parent | 5d3f24dac1c461ec095d879aa4984ae09916be88 (diff) | |
parent | 030d2d425b0919dc3ca81820e110aabbddb2ef77 (diff) | |
download | pip-f7787f8798712e475ebbf71f5487f92158f043a9.tar.gz |
Merge pull request #11949 from sbidoul/hash2hashes-sbi
Generate download_info.info.hashes in install report for direct URL archives
-rw-r--r-- | news/11948.bugfix.rst | 3 | ||||
-rw-r--r-- | src/pip/_internal/models/direct_url.py | 31 | ||||
-rw-r--r-- | src/pip/_internal/models/installation_report.py | 2 | ||||
-rw-r--r-- | src/pip/_internal/operations/prepare.py | 5 | ||||
-rw-r--r-- | src/pip/_internal/resolution/legacy/resolver.py | 2 | ||||
-rw-r--r-- | src/pip/_internal/resolution/resolvelib/candidates.py | 2 | ||||
-rw-r--r-- | tests/functional/test_install_report.py | 33 | ||||
-rw-r--r-- | tests/unit/test_direct_url.py | 30 |
8 files changed, 93 insertions, 15 deletions
diff --git a/news/11948.bugfix.rst b/news/11948.bugfix.rst new file mode 100644 index 000000000..74af91381 --- /dev/null +++ b/news/11948.bugfix.rst @@ -0,0 +1,3 @@ +When installing an archive from a direct URL or local file, populate +``download_info.info.hashes`` in the installation report, in addition to the legacy +``download_info.info.hash`` key. diff --git a/src/pip/_internal/models/direct_url.py b/src/pip/_internal/models/direct_url.py index c3de70a74..e219d7384 100644 --- a/src/pip/_internal/models/direct_url.py +++ b/src/pip/_internal/models/direct_url.py @@ -105,22 +105,31 @@ class ArchiveInfo: hash: Optional[str] = None, hashes: Optional[Dict[str, str]] = None, ) -> None: - if hash is not None: + # set hashes before hash, since the hash setter will further populate hashes + self.hashes = hashes + self.hash = hash + + @property + def hash(self) -> Optional[str]: + return self._hash + + @hash.setter + def hash(self, value: Optional[str]) -> None: + if value is not None: # Auto-populate the hashes key to upgrade to the new format automatically. - # We don't back-populate the legacy hash key. + # We don't back-populate the legacy hash key from hashes. try: - hash_name, hash_value = hash.split("=", 1) + hash_name, hash_value = value.split("=", 1) except ValueError: raise DirectUrlValidationError( - f"invalid archive_info.hash format: {hash!r}" + f"invalid archive_info.hash format: {value!r}" ) - if hashes is None: - hashes = {hash_name: hash_value} - elif hash_name not in hash: - hashes = hashes.copy() - hashes[hash_name] = hash_value - self.hash = hash - self.hashes = hashes + if self.hashes is None: + self.hashes = {hash_name: hash_value} + elif hash_name not in self.hashes: + self.hashes = self.hashes.copy() + self.hashes[hash_name] = hash_value + self._hash = value @classmethod def _from_dict(cls, d: Optional[Dict[str, Any]]) -> Optional["ArchiveInfo"]: diff --git a/src/pip/_internal/models/installation_report.py b/src/pip/_internal/models/installation_report.py index b54afb109..fef3757f2 100644 --- a/src/pip/_internal/models/installation_report.py +++ b/src/pip/_internal/models/installation_report.py @@ -14,7 +14,7 @@ class InstallationReport: def _install_req_to_dict(cls, ireq: InstallRequirement) -> Dict[str, Any]: assert ireq.download_info, f"No download_info for {ireq}" res = { - # PEP 610 json for the download URL. download_info.archive_info.hash may + # PEP 610 json for the download URL. download_info.archive_info.hashes may # be absent when the requirement was installed from the wheel cache # and the cache entry was populated by an older pip version that did not # record origin.json. diff --git a/src/pip/_internal/operations/prepare.py b/src/pip/_internal/operations/prepare.py index 343a01bef..dda92d29b 100644 --- a/src/pip/_internal/operations/prepare.py +++ b/src/pip/_internal/operations/prepare.py @@ -571,12 +571,15 @@ class RequirementPreparer: # 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. + # FIXME: https://github.com/pypa/pip/issues/11943 if ( isinstance(req.download_info.info, ArchiveInfo) - and not req.download_info.info.hash + and not req.download_info.info.hashes and local_file ): hash = hash_file(local_file.path)[0].hexdigest() + # We populate info.hash for backward compatibility. + # This will automatically populate info.hashes. req.download_info.info.hash = f"sha256={hash}" # For use in later processing, diff --git a/src/pip/_internal/resolution/legacy/resolver.py b/src/pip/_internal/resolution/legacy/resolver.py index fb49d4169..3a561e6db 100644 --- a/src/pip/_internal/resolution/legacy/resolver.py +++ b/src/pip/_internal/resolution/legacy/resolver.py @@ -436,7 +436,7 @@ class Resolver(BaseResolver): req.download_info = cache_entry.origin else: # Legacy cache entry that does not have origin.json. - # download_info may miss the archive_info.hash field. + # download_info may miss the archive_info.hashes field. req.download_info = direct_url_from_link( req.link, link_is_in_wheel_cache=cache_entry.persistent ) diff --git a/src/pip/_internal/resolution/resolvelib/candidates.py b/src/pip/_internal/resolution/resolvelib/candidates.py index 39af0d5db..e5e9d1fd7 100644 --- a/src/pip/_internal/resolution/resolvelib/candidates.py +++ b/src/pip/_internal/resolution/resolvelib/candidates.py @@ -283,7 +283,7 @@ class LinkCandidate(_InstallRequirementBackedCandidate): ireq.download_info = cache_entry.origin else: # Legacy cache entry that does not have origin.json. - # download_info may miss the archive_info.hash field. + # download_info may miss the archive_info.hashes field. ireq.download_info = direct_url_from_link( source_link, link_is_in_wheel_cache=cache_entry.persistent ) diff --git a/tests/functional/test_install_report.py b/tests/functional/test_install_report.py index b8df6936f..003b29d38 100644 --- a/tests/functional/test_install_report.py +++ b/tests/functional/test_install_report.py @@ -94,6 +94,39 @@ def test_install_report_index(script: PipTestEnvironment, tmp_path: Path) -> Non @pytest.mark.network +def test_install_report_direct_archive( + script: PipTestEnvironment, tmp_path: Path, shared_data: TestData +) -> None: + """Test report for direct URL archive.""" + report_path = tmp_path / "report.json" + script.pip( + "install", + str(shared_data.root / "packages" / "simplewheel-1.0-py2.py3-none-any.whl"), + "--dry-run", + "--no-index", + "--report", + str(report_path), + ) + report = json.loads(report_path.read_text()) + assert "install" in report + assert len(report["install"]) == 1 + simplewheel_report = _install_dict(report)["simplewheel"] + assert simplewheel_report["metadata"]["name"] == "simplewheel" + assert simplewheel_report["requested"] is True + assert simplewheel_report["is_direct"] is True + url = simplewheel_report["download_info"]["url"] + assert url.startswith("file://") + assert url.endswith("/packages/simplewheel-1.0-py2.py3-none-any.whl") + assert ( + simplewheel_report["download_info"]["archive_info"]["hash"] + == "sha256=e63aa139caee941ec7f33f057a5b987708c2128238357cf905429846a2008718" + ) + assert simplewheel_report["download_info"]["archive_info"]["hashes"] == { + "sha256": "e63aa139caee941ec7f33f057a5b987708c2128238357cf905429846a2008718" + } + + +@pytest.mark.network def test_install_report_vcs_and_wheel_cache( script: PipTestEnvironment, tmp_path: Path ) -> None: diff --git a/tests/unit/test_direct_url.py b/tests/unit/test_direct_url.py index 3ca982b50..151e0a30f 100644 --- a/tests/unit/test_direct_url.py +++ b/tests/unit/test_direct_url.py @@ -140,3 +140,33 @@ def test_redact_url() -> None: == "https://${PIP_TOKEN}@g.c/u/p.git" ) assert _redact_git("ssh://git@g.c/u/p.git") == "ssh://git@g.c/u/p.git" + + +def test_hash_to_hashes() -> None: + direct_url = DirectUrl(url="https://e.c/archive.tar.gz", info=ArchiveInfo()) + assert isinstance(direct_url.info, ArchiveInfo) + direct_url.info.hash = "sha256=abcdef" + assert direct_url.info.hashes == {"sha256": "abcdef"} + + +def test_hash_to_hashes_constructor() -> None: + direct_url = DirectUrl( + url="https://e.c/archive.tar.gz", info=ArchiveInfo(hash="sha256=abcdef") + ) + assert isinstance(direct_url.info, ArchiveInfo) + assert direct_url.info.hashes == {"sha256": "abcdef"} + direct_url = DirectUrl( + url="https://e.c/archive.tar.gz", + info=ArchiveInfo(hash="sha256=abcdef", hashes={"sha512": "123456"}), + ) + assert isinstance(direct_url.info, ArchiveInfo) + assert direct_url.info.hashes == {"sha256": "abcdef", "sha512": "123456"} + # In case of conflict between hash and hashes, hashes wins. + direct_url = DirectUrl( + url="https://e.c/archive.tar.gz", + info=ArchiveInfo( + hash="sha256=abcdef", hashes={"sha256": "012345", "sha512": "123456"} + ), + ) + assert isinstance(direct_url.info, ArchiveInfo) + assert direct_url.info.hashes == {"sha256": "012345", "sha512": "123456"} |