1
1
Fork 0
mirror of https://github.com/pypa/pip synced 2023-12-13 21:30:23 +01:00

Split find_matches into generation and sorting

This commit is contained in:
Paul Moore 2020-05-13 17:52:09 +01:00
parent d911a9fcb8
commit ddbc8fd7c1
5 changed files with 127 additions and 63 deletions

View file

@ -101,6 +101,14 @@ def make_install_req_from_dist(dist, parent):
return ireq return ireq
def is_already_installed(cand):
# type: (Candidate) -> bool
# For an ExtrasCandidate, we check the base
if isinstance(cand, ExtrasCandidate):
cand = cand.base
return isinstance(cand, AlreadyInstalledCandidate)
class _InstallRequirementBackedCandidate(Candidate): class _InstallRequirementBackedCandidate(Candidate):
def __init__( def __init__(
self, self,

View file

@ -42,8 +42,6 @@ if MYPY_CHECK_RUNNING:
class Factory(object): class Factory(object):
_allowed_strategies = {"eager", "only-if-needed", "to-satisfy-only"}
def __init__( def __init__(
self, self,
finder, # type: PackageFinder finder, # type: PackageFinder
@ -52,11 +50,9 @@ class Factory(object):
force_reinstall, # type: bool force_reinstall, # type: bool
ignore_installed, # type: bool ignore_installed, # type: bool
ignore_requires_python, # type: bool ignore_requires_python, # type: bool
upgrade_strategy, # type: str
py_version_info=None, # type: Optional[Tuple[int, ...]] py_version_info=None, # type: Optional[Tuple[int, ...]]
): ):
# type: (...) -> None # type: (...) -> None
assert upgrade_strategy in self._allowed_strategies
self.finder = finder self.finder = finder
self.preparer = preparer self.preparer = preparer
@ -64,9 +60,6 @@ class Factory(object):
self._make_install_req_from_spec = make_install_req self._make_install_req_from_spec = make_install_req
self._force_reinstall = force_reinstall self._force_reinstall = force_reinstall
self._ignore_requires_python = ignore_requires_python self._ignore_requires_python = ignore_requires_python
self._upgrade_strategy = upgrade_strategy
self.root_reqs = set() # type: Set[str]
self._link_candidate_cache = {} # type: Cache[LinkCandidate] self._link_candidate_cache = {} # type: Cache[LinkCandidate]
self._editable_candidate_cache = {} # type: Cache[EditableCandidate] self._editable_candidate_cache = {} # type: Cache[EditableCandidate]
@ -118,23 +111,27 @@ class Factory(object):
return ExtrasCandidate(base, extras) return ExtrasCandidate(base, extras)
return base return base
def _eligible_for_upgrade(self, dist_name):
# type: (str) -> bool
if self._upgrade_strategy == "eager":
return True
elif self._upgrade_strategy == "only-if-needed":
return (dist_name in self.root_reqs)
return False
def iter_found_candidates(self, ireq, extras): def iter_found_candidates(self, ireq, extras):
# type: (InstallRequirement, Set[str]) -> Iterator[Candidate] # type: (InstallRequirement, Set[str]) -> Iterator[Candidate]
name = canonicalize_name(ireq.req.name) name = canonicalize_name(ireq.req.name)
if not self._force_reinstall: seen_versions = set()
installed_dist = self._installed_dists.get(name)
can_upgrade = self._eligible_for_upgrade(name) # Yield the installed version, if it matches, unless the user
else: # specified `--force-reinstall`, when we want the version from
installed_dist = None # the index instead.
can_upgrade = False if not self._force_reinstall and name in self._installed_dists:
installed_dist = self._installed_dists[name]
installed_version = installed_dist.parsed_version
if ireq.req.specifier.contains(
installed_version,
prereleases=True
):
seen_versions.add(installed_version)
yield self._make_candidate_from_dist(
dist=installed_dist,
extras=extras,
parent=ireq,
)
found = self.finder.find_best_candidate( found = self.finder.find_best_candidate(
project_name=ireq.req.name, project_name=ireq.req.name,
@ -142,40 +139,18 @@ class Factory(object):
hashes=ireq.hashes(trust_internet=False), hashes=ireq.hashes(trust_internet=False),
) )
for ican in found.iter_applicable(): for ican in found.iter_applicable():
if (installed_dist is not None and if ican.version not in seen_versions:
installed_dist.parsed_version == ican.version): seen_versions.add(ican.version)
if can_upgrade: yield self._make_candidate_from_link(
yield self._make_candidate_from_dist( link=ican.link,
dist=installed_dist, extras=extras,
extras=extras, parent=ireq,
parent=ireq, name=name,
) version=ican.version,
continue )
yield self._make_candidate_from_link(
link=ican.link,
extras=extras,
parent=ireq,
name=name,
version=ican.version,
)
# Return installed distribution if it matches the specifier. This is
# done last so the resolver will prefer it over downloading links.
if can_upgrade or installed_dist is None:
return
installed_version = installed_dist.parsed_version
if ireq.req.specifier.contains(installed_version, prereleases=True):
yield self._make_candidate_from_dist(
dist=installed_dist,
extras=extras,
parent=ireq,
)
def make_requirement_from_install_req(self, ireq): def make_requirement_from_install_req(self, ireq):
# type: (InstallRequirement) -> Requirement # type: (InstallRequirement) -> Requirement
if ireq.is_direct and ireq.name:
self.root_reqs.add(canonicalize_name(ireq.name))
if ireq.link: if ireq.link:
# TODO: Get name and version from ireq, if possible? # TODO: Get name and version from ireq, if possible?
# Specifically, this might be needed in "name @ URL" # Specifically, this might be needed in "name @ URL"

View file

@ -3,14 +3,35 @@ from pip._vendor.resolvelib.providers import AbstractProvider
from pip._internal.utils.typing import MYPY_CHECK_RUNNING from pip._internal.utils.typing import MYPY_CHECK_RUNNING
from .candidates import is_already_installed
if MYPY_CHECK_RUNNING: if MYPY_CHECK_RUNNING:
from typing import Any, Dict, Optional, Sequence, Tuple, Union from typing import Any, Dict, Optional, Sequence, Set, Tuple, Union
from pip._internal.req.req_install import InstallRequirement from pip._internal.req.req_install import InstallRequirement
from pip._vendor.packaging.version import _BaseVersion
from .base import Requirement, Candidate from .base import Requirement, Candidate
from .factory import Factory from .factory import Factory
# Notes on the relationship between the provider, the factory, and the
# candidate and requirement classes.
#
# The provider is a direct implementation of the resolvelib class. Its role
# is to deliver the API that resolvelib expects.
#
# Rather than work with completely abstract "requirement" and "candidate"
# concepts as resolvelib does, pip has concrete classes implementing these two
# ideas. The API of Requirement and Candidate objects are defined in the base
# classes, but essentially map fairly directly to the equivalent provider
# methods. In particular, `find_matches` and `is_satisfied_by` are
# requirement methods, and `get_dependencies` is a candidate method.
#
# The factory is the interface to pip's internal mechanisms. It is stateless,
# and is created by the resolver and held as a property of the provider. It is
# responsible for creating Requirement and Candidate objects, and provides
# services to those objects (access to pip's finder and preparer).
class PipProvider(AbstractProvider): class PipProvider(AbstractProvider):
def __init__( def __init__(
@ -18,11 +39,66 @@ class PipProvider(AbstractProvider):
factory, # type: Factory factory, # type: Factory
constraints, # type: Dict[str, SpecifierSet] constraints, # type: Dict[str, SpecifierSet]
ignore_dependencies, # type: bool ignore_dependencies, # type: bool
upgrade_strategy, # type: str
roots, # type: Set[str]
): ):
# type: (...) -> None # type: (...) -> None
self._factory = factory self._factory = factory
self._constraints = constraints self._constraints = constraints
self._ignore_dependencies = ignore_dependencies self._ignore_dependencies = ignore_dependencies
self._upgrade_strategy = upgrade_strategy
self.roots = roots
def sort_matches(self, matches):
# type: (Sequence[Candidate]) -> Sequence[Candidate]
# The requirement is responsible for returning a sequence of potential
# candidates, one per version. The provider handles the logic of
# deciding the order in which these candidates should be passed to
# the resolver.
# The `matches` argument is a sequence of candidates, one per version,
# which are potential options to be installed. The requirement will
# have already sorted out whether to give us an already-installed
# candidate or a version from PyPI (i.e., it will deal with options
# like --force-reinstall and --ignore-installed).
# We now work out the correct order.
#
# 1. If no other considerations apply, later versions take priority.
# 2. An already installed distribution is preferred over any other,
# unless the user has requested an upgrade.
# Upgrades are allowed when:
# * The --upgrade flag is set, and
# - The project was specified on the command line, or
# - The project is a dependency and the "eager" upgrade strategy
# was requested.
def _eligible_for_upgrade(name):
# type: (str) -> bool
if self._upgrade_strategy == "eager":
return True
elif self._upgrade_strategy == "only-if-needed":
print(name, self.roots)
return (name in self.roots)
return False
def keep_installed(c):
# type: (Candidate) -> int
"""Give priority to an installed version?"""
if not is_already_installed(c):
return 0
if _eligible_for_upgrade(c.name):
return 0
return 1
def key(c):
# type: (Candidate) -> Tuple[int, _BaseVersion]
return (keep_installed(c), c.version)
return sorted(matches, key=key)
def get_install_requirement(self, c): def get_install_requirement(self, c):
# type: (Candidate) -> Optional[InstallRequirement] # type: (Candidate) -> Optional[InstallRequirement]
@ -45,7 +121,8 @@ class PipProvider(AbstractProvider):
def find_matches(self, requirement): def find_matches(self, requirement):
# type: (Requirement) -> Sequence[Candidate] # type: (Requirement) -> Sequence[Candidate]
constraint = self._constraints.get(requirement.name, SpecifierSet()) constraint = self._constraints.get(requirement.name, SpecifierSet())
return requirement.find_matches(constraint) matches = requirement.find_matches(constraint)
return self.sort_matches(matches)
def is_satisfied_by(self, requirement, candidate): def is_satisfied_by(self, requirement, candidate):
# type: (Requirement, Candidate) -> bool # type: (Requirement, Candidate) -> bool

View file

@ -32,6 +32,8 @@ logger = logging.getLogger(__name__)
class Resolver(BaseResolver): class Resolver(BaseResolver):
_allowed_strategies = {"eager", "only-if-needed", "to-satisfy-only"}
def __init__( def __init__(
self, self,
preparer, # type: RequirementPreparer preparer, # type: RequirementPreparer
@ -47,6 +49,9 @@ class Resolver(BaseResolver):
py_version_info=None, # type: Optional[Tuple[int, ...]] py_version_info=None, # type: Optional[Tuple[int, ...]]
): ):
super(Resolver, self).__init__() super(Resolver, self).__init__()
assert upgrade_strategy in self._allowed_strategies
self.factory = Factory( self.factory = Factory(
finder=finder, finder=finder,
preparer=preparer, preparer=preparer,
@ -54,23 +59,17 @@ class Resolver(BaseResolver):
force_reinstall=force_reinstall, force_reinstall=force_reinstall,
ignore_installed=ignore_installed, ignore_installed=ignore_installed,
ignore_requires_python=ignore_requires_python, ignore_requires_python=ignore_requires_python,
upgrade_strategy=upgrade_strategy,
py_version_info=py_version_info, py_version_info=py_version_info,
) )
self.ignore_dependencies = ignore_dependencies self.ignore_dependencies = ignore_dependencies
self.upgrade_strategy = upgrade_strategy
self._result = None # type: Optional[Result] self._result = None # type: Optional[Result]
def resolve(self, root_reqs, check_supported_wheels): def resolve(self, root_reqs, check_supported_wheels):
# type: (List[InstallRequirement], bool) -> RequirementSet # type: (List[InstallRequirement], bool) -> RequirementSet
# The factory should not have retained state from any previous usage.
# In theory this could only happen if self was reused to do a second
# resolve, which isn't something we do at the moment. We assert here
# in order to catch the issue if that ever changes.
# The persistent state that we care about is `root_reqs`.
assert len(self.factory.root_reqs) == 0, "Factory is being re-used"
constraints = {} # type: Dict[str, SpecifierSet] constraints = {} # type: Dict[str, SpecifierSet]
roots = set()
requirements = [] requirements = []
for req in root_reqs: for req in root_reqs:
if req.constraint: if req.constraint:
@ -82,6 +81,8 @@ class Resolver(BaseResolver):
else: else:
constraints[name] = req.specifier constraints[name] = req.specifier
else: else:
if req.is_direct and req.name:
roots.add(canonicalize_name(req.name))
requirements.append( requirements.append(
self.factory.make_requirement_from_install_req(req) self.factory.make_requirement_from_install_req(req)
) )
@ -90,6 +91,8 @@ class Resolver(BaseResolver):
factory=self.factory, factory=self.factory,
constraints=constraints, constraints=constraints,
ignore_dependencies=self.ignore_dependencies, ignore_dependencies=self.ignore_dependencies,
upgrade_strategy=self.upgrade_strategy,
roots=roots,
) )
reporter = BaseReporter() reporter = BaseReporter()
resolver = RLResolver(provider, reporter) resolver = RLResolver(provider, reporter)

View file

@ -55,7 +55,6 @@ def factory(finder, preparer):
force_reinstall=False, force_reinstall=False,
ignore_installed=False, ignore_installed=False,
ignore_requires_python=False, ignore_requires_python=False,
upgrade_strategy="to-satisfy-only",
py_version_info=None, py_version_info=None,
) )
@ -66,4 +65,6 @@ def provider(factory):
factory=factory, factory=factory,
constraints={}, constraints={},
ignore_dependencies=False, ignore_dependencies=False,
upgrade_strategy="to-satisfy-only",
roots=set(),
) )