Skip to content

Commit a012fbe

Browse files
committed
Pull in hashes from constraint files
1 parent 700eb77 commit a012fbe

File tree

7 files changed

+97
-17
lines changed

7 files changed

+97
-17
lines changed

news/8792.bugfix

+2
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
New resolver: Pick up hash declarations in constraints files and use them to
2+
filter available distributions.

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

+36-1
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,8 @@
1+
from pip._vendor.packaging.specifiers import SpecifierSet
12
from pip._vendor.packaging.utils import canonicalize_name
23

4+
from pip._internal.req.req_install import InstallRequirement
5+
from pip._internal.utils.hashes import Hashes
36
from pip._internal.utils.typing import MYPY_CHECK_RUNNING
47

58
if MYPY_CHECK_RUNNING:
@@ -8,7 +11,6 @@
811
from pip._vendor.packaging.version import _BaseVersion
912

1013
from pip._internal.models.link import Link
11-
from pip._internal.req.req_install import InstallRequirement
1214

1315
CandidateLookup = Tuple[
1416
Optional["Candidate"],
@@ -24,6 +26,39 @@ def format_name(project, extras):
2426
return "{}[{}]".format(project, ",".join(canonical_extras))
2527

2628

29+
class Constraint(object):
30+
def __init__(self, specifier, hashes):
31+
# type: (SpecifierSet, Hashes) -> None
32+
self.specifier = specifier
33+
self.hashes = hashes
34+
35+
@classmethod
36+
def empty(cls):
37+
# type: () -> Constraint
38+
return Constraint(SpecifierSet(), Hashes())
39+
40+
@classmethod
41+
def from_ireq(cls, ireq):
42+
# type: (InstallRequirement) -> Constraint
43+
return Constraint(ireq.specifier, ireq.hashes(trust_internet=False))
44+
45+
def __nonzero__(self):
46+
# type: () -> bool
47+
return bool(self.specifier) or bool(self.hashes)
48+
49+
def __bool__(self):
50+
# type: () -> bool
51+
return self.__nonzero__()
52+
53+
def __and__(self, other):
54+
# type: (InstallRequirement) -> Constraint
55+
if not isinstance(other, InstallRequirement):
56+
return NotImplemented
57+
specifier = self.specifier & other.specifier
58+
hashes = self.hashes & other.hashes(trust_internet=False)
59+
return Constraint(specifier, hashes)
60+
61+
2762
class Requirement(object):
2863
@property
2964
def name(self):

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

+8-3
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@
2222
from pip._internal.utils.typing import MYPY_CHECK_RUNNING
2323
from pip._internal.utils.virtualenv import running_under_virtualenv
2424

25+
from .base import Constraint
2526
from .candidates import (
2627
AlreadyInstalledCandidate,
2728
EditableCandidate,
@@ -152,6 +153,7 @@ def _iter_found_candidates(
152153
self,
153154
ireqs, # type: Sequence[InstallRequirement]
154155
specifier, # type: SpecifierSet
156+
hashes, # type: Hashes
155157
):
156158
# type: (...) -> Iterable[Candidate]
157159
if not ireqs:
@@ -164,7 +166,6 @@ def _iter_found_candidates(
164166
template = ireqs[0]
165167
name = canonicalize_name(template.req.name)
166168

167-
hashes = Hashes()
168169
extras = frozenset() # type: FrozenSet[str]
169170
for ireq in ireqs:
170171
specifier &= ireq.req.specifier
@@ -218,7 +219,7 @@ def _iter_found_candidates(
218219
return six.itervalues(candidates)
219220

220221
def find_candidates(self, requirements, constraint):
221-
# type: (Sequence[Requirement], SpecifierSet) -> Iterable[Candidate]
222+
# type: (Sequence[Requirement], Constraint) -> Iterable[Candidate]
222223
explicit_candidates = set() # type: Set[Candidate]
223224
ireqs = [] # type: List[InstallRequirement]
224225
for req in requirements:
@@ -231,7 +232,11 @@ def find_candidates(self, requirements, constraint):
231232
# If none of the requirements want an explicit candidate, we can ask
232233
# the finder for candidates.
233234
if not explicit_candidates:
234-
return self._iter_found_candidates(ireqs, constraint)
235+
return self._iter_found_candidates(
236+
ireqs,
237+
constraint.specifier,
238+
constraint.hashes,
239+
)
235240

236241
if constraint:
237242
name = explicit_candidates.pop().name

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

+4-3
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,9 @@
1-
from pip._vendor.packaging.specifiers import SpecifierSet
21
from pip._vendor.resolvelib.providers import AbstractProvider
32

43
from pip._internal.utils.typing import MYPY_CHECK_RUNNING
54

5+
from .base import Constraint
6+
67
if MYPY_CHECK_RUNNING:
78
from typing import (
89
Any,
@@ -41,7 +42,7 @@ class PipProvider(AbstractProvider):
4142
def __init__(
4243
self,
4344
factory, # type: Factory
44-
constraints, # type: Dict[str, SpecifierSet]
45+
constraints, # type: Dict[str, Constraint]
4546
ignore_dependencies, # type: bool
4647
upgrade_strategy, # type: str
4748
user_requested, # type: Set[str]
@@ -134,7 +135,7 @@ def find_matches(self, requirements):
134135
if not requirements:
135136
return []
136137
constraint = self._constraints.get(
137-
requirements[0].name, SpecifierSet(),
138+
requirements[0].name, Constraint.empty(),
138139
)
139140
candidates = self._factory.find_candidates(requirements, constraint)
140141
return reversed(self._sort_matches(candidates))

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

+6-6
Original file line numberDiff line numberDiff line change
@@ -14,12 +14,12 @@
1414
from pip._internal.utils.misc import dist_is_editable
1515
from pip._internal.utils.typing import MYPY_CHECK_RUNNING
1616

17+
from .base import Constraint
1718
from .factory import Factory
1819

1920
if MYPY_CHECK_RUNNING:
2021
from typing import Dict, List, Optional, Set, Tuple
2122

22-
from pip._vendor.packaging.specifiers import SpecifierSet
2323
from pip._vendor.resolvelib.resolvers import Result
2424
from pip._vendor.resolvelib.structs import Graph
2525

@@ -71,25 +71,25 @@ def __init__(
7171
def resolve(self, root_reqs, check_supported_wheels):
7272
# type: (List[InstallRequirement], bool) -> RequirementSet
7373

74-
constraints = {} # type: Dict[str, SpecifierSet]
74+
constraints = {} # type: Dict[str, Constraint]
7575
user_requested = set() # type: Set[str]
7676
requirements = []
7777
for req in root_reqs:
78+
name = canonicalize_name(req.name)
7879
if req.constraint:
7980
# Ensure we only accept valid constraints
8081
problem = check_invalid_constraint_type(req)
8182
if problem:
8283
raise InstallationError(problem)
8384
if not req.match_markers():
8485
continue
85-
name = canonicalize_name(req.name)
8686
if name in constraints:
87-
constraints[name] = constraints[name] & req.specifier
87+
constraints[name] &= req
8888
else:
89-
constraints[name] = req.specifier
89+
constraints[name] = Constraint.from_ireq(req)
9090
else:
9191
if req.user_supplied and req.name:
92-
user_requested.add(canonicalize_name(req.name))
92+
user_requested.add(name)
9393
r = self.factory.make_requirement_from_install_req(
9494
req, requested_extras=(),
9595
)

tests/functional/test_new_resolver_hashes.py

+38
Original file line numberDiff line numberDiff line change
@@ -85,3 +85,41 @@ def test_new_resolver_hash_intersect(script, requirements_template, message):
8585
)
8686

8787
assert message in result.stdout, str(result)
88+
89+
90+
def test_new_resolver_hash_intersect_from_constraint(script):
91+
find_links = _create_find_links(script)
92+
93+
constraints_txt = script.scratch_path / "constraints.txt"
94+
constraints_txt.write_text(
95+
"base==0.1.0 --hash=sha256:{sdist_hash}".format(
96+
sdist_hash=find_links.sdist_hash,
97+
),
98+
)
99+
requirements_txt = script.scratch_path / "requirements.txt"
100+
requirements_txt.write_text(
101+
"""
102+
base==0.1.0 --hash=sha256:{sdist_hash} --hash=sha256:{wheel_hash}
103+
""".format(
104+
sdist_hash=find_links.sdist_hash,
105+
wheel_hash=find_links.wheel_hash,
106+
),
107+
)
108+
109+
result = script.pip(
110+
"install",
111+
"--use-feature=2020-resolver",
112+
"--no-cache-dir",
113+
"--no-deps",
114+
"--no-index",
115+
"--find-links", find_links.index_html,
116+
"--verbose",
117+
"--constraint", constraints_txt,
118+
"--requirement", requirements_txt,
119+
)
120+
121+
message = (
122+
"Checked 2 links for project 'base' against 1 hashes "
123+
"(1 matches, 0 no digest): discarding 1 non-matches"
124+
)
125+
assert message in result.stdout, str(result)

tests/unit/resolution_resolvelib/test_requirement.py

+3-4
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,7 @@
11
import pytest
2-
from pip._vendor.packaging.specifiers import SpecifierSet
32
from pip._vendor.resolvelib import BaseReporter, Resolver
43

5-
from pip._internal.resolution.resolvelib.base import Candidate
4+
from pip._internal.resolution.resolvelib.base import Candidate, Constraint
65
from pip._internal.utils.urls import path_to_url
76

87
# NOTE: All tests are prefixed `test_rlr` (for "test resolvelib resolver").
@@ -59,7 +58,7 @@ def test_new_resolver_correct_number_of_matches(test_cases, factory):
5958
"""Requirements should return the correct number of candidates"""
6059
for spec, _, match_count in test_cases:
6160
req = factory.make_requirement_from_spec(spec, comes_from=None)
62-
matches = factory.find_candidates([req], SpecifierSet())
61+
matches = factory.find_candidates([req], Constraint.empty())
6362
assert len(list(matches)) == match_count
6463

6564

@@ -68,7 +67,7 @@ def test_new_resolver_candidates_match_requirement(test_cases, factory):
6867
"""
6968
for spec, _, _ in test_cases:
7069
req = factory.make_requirement_from_spec(spec, comes_from=None)
71-
for c in factory.find_candidates([req], SpecifierSet()):
70+
for c in factory.find_candidates([req], Constraint.empty()):
7271
assert isinstance(c, Candidate)
7372
assert req.is_satisfied_by(c)
7473

0 commit comments

Comments
 (0)