Skip to content

Commit

Permalink
add --prefer-minimum-versions flag
Browse files Browse the repository at this point in the history
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 <doug@doughellmann.com>
  • Loading branch information
dhellmann committed Apr 19, 2020
1 parent 97f6390 commit e7a44f3
Show file tree
Hide file tree
Showing 6 changed files with 44 additions and 8 deletions.
5 changes: 5 additions & 0 deletions news/8085.feature
Original file line number Diff line number Diff line change
@@ -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.
11 changes: 11 additions & 0 deletions src/pip/_internal/cli/cmdoptions.py
Original file line number Diff line number Diff line change
Expand Up @@ -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 #
##########
Expand Down
2 changes: 2 additions & 0 deletions src/pip/_internal/cli/req_command.py
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand Down Expand Up @@ -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(
Expand Down
5 changes: 4 additions & 1 deletion src/pip/_internal/resolution/resolvelib/provider.py
Original file line number Diff line number Diff line change
Expand Up @@ -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]
Expand All @@ -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):
Expand Down
8 changes: 7 additions & 1 deletion src/pip/_internal/resolution/resolvelib/resolver.py
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand All @@ -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):
Expand All @@ -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)
Expand All @@ -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)
Expand Down
21 changes: 15 additions & 6 deletions src/pip/_vendor/resolvelib/resolvers.py
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down Expand Up @@ -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")

Expand Down Expand Up @@ -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:
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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)

0 comments on commit e7a44f3

Please sign in to comment.