From e7a44f386097e061b973a548a10dfb21c6b85ed0 Mon Sep 17 00:00:00 2001 From: Doug Hellmann Date: Sun, 19 Apr 2020 12:45:00 -0400 Subject: [PATCH] add --prefer-minimum-versions flag Add a flag to the resolver to make it try to use older versions instead of newer versions for all dependencies. This is useful for running test suites using the "lower bounds" of requirements to ensure that they are accurate. Signed-off-by: Doug Hellmann --- news/8085.feature | 5 +++++ src/pip/_internal/cli/cmdoptions.py | 11 ++++++++++ src/pip/_internal/cli/req_command.py | 2 ++ .../resolution/resolvelib/provider.py | 5 ++++- .../resolution/resolvelib/resolver.py | 8 ++++++- src/pip/_vendor/resolvelib/resolvers.py | 21 +++++++++++++------ 6 files changed, 44 insertions(+), 8 deletions(-) create mode 100644 news/8085.feature diff --git a/news/8085.feature b/news/8085.feature new file mode 100644 index 00000000000..e1d56dd2aab --- /dev/null +++ b/news/8085.feature @@ -0,0 +1,5 @@ +Add a ``--prefer-minimum-versions`` command line flag to tell pip to +use older versions instead of newer versions for all +dependencies. This is useful for running test suites using the "lower +bounds" of requirements to ensure that they are accurate. The flag is +only available when the new resolver is enabled. diff --git a/src/pip/_internal/cli/cmdoptions.py b/src/pip/_internal/cli/cmdoptions.py index ff9acfd4644..5ba79b1ce1c 100644 --- a/src/pip/_internal/cli/cmdoptions.py +++ b/src/pip/_internal/cli/cmdoptions.py @@ -930,6 +930,17 @@ def check_list_path_option(options): ) # type: Callable[..., Option] +prefer_minimum_versions = partial( + Option, + '--prefer-minimum-versions', + dest='prefer_minimum_versions', + action='store_true', + default=False, + help=SUPPRESS_HELP, # TODO: Enable this when the resolver actually works. + # help='Use the lowest version that matches a requirement.', +) # type: Callable[..., Option] + + ########## # groups # ########## diff --git a/src/pip/_internal/cli/req_command.py b/src/pip/_internal/cli/req_command.py index 104b033281f..c722ae6e3e6 100644 --- a/src/pip/_internal/cli/req_command.py +++ b/src/pip/_internal/cli/req_command.py @@ -200,6 +200,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()) @staticmethod def make_requirement_preparer( @@ -274,6 +275,7 @@ def make_resolver( force_reinstall=force_reinstall, upgrade_strategy=upgrade_strategy, 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( diff --git a/src/pip/_internal/resolution/resolvelib/provider.py b/src/pip/_internal/resolution/resolvelib/provider.py index 5c3d210a31a..775b5f898ef 100644 --- a/src/pip/_internal/resolution/resolvelib/provider.py +++ b/src/pip/_internal/resolution/resolvelib/provider.py @@ -16,10 +16,12 @@ def __init__( self, factory, # type: Factory ignore_dependencies, # type: bool + prefer_minimum_versions, # type: bool ): # type: (...) -> None self._factory = factory self._ignore_dependencies = ignore_dependencies + self._prefer_minimum_versions = prefer_minimum_versions def get_install_requirement(self, c): # type: (Candidate) -> Optional[InstallRequirement] @@ -36,7 +38,8 @@ def get_preference( information # type: Sequence[Tuple[Requirement, Candidate]] ): # type: (...) -> Any - # Use the "usual" value for now + if self._prefer_minimum_versions: + return 0 return len(candidates) def find_matches(self, requirement): diff --git a/src/pip/_internal/resolution/resolvelib/resolver.py b/src/pip/_internal/resolution/resolvelib/resolver.py index cba5a496508..ed25fe53045 100644 --- a/src/pip/_internal/resolution/resolvelib/resolver.py +++ b/src/pip/_internal/resolution/resolvelib/resolver.py @@ -43,6 +43,7 @@ def __init__( force_reinstall, # type: bool upgrade_strategy, # type: str py_version_info=None, # type: Optional[Tuple[int, ...]] + prefer_minimum_versions=False, # type: bool ): super(Resolver, self).__init__() self.factory = Factory( @@ -55,6 +56,7 @@ def __init__( py_version_info=py_version_info, ) self.ignore_dependencies = ignore_dependencies + self.prefer_minimum_versions = prefer_minimum_versions self._result = None # type: Optional[Result] def resolve(self, root_reqs, check_supported_wheels): @@ -67,6 +69,7 @@ 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, ) reporter = BaseReporter() resolver = RLResolver(provider, reporter) @@ -77,7 +80,10 @@ def resolve(self, root_reqs, check_supported_wheels): ] try: - self._result = resolver.resolve(requirements) + self._result = resolver.resolve( + requirements, + prefer_minimum_versions=self.prefer_minimum_versions, + ) except ResolutionImpossible as e: error = self.factory.get_installation_error(e) diff --git a/src/pip/_vendor/resolvelib/resolvers.py b/src/pip/_vendor/resolvelib/resolvers.py index b51d337d231..90d602ba250 100644 --- a/src/pip/_vendor/resolvelib/resolvers.py +++ b/src/pip/_vendor/resolvelib/resolvers.py @@ -216,9 +216,13 @@ def _get_criteria_to_update(self, candidate): criteria[name] = crit return criteria - def _attempt_to_pin_criterion(self, name, criterion): + def _attempt_to_pin_criterion(self, name, criterion, prefer_minimum_versions): causes = [] - for candidate in reversed(criterion.candidates): + if prefer_minimum_versions: + candidates_in_order = criterion.candidates + else: + candidates_in_order = reversed(criterion.candidates) + for candidate in candidates_in_order: try: criteria = self._get_criteria_to_update(candidate) except RequirementsConflicted as e: @@ -270,7 +274,7 @@ def _backtrack(self): return False - def resolve(self, requirements, max_rounds): + def resolve(self, requirements, max_rounds, prefer_minimum_versions): if self._states: raise RuntimeError("already resolved") @@ -307,7 +311,8 @@ def resolve(self, requirements, max_rounds): unsatisfied_criterion_items, key=self._get_criterion_item_preference, ) - failure_causes = self._attempt_to_pin_criterion(name, criterion) + failure_causes = self._attempt_to_pin_criterion( + name, criterion, prefer_minimum_versions) # Backtrack if pinning fails. if failure_causes: @@ -381,7 +386,7 @@ class Resolver(AbstractResolver): base_exception = ResolverException - def resolve(self, requirements, max_rounds=100): + def resolve(self, requirements, max_rounds=100, prefer_minimum_versions=False): """Take a collection of constraints, spit out the resolution result. The return value is a representation to the final resolution result. It @@ -410,5 +415,9 @@ def resolve(self, requirements, max_rounds=100): `max_rounds` argument. """ resolution = Resolution(self.provider, self.reporter) - state = resolution.resolve(requirements, max_rounds=max_rounds) + state = resolution.resolve( + requirements, + max_rounds=max_rounds, + prefer_minimum_versions=prefer_minimum_versions, + ) return _build_result(state)