Skip to content

Commit f7787f8

Browse files
authored
Merge pull request #11949 from sbidoul/hash2hashes-sbi
Generate download_info.info.hashes in install report for direct URL archives
2 parents 5d3f24d + 030d2d4 commit f7787f8

File tree

8 files changed

+93
-15
lines changed

8 files changed

+93
-15
lines changed

news/11948.bugfix.rst

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
When installing an archive from a direct URL or local file, populate
2+
``download_info.info.hashes`` in the installation report, in addition to the legacy
3+
``download_info.info.hash`` key.

src/pip/_internal/models/direct_url.py

Lines changed: 20 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -105,22 +105,31 @@ def __init__(
105105
hash: Optional[str] = None,
106106
hashes: Optional[Dict[str, str]] = None,
107107
) -> None:
108-
if hash is not None:
108+
# set hashes before hash, since the hash setter will further populate hashes
109+
self.hashes = hashes
110+
self.hash = hash
111+
112+
@property
113+
def hash(self) -> Optional[str]:
114+
return self._hash
115+
116+
@hash.setter
117+
def hash(self, value: Optional[str]) -> None:
118+
if value is not None:
109119
# Auto-populate the hashes key to upgrade to the new format automatically.
110-
# We don't back-populate the legacy hash key.
120+
# We don't back-populate the legacy hash key from hashes.
111121
try:
112-
hash_name, hash_value = hash.split("=", 1)
122+
hash_name, hash_value = value.split("=", 1)
113123
except ValueError:
114124
raise DirectUrlValidationError(
115-
f"invalid archive_info.hash format: {hash!r}"
125+
f"invalid archive_info.hash format: {value!r}"
116126
)
117-
if hashes is None:
118-
hashes = {hash_name: hash_value}
119-
elif hash_name not in hash:
120-
hashes = hashes.copy()
121-
hashes[hash_name] = hash_value
122-
self.hash = hash
123-
self.hashes = hashes
127+
if self.hashes is None:
128+
self.hashes = {hash_name: hash_value}
129+
elif hash_name not in self.hashes:
130+
self.hashes = self.hashes.copy()
131+
self.hashes[hash_name] = hash_value
132+
self._hash = value
124133

125134
@classmethod
126135
def _from_dict(cls, d: Optional[Dict[str, Any]]) -> Optional["ArchiveInfo"]:

src/pip/_internal/models/installation_report.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ def __init__(self, install_requirements: Sequence[InstallRequirement]):
1414
def _install_req_to_dict(cls, ireq: InstallRequirement) -> Dict[str, Any]:
1515
assert ireq.download_info, f"No download_info for {ireq}"
1616
res = {
17-
# PEP 610 json for the download URL. download_info.archive_info.hash may
17+
# PEP 610 json for the download URL. download_info.archive_info.hashes may
1818
# be absent when the requirement was installed from the wheel cache
1919
# and the cache entry was populated by an older pip version that did not
2020
# record origin.json.

src/pip/_internal/operations/prepare.py

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -571,12 +571,15 @@ def _prepare_linked_requirement(
571571
# Make sure we have a hash in download_info. If we got it as part of the
572572
# URL, it will have been verified and we can rely on it. Otherwise we
573573
# compute it from the downloaded file.
574+
# FIXME: https://github.com/pypa/pip/issues/11943
574575
if (
575576
isinstance(req.download_info.info, ArchiveInfo)
576-
and not req.download_info.info.hash
577+
and not req.download_info.info.hashes
577578
and local_file
578579
):
579580
hash = hash_file(local_file.path)[0].hexdigest()
581+
# We populate info.hash for backward compatibility.
582+
# This will automatically populate info.hashes.
580583
req.download_info.info.hash = f"sha256={hash}"
581584

582585
# For use in later processing,

src/pip/_internal/resolution/legacy/resolver.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -436,7 +436,7 @@ def _populate_link(self, req: InstallRequirement) -> None:
436436
req.download_info = cache_entry.origin
437437
else:
438438
# Legacy cache entry that does not have origin.json.
439-
# download_info may miss the archive_info.hash field.
439+
# download_info may miss the archive_info.hashes field.
440440
req.download_info = direct_url_from_link(
441441
req.link, link_is_in_wheel_cache=cache_entry.persistent
442442
)

src/pip/_internal/resolution/resolvelib/candidates.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -283,7 +283,7 @@ def __init__(
283283
ireq.download_info = cache_entry.origin
284284
else:
285285
# Legacy cache entry that does not have origin.json.
286-
# download_info may miss the archive_info.hash field.
286+
# download_info may miss the archive_info.hashes field.
287287
ireq.download_info = direct_url_from_link(
288288
source_link, link_is_in_wheel_cache=cache_entry.persistent
289289
)

tests/functional/test_install_report.py

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -93,6 +93,39 @@ def test_install_report_index(script: PipTestEnvironment, tmp_path: Path) -> Non
9393
assert "requires_dist" in paste_report["metadata"]
9494

9595

96+
@pytest.mark.network
97+
def test_install_report_direct_archive(
98+
script: PipTestEnvironment, tmp_path: Path, shared_data: TestData
99+
) -> None:
100+
"""Test report for direct URL archive."""
101+
report_path = tmp_path / "report.json"
102+
script.pip(
103+
"install",
104+
str(shared_data.root / "packages" / "simplewheel-1.0-py2.py3-none-any.whl"),
105+
"--dry-run",
106+
"--no-index",
107+
"--report",
108+
str(report_path),
109+
)
110+
report = json.loads(report_path.read_text())
111+
assert "install" in report
112+
assert len(report["install"]) == 1
113+
simplewheel_report = _install_dict(report)["simplewheel"]
114+
assert simplewheel_report["metadata"]["name"] == "simplewheel"
115+
assert simplewheel_report["requested"] is True
116+
assert simplewheel_report["is_direct"] is True
117+
url = simplewheel_report["download_info"]["url"]
118+
assert url.startswith("file://")
119+
assert url.endswith("/packages/simplewheel-1.0-py2.py3-none-any.whl")
120+
assert (
121+
simplewheel_report["download_info"]["archive_info"]["hash"]
122+
== "sha256=e63aa139caee941ec7f33f057a5b987708c2128238357cf905429846a2008718"
123+
)
124+
assert simplewheel_report["download_info"]["archive_info"]["hashes"] == {
125+
"sha256": "e63aa139caee941ec7f33f057a5b987708c2128238357cf905429846a2008718"
126+
}
127+
128+
96129
@pytest.mark.network
97130
def test_install_report_vcs_and_wheel_cache(
98131
script: PipTestEnvironment, tmp_path: Path

tests/unit/test_direct_url.py

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -140,3 +140,33 @@ def _redact_archive(url: str) -> str:
140140
== "https://${PIP_TOKEN}@g.c/u/p.git"
141141
)
142142
assert _redact_git("ssh://[email protected]/u/p.git") == "ssh://[email protected]/u/p.git"
143+
144+
145+
def test_hash_to_hashes() -> None:
146+
direct_url = DirectUrl(url="https://e.c/archive.tar.gz", info=ArchiveInfo())
147+
assert isinstance(direct_url.info, ArchiveInfo)
148+
direct_url.info.hash = "sha256=abcdef"
149+
assert direct_url.info.hashes == {"sha256": "abcdef"}
150+
151+
152+
def test_hash_to_hashes_constructor() -> None:
153+
direct_url = DirectUrl(
154+
url="https://e.c/archive.tar.gz", info=ArchiveInfo(hash="sha256=abcdef")
155+
)
156+
assert isinstance(direct_url.info, ArchiveInfo)
157+
assert direct_url.info.hashes == {"sha256": "abcdef"}
158+
direct_url = DirectUrl(
159+
url="https://e.c/archive.tar.gz",
160+
info=ArchiveInfo(hash="sha256=abcdef", hashes={"sha512": "123456"}),
161+
)
162+
assert isinstance(direct_url.info, ArchiveInfo)
163+
assert direct_url.info.hashes == {"sha256": "abcdef", "sha512": "123456"}
164+
# In case of conflict between hash and hashes, hashes wins.
165+
direct_url = DirectUrl(
166+
url="https://e.c/archive.tar.gz",
167+
info=ArchiveInfo(
168+
hash="sha256=abcdef", hashes={"sha256": "012345", "sha512": "123456"}
169+
),
170+
)
171+
assert isinstance(direct_url.info, ArchiveInfo)
172+
assert direct_url.info.hashes == {"sha256": "012345", "sha512": "123456"}

0 commit comments

Comments
 (0)