Skip to content

Commit

Permalink
Merge pull request pypa#8839 from uranusjr/new-resolver-hash-intersect
Browse files Browse the repository at this point in the history
  • Loading branch information
pradyunsg committed Oct 12, 2020
1 parent c0ff001 commit 8945fe3
Show file tree
Hide file tree
Showing 9 changed files with 288 additions and 22 deletions.
2 changes: 2 additions & 0 deletions news/8792.bugfix
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
New resolver: Pick up hash declarations in constraints files and use them to
filter available distributions.
3 changes: 3 additions & 0 deletions news/8839.bugfix
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
New resolver: If a package appears multiple times in user specification with
different ``--hash`` options, only hashes that present in all specifications
should be allowed.
37 changes: 36 additions & 1 deletion src/pip/_internal/resolution/resolvelib/base.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
from pip._vendor.packaging.specifiers import SpecifierSet
from pip._vendor.packaging.utils import canonicalize_name

from pip._internal.req.req_install import InstallRequirement
from pip._internal.utils.hashes import Hashes
from pip._internal.utils.typing import MYPY_CHECK_RUNNING

if MYPY_CHECK_RUNNING:
Expand All @@ -8,7 +11,6 @@
from pip._vendor.packaging.version import _BaseVersion

from pip._internal.models.link import Link
from pip._internal.req.req_install import InstallRequirement

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


class Constraint(object):
def __init__(self, specifier, hashes):
# type: (SpecifierSet, Hashes) -> None
self.specifier = specifier
self.hashes = hashes

@classmethod
def empty(cls):
# type: () -> Constraint
return Constraint(SpecifierSet(), Hashes())

@classmethod
def from_ireq(cls, ireq):
# type: (InstallRequirement) -> Constraint
return Constraint(ireq.specifier, ireq.hashes(trust_internet=False))

def __nonzero__(self):
# type: () -> bool
return bool(self.specifier) or bool(self.hashes)

def __bool__(self):
# type: () -> bool
return self.__nonzero__()

def __and__(self, other):
# type: (InstallRequirement) -> Constraint
if not isinstance(other, InstallRequirement):
return NotImplemented
specifier = self.specifier & other.specifier
hashes = self.hashes & other.hashes(trust_internet=False)
return Constraint(specifier, hashes)


class Requirement(object):
@property
def name(self):
Expand Down
13 changes: 9 additions & 4 deletions src/pip/_internal/resolution/resolvelib/factory.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@
from pip._internal.utils.typing import MYPY_CHECK_RUNNING
from pip._internal.utils.virtualenv import running_under_virtualenv

from .base import Constraint
from .candidates import (
AlreadyInstalledCandidate,
EditableCandidate,
Expand Down Expand Up @@ -154,6 +155,7 @@ def _iter_found_candidates(
self,
ireqs, # type: Sequence[InstallRequirement]
specifier, # type: SpecifierSet
hashes, # type: Hashes
):
# type: (...) -> Iterable[Candidate]
if not ireqs:
Expand All @@ -166,11 +168,10 @@ def _iter_found_candidates(
template = ireqs[0]
name = canonicalize_name(template.req.name)

hashes = Hashes()
extras = frozenset() # type: FrozenSet[str]
for ireq in ireqs:
specifier &= ireq.req.specifier
hashes |= ireq.hashes(trust_internet=False)
hashes &= ireq.hashes(trust_internet=False)
extras |= frozenset(ireq.extras)

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

def find_candidates(self, requirements, constraint):
# type: (Sequence[Requirement], SpecifierSet) -> Iterable[Candidate]
# type: (Sequence[Requirement], Constraint) -> Iterable[Candidate]
explicit_candidates = set() # type: Set[Candidate]
ireqs = [] # type: List[InstallRequirement]
for req in requirements:
Expand All @@ -233,7 +234,11 @@ def find_candidates(self, requirements, constraint):
# If none of the requirements want an explicit candidate, we can ask
# the finder for candidates.
if not explicit_candidates:
return self._iter_found_candidates(ireqs, constraint)
return self._iter_found_candidates(
ireqs,
constraint.specifier,
constraint.hashes,
)

if constraint:
name = explicit_candidates.pop().name
Expand Down
7 changes: 4 additions & 3 deletions src/pip/_internal/resolution/resolvelib/provider.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
from pip._vendor.packaging.specifiers import SpecifierSet
from pip._vendor.resolvelib.providers import AbstractProvider

from pip._internal.utils.typing import MYPY_CHECK_RUNNING

from .base import Constraint

if MYPY_CHECK_RUNNING:
from typing import (
Any,
Expand Down Expand Up @@ -41,7 +42,7 @@ class PipProvider(AbstractProvider):
def __init__(
self,
factory, # type: Factory
constraints, # type: Dict[str, SpecifierSet]
constraints, # type: Dict[str, Constraint]
ignore_dependencies, # type: bool
upgrade_strategy, # type: str
user_requested, # type: Set[str]
Expand Down Expand Up @@ -141,7 +142,7 @@ def find_matches(self, requirements):
if not requirements:
return []
constraint = self._constraints.get(
requirements[0].name, SpecifierSet(),
requirements[0].name, Constraint.empty(),
)
candidates = self._factory.find_candidates(requirements, constraint)
return reversed(self._sort_matches(candidates))
Expand Down
8 changes: 4 additions & 4 deletions src/pip/_internal/resolution/resolvelib/resolver.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,12 +14,12 @@
from pip._internal.utils.misc import dist_is_editable
from pip._internal.utils.typing import MYPY_CHECK_RUNNING

from .base import Constraint
from .factory import Factory

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

from pip._vendor.packaging.specifiers import SpecifierSet
from pip._vendor.resolvelib.resolvers import Result
from pip._vendor.resolvelib.structs import Graph

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

constraints = {} # type: Dict[str, SpecifierSet]
constraints = {} # type: Dict[str, Constraint]
user_requested = set() # type: Set[str]
requirements = []
for req in root_reqs:
Expand All @@ -94,9 +94,9 @@ def resolve(self, root_reqs, check_supported_wheels):
continue
name = canonicalize_name(req.name)
if name in constraints:
constraints[name] = constraints[name] & req.specifier
constraints[name] &= req
else:
constraints[name] = req.specifier
constraints[name] = Constraint.from_ireq(req)
else:
if req.user_supplied and req.name:
user_requested.add(canonicalize_name(req.name))
Expand Down
20 changes: 14 additions & 6 deletions src/pip/_internal/utils/hashes.py
Original file line number Diff line number Diff line change
Expand Up @@ -46,16 +46,24 @@ def __init__(self, hashes=None):
"""
self._allowed = {} if hashes is None else hashes

def __or__(self, other):
def __and__(self, other):
# type: (Hashes) -> Hashes
if not isinstance(other, Hashes):
return NotImplemented
new = self._allowed.copy()

# If either of the Hashes object is entirely empty (i.e. no hash
# specified at all), all hashes from the other object are allowed.
if not other:
return self
if not self:
return other

# Otherwise only hashes that present in both objects are allowed.
new = {}
for alg, values in iteritems(other._allowed):
try:
new[alg] += values
except KeyError:
new[alg] = values
if alg not in self._allowed:
continue
new[alg] = [v for v in values if v in self._allowed[alg]]
return Hashes(new)

@property
Expand Down
Loading

0 comments on commit 8945fe3

Please sign in to comment.