Skip to content

Commit 700eb77

Browse files
committed
Hashes from lines should intersect, not union
1 parent 567630b commit 700eb77

File tree

4 files changed

+105
-7
lines changed

4 files changed

+105
-7
lines changed

news/8839.bugfix

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
New resolver: If a package appears multiple times in user specification with
2+
different ``--hash`` options, only hashes that present in all specifications
3+
should be allowed.

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -168,7 +168,7 @@ def _iter_found_candidates(
168168
extras = frozenset() # type: FrozenSet[str]
169169
for ireq in ireqs:
170170
specifier &= ireq.req.specifier
171-
hashes |= ireq.hashes(trust_internet=False)
171+
hashes &= ireq.hashes(trust_internet=False)
172172
extras |= frozenset(ireq.extras)
173173

174174
# We use this to ensure that we only yield a single candidate for

src/pip/_internal/utils/hashes.py

Lines changed: 14 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -46,16 +46,24 @@ def __init__(self, hashes=None):
4646
"""
4747
self._allowed = {} if hashes is None else hashes
4848

49-
def __or__(self, other):
49+
def __and__(self, other):
5050
# type: (Hashes) -> Hashes
5151
if not isinstance(other, Hashes):
5252
return NotImplemented
53-
new = self._allowed.copy()
53+
54+
# If either of the Hashes object is entirely empty (i.e. no hash
55+
# specified at all), all hashes from the other object are allowed.
56+
if not other:
57+
return self
58+
if not self:
59+
return other
60+
61+
# Otherwise only hashes that present in both objects are allowed.
62+
new = {}
5463
for alg, values in iteritems(other._allowed):
55-
try:
56-
new[alg] += values
57-
except KeyError:
58-
new[alg] = values
64+
if alg not in self._allowed:
65+
continue
66+
new[alg] = [v for v in values if v in self._allowed[alg]]
5967
return Hashes(new)
6068

6169
@property
Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,87 @@
1+
import collections
2+
import hashlib
3+
4+
import pytest
5+
6+
from pip._internal.utils.urls import path_to_url
7+
from tests.lib import (
8+
create_basic_sdist_for_package,
9+
create_basic_wheel_for_package,
10+
)
11+
12+
_FindLinks = collections.namedtuple(
13+
"_FindLinks", "index_html sdist_hash wheel_hash",
14+
)
15+
16+
17+
def _create_find_links(script):
18+
sdist_path = create_basic_sdist_for_package(script, "base", "0.1.0")
19+
wheel_path = create_basic_wheel_for_package(script, "base", "0.1.0")
20+
21+
sdist_hash = hashlib.sha256(sdist_path.read_bytes()).hexdigest()
22+
wheel_hash = hashlib.sha256(wheel_path.read_bytes()).hexdigest()
23+
24+
index_html = script.scratch_path / "index.html"
25+
index_html.write_text(
26+
"""
27+
<a href="{sdist_url}#sha256={sdist_hash}">{sdist_path.stem}</a>
28+
<a href="{wheel_url}#sha256={wheel_hash}">{wheel_path.stem}</a>
29+
""".format(
30+
sdist_url=path_to_url(sdist_path),
31+
sdist_hash=sdist_hash,
32+
sdist_path=sdist_path,
33+
wheel_url=path_to_url(wheel_path),
34+
wheel_hash=wheel_hash,
35+
wheel_path=wheel_path,
36+
)
37+
)
38+
39+
return _FindLinks(index_html, sdist_hash, wheel_hash)
40+
41+
42+
@pytest.mark.parametrize(
43+
"requirements_template, message",
44+
[
45+
(
46+
"""
47+
base==0.1.0 --hash=sha256:{sdist_hash} --hash=sha256:{wheel_hash}
48+
base==0.1.0 --hash=sha256:{sdist_hash} --hash=sha256:{wheel_hash}
49+
""",
50+
"Checked 2 links for project 'base' against 2 hashes "
51+
"(2 matches, 0 no digest): discarding no candidates",
52+
),
53+
(
54+
# Different hash lists are intersected.
55+
"""
56+
base==0.1.0 --hash=sha256:{sdist_hash} --hash=sha256:{wheel_hash}
57+
base==0.1.0 --hash=sha256:{sdist_hash}
58+
""",
59+
"Checked 2 links for project 'base' against 1 hashes "
60+
"(1 matches, 0 no digest): discarding 1 non-matches",
61+
),
62+
],
63+
ids=["identical", "intersect"],
64+
)
65+
def test_new_resolver_hash_intersect(script, requirements_template, message):
66+
find_links = _create_find_links(script)
67+
68+
requirements_txt = script.scratch_path / "requirements.txt"
69+
requirements_txt.write_text(
70+
requirements_template.format(
71+
sdist_hash=find_links.sdist_hash,
72+
wheel_hash=find_links.wheel_hash,
73+
),
74+
)
75+
76+
result = script.pip(
77+
"install",
78+
"--use-feature=2020-resolver",
79+
"--no-cache-dir",
80+
"--no-deps",
81+
"--no-index",
82+
"--find-links", find_links.index_html,
83+
"--verbose",
84+
"--requirement", requirements_txt,
85+
)
86+
87+
assert message in result.stdout, str(result)

0 commit comments

Comments
 (0)