Skip to content

Commit 0870fca

Browse files
committed
Merge pull request pypa#8839 from uranusjr/new-resolver-hash-intersect
1 parent d985650 commit 0870fca

File tree

9 files changed

+288
-22
lines changed

9 files changed

+288
-22
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.

news/8839.bugfix

+3
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/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

+9-4
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,
@@ -154,6 +155,7 @@ def _iter_found_candidates(
154155
self,
155156
ireqs, # type: Sequence[InstallRequirement]
156157
specifier, # type: SpecifierSet
158+
hashes, # type: Hashes
157159
):
158160
# type: (...) -> Iterable[Candidate]
159161
if not ireqs:
@@ -166,11 +168,10 @@ def _iter_found_candidates(
166168
template = ireqs[0]
167169
name = canonicalize_name(template.req.name)
168170

169-
hashes = Hashes()
170171
extras = frozenset() # type: FrozenSet[str]
171172
for ireq in ireqs:
172173
specifier &= ireq.req.specifier
173-
hashes |= ireq.hashes(trust_internet=False)
174+
hashes &= ireq.hashes(trust_internet=False)
174175
extras |= frozenset(ireq.extras)
175176

176177
# We use this to ensure that we only yield a single candidate for
@@ -220,7 +221,7 @@ def _iter_found_candidates(
220221
return six.itervalues(candidates)
221222

222223
def find_candidates(self, requirements, constraint):
223-
# type: (Sequence[Requirement], SpecifierSet) -> Iterable[Candidate]
224+
# type: (Sequence[Requirement], Constraint) -> Iterable[Candidate]
224225
explicit_candidates = set() # type: Set[Candidate]
225226
ireqs = [] # type: List[InstallRequirement]
226227
for req in requirements:
@@ -233,7 +234,11 @@ def find_candidates(self, requirements, constraint):
233234
# If none of the requirements want an explicit candidate, we can ask
234235
# the finder for candidates.
235236
if not explicit_candidates:
236-
return self._iter_found_candidates(ireqs, constraint)
237+
return self._iter_found_candidates(
238+
ireqs,
239+
constraint.specifier,
240+
constraint.hashes,
241+
)
237242

238243
if constraint:
239244
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]
@@ -141,7 +142,7 @@ def find_matches(self, requirements):
141142
if not requirements:
142143
return []
143144
constraint = self._constraints.get(
144-
requirements[0].name, SpecifierSet(),
145+
requirements[0].name, Constraint.empty(),
145146
)
146147
candidates = self._factory.find_candidates(requirements, constraint)
147148
return reversed(self._sort_matches(candidates))

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

+4-4
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

@@ -81,7 +81,7 @@ def __init__(
8181
def resolve(self, root_reqs, check_supported_wheels):
8282
# type: (List[InstallRequirement], bool) -> RequirementSet
8383

84-
constraints = {} # type: Dict[str, SpecifierSet]
84+
constraints = {} # type: Dict[str, Constraint]
8585
user_requested = set() # type: Set[str]
8686
requirements = []
8787
for req in root_reqs:
@@ -94,9 +94,9 @@ def resolve(self, root_reqs, check_supported_wheels):
9494
continue
9595
name = canonicalize_name(req.name)
9696
if name in constraints:
97-
constraints[name] = constraints[name] & req.specifier
97+
constraints[name] &= req
9898
else:
99-
constraints[name] = req.specifier
99+
constraints[name] = Constraint.from_ireq(req)
100100
else:
101101
if req.user_supplied and req.name:
102102
user_requested.add(canonicalize_name(req.name))

src/pip/_internal/utils/hashes.py

+14-6
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

0 commit comments

Comments
 (0)