Skip to content

Commit

Permalink
implement strategies as classes
Browse files Browse the repository at this point in the history
Signed-off-by: Doug Hellmann <doug@doughellmann.com>
  • Loading branch information
dhellmann committed Apr 22, 2020
1 parent e7a44f3 commit de6e70d
Show file tree
Hide file tree
Showing 8 changed files with 111 additions and 39 deletions.
20 changes: 14 additions & 6 deletions src/pip/_internal/cli/cmdoptions.py
Original file line number Diff line number Diff line change
Expand Up @@ -930,14 +930,22 @@ def check_list_path_option(options):
) # type: Callable[..., Option]


prefer_minimum_versions = partial(
strategy = partial(
Option,
'--prefer-minimum-versions',
dest='prefer_minimum_versions',
action='store_true',
default=False,
'--strategy',
dest='strategy',
default='only-if-needed',
choices=['only-if-needed', 'eager', 'earliest-compatible'],
help=SUPPRESS_HELP, # TODO: Enable this when the resolver actually works.
# help='Use the lowest version that matches a requirement.',
# help='Determines how dependency resolution should be handled '
# '[default: %default]. '
# '"eager" - dependencies are upgraded regardless of '
# 'whether the currently installed version satisfies the '
# 'requirements of the upgraded package(s). '
# '"only-if-needed" - are upgraded only when they do not '
# 'satisfy the requirements of the upgraded package(s).'
# '"earliest-compatible" - dependencies with the lowest '
# 'compatible version number are selected.'
) # type: Callable[..., Option]


Expand Down
11 changes: 6 additions & 5 deletions src/pip/_internal/cli/req_command.py
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@
)
from pip._internal.utils.temp_dir import tempdir_kinds
from pip._internal.utils.typing import MYPY_CHECK_RUNNING
from pip._internal.resolution.resolvelib.strategy import strategy_factory

if MYPY_CHECK_RUNNING:
from optparse import Values
Expand Down Expand Up @@ -200,7 +201,7 @@ def __init__(self, *args, **kw):
super(RequirementCommand, self).__init__(*args, **kw)

self.cmd_opts.add_option(cmdoptions.no_clean())
self.cmd_opts.add_option(cmdoptions.prefer_minimum_versions())
self.cmd_opts.add_option(cmdoptions.strategy())

@staticmethod
def make_requirement_preparer(
Expand Down Expand Up @@ -245,7 +246,7 @@ def make_resolver(
ignore_installed=True, # type: bool
ignore_requires_python=False, # type: bool
force_reinstall=False, # type: bool
upgrade_strategy="to-satisfy-only", # type: str
strategy="to-satisfy-only", # type: str
use_pep517=None, # type: Optional[bool]
py_version_info=None # type: Optional[Tuple[int, ...]]
):
Expand All @@ -263,6 +264,7 @@ def make_resolver(
# "Resolver" class being redefined.
if 'resolver' in options.unstable_features:
import pip._internal.resolution.resolvelib.resolver
strategy_impl = strategy_factory(options.strategy)
return pip._internal.resolution.resolvelib.resolver.Resolver(
preparer=preparer,
finder=finder,
Expand All @@ -273,9 +275,8 @@ def make_resolver(
ignore_installed=ignore_installed,
ignore_requires_python=ignore_requires_python,
force_reinstall=force_reinstall,
upgrade_strategy=upgrade_strategy,
strategy=strategy_impl,
py_version_info=py_version_info,
prefer_minimum_versions=options.prefer_minimum_versions,
)
import pip._internal.resolution.legacy.resolver
return pip._internal.resolution.legacy.resolver.Resolver(
Expand All @@ -288,7 +289,7 @@ def make_resolver(
ignore_installed=ignore_installed,
ignore_requires_python=ignore_requires_python,
force_reinstall=force_reinstall,
upgrade_strategy=upgrade_strategy,
upgrade_strategy=strategy,
py_version_info=py_version_info,
)

Expand Down
9 changes: 6 additions & 3 deletions src/pip/_internal/commands/install.py
Original file line number Diff line number Diff line change
Expand Up @@ -241,9 +241,12 @@ def run(self, options, args):
raise CommandError("Can not combine '--user' and '--target'")

cmdoptions.check_install_build_global(options)
upgrade_strategy = "to-satisfy-only"
if options.upgrade:
upgrade_strategy = options.upgrade_strategy
if 'resolver' in options.unstable_features:
upgrade_strategy = options.strategy
else:
upgrade_strategy = "to-satisfy-only"
if options.upgrade:
upgrade_strategy = options.upgrade_strategy

cmdoptions.check_dist_restriction(options, check_target=True)

Expand Down
12 changes: 7 additions & 5 deletions src/pip/_internal/resolution/resolvelib/provider.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,12 +16,12 @@ def __init__(
self,
factory, # type: Factory
ignore_dependencies, # type: bool
prefer_minimum_versions, # type: bool
strategy,
):
# type: (...) -> None
self._factory = factory
self._ignore_dependencies = ignore_dependencies
self._prefer_minimum_versions = prefer_minimum_versions
self._strategy = strategy

def get_install_requirement(self, c):
# type: (Candidate) -> Optional[InstallRequirement]
Expand All @@ -38,9 +38,11 @@ def get_preference(
information # type: Sequence[Tuple[Requirement, Candidate]]
):
# type: (...) -> Any
if self._prefer_minimum_versions:
return 0
return len(candidates)
return self._strategy.get_preferred_candidate(
resolution,
candidates,
information,
)

def find_matches(self, requirement):
# type: (Requirement) -> Sequence[Candidate]
Expand Down
14 changes: 6 additions & 8 deletions src/pip/_internal/resolution/resolvelib/resolver.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
from pip._internal.req.req_set import RequirementSet
from pip._internal.resolution.base import BaseResolver
from pip._internal.resolution.resolvelib.provider import PipProvider
from pip._internal.resolution.resolvelib.strategy import AbstractStrategy
from pip._internal.utils.typing import MYPY_CHECK_RUNNING

from .factory import Factory
Expand Down Expand Up @@ -41,7 +42,7 @@ def __init__(
ignore_installed, # type: bool
ignore_requires_python, # type: bool
force_reinstall, # type: bool
upgrade_strategy, # type: str
strategy, # type: AbstractStrategy
py_version_info=None, # type: Optional[Tuple[int, ...]]
prefer_minimum_versions=False, # type: bool
):
Expand All @@ -56,7 +57,7 @@ def __init__(
py_version_info=py_version_info,
)
self.ignore_dependencies = ignore_dependencies
self.prefer_minimum_versions = prefer_minimum_versions
self.strategy = strategy
self._result = None # type: Optional[Result]

def resolve(self, root_reqs, check_supported_wheels):
Expand All @@ -69,21 +70,18 @@ def resolve(self, root_reqs, check_supported_wheels):
provider = PipProvider(
factory=self.factory,
ignore_dependencies=self.ignore_dependencies,
prefer_minimum_versions=self.prefer_minimum_versions,
strategy=self.strategy,
)
reporter = BaseReporter()
resolver = RLResolver(provider, reporter)
resolver = RLResolver(provider, reporter, self.strategy)

requirements = [
self.factory.make_requirement_from_install_req(r)
for r in root_reqs
]

try:
self._result = resolver.resolve(
requirements,
prefer_minimum_versions=self.prefer_minimum_versions,
)
self._result = resolver.resolve(requirements)

except ResolutionImpossible as e:
error = self.factory.get_installation_error(e)
Expand Down
62 changes: 62 additions & 0 deletions src/pip/_internal/resolution/resolvelib/strategy.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
import abc

from pip._vendor.six import add_metaclass


@add_metaclass(abc.ABCMeta)
class AbstractStrategy:

@abc.abstractmethod
def get_preferred_candidate(
self,
resolution, # type: Optional[Candidate]
candidates, # type: Sequence[Candidate]
information, # type: Sequence[Tuple[Requirement, Candidate]]
):
# type: (...) -> int
raise NotImplementedError()

@abc.abstractmethod
def sort_candidates(self, candidates):
# type: (Sequence[Candidates]) -> Sequence[Candidate]
raise NotImplementedError()


class DefaultStrategy(AbstractStrategy):

def get_preferred_candidate(
self,
resolution, # type: Optional[Candidate]
candidates, # type: Sequence[Candidate]
information, # type: Sequence[Tuple[Requirement, Candidate]]
):
# type: (...) -> int
return len(candidates)

def sort_candidates(self, candidates):
# type: (Sequence[Candidate]) -> Sequence[Candidate]
return reversed(candidates)


class EarliestCompatible(AbstractStrategy):

def get_preferred_candidate(
self,
resolution, # type: Optional[Candidate]
candidates, # type: Sequence[Candidate]
information, # type: Sequence[Tuple[Requirement, Candidate]]
):
# type: (...) -> int
return 0

def sort_candidates(self, candidates):
# type: (Sequence[Candidate]) -> Sequence[Candidate]
return candidates


def strategy_factory(name):
# type: (str) -> AbstractStrategy
f = {
'earliest-compatible': EarliestCompatible,
}.get(name, DefaultStrategy)
return f()
3 changes: 2 additions & 1 deletion src/pip/_vendor/resolvelib/providers.py
Original file line number Diff line number Diff line change
Expand Up @@ -85,9 +85,10 @@ class AbstractResolver(object):

base_exception = Exception

def __init__(self, provider, reporter):
def __init__(self, provider, reporter, strategy):
self.provider = provider
self.reporter = reporter
self.strategy = strategy

def resolve(self, requirements, **kwargs):
"""Take a collection of constraints, spit out the resolution result.
Expand Down
19 changes: 8 additions & 11 deletions src/pip/_vendor/resolvelib/resolvers.py
Original file line number Diff line number Diff line change
Expand Up @@ -150,9 +150,10 @@ class Resolution(object):
the resolution process, and holds the results afterwards.
"""

def __init__(self, provider, reporter):
def __init__(self, provider, reporter, strategy):
self._p = provider
self._r = reporter
self._strategy = strategy
self._states = []

@property
Expand Down Expand Up @@ -216,12 +217,9 @@ def _get_criteria_to_update(self, candidate):
criteria[name] = crit
return criteria

def _attempt_to_pin_criterion(self, name, criterion, prefer_minimum_versions):
def _attempt_to_pin_criterion(self, name, criterion):
causes = []
if prefer_minimum_versions:
candidates_in_order = criterion.candidates
else:
candidates_in_order = reversed(criterion.candidates)
candidates_in_order = self._strategy.sort_candidates(criterion.candidates)
for candidate in candidates_in_order:
try:
criteria = self._get_criteria_to_update(candidate)
Expand Down Expand Up @@ -274,7 +272,7 @@ def _backtrack(self):

return False

def resolve(self, requirements, max_rounds, prefer_minimum_versions):
def resolve(self, requirements, max_rounds):
if self._states:
raise RuntimeError("already resolved")

Expand Down Expand Up @@ -312,7 +310,7 @@ def resolve(self, requirements, max_rounds, prefer_minimum_versions):
key=self._get_criterion_item_preference,
)
failure_causes = self._attempt_to_pin_criterion(
name, criterion, prefer_minimum_versions)
name, criterion)

# Backtrack if pinning fails.
if failure_causes:
Expand Down Expand Up @@ -386,7 +384,7 @@ class Resolver(AbstractResolver):

base_exception = ResolverException

def resolve(self, requirements, max_rounds=100, prefer_minimum_versions=False):
def resolve(self, requirements, max_rounds=100):
"""Take a collection of constraints, spit out the resolution result.
The return value is a representation to the final resolution result. It
Expand Down Expand Up @@ -414,10 +412,9 @@ def resolve(self, requirements, max_rounds=100, prefer_minimum_versions=False):
dependency, but you can try to resolve this by increasing the
`max_rounds` argument.
"""
resolution = Resolution(self.provider, self.reporter)
resolution = Resolution(self.provider, self.reporter, self.strategy)
state = resolution.resolve(
requirements,
max_rounds=max_rounds,
prefer_minimum_versions=prefer_minimum_versions,
)
return _build_result(state)

0 comments on commit de6e70d

Please sign in to comment.