Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add --no-backtracking option for new resolver #9258

Closed
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions news/9258.feature.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
New resolver: Add ``--no-backtracking`` option to enable failfast debugging.
11 changes: 11 additions & 0 deletions src/pip/_internal/cli/cmdoptions.py
Original file line number Diff line number Diff line change
Expand Up @@ -927,6 +927,17 @@ def check_list_path_option(options):
),
) # type: Callable[..., Option]

disable_backtracking = partial(
Option,
'--no-backtracking',
dest='disable_backtracking',
action='store_true',
default=False,
help='Do not attempt to backtrack to resolve package conflicts. This will '
'cause conflicts which may be otherwise automatically solveable to '
'fail fast and require manual intervention!',
) # type: Callable[..., Option]


##########
# groups #
Expand Down
4 changes: 3 additions & 1 deletion src/pip/_internal/commands/download.py
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,7 @@ def add_options(self):
self.cmd_opts.add_option(cmdoptions.no_build_isolation())
self.cmd_opts.add_option(cmdoptions.use_pep517())
self.cmd_opts.add_option(cmdoptions.no_use_pep517())
self.cmd_opts.add_option(cmdoptions.disable_backtracking())

self.cmd_opts.add_option(
'-d', '--dest', '--destination-dir', '--destination-directory',
Expand Down Expand Up @@ -128,7 +129,8 @@ def run(self, options, args):
self.trace_basic_info(finder)

requirement_set = resolver.resolve(
reqs, check_supported_wheels=True
reqs, check_supported_wheels=True,
should_backtrack=not options.disable_backtracking
)

downloaded = [] # type: List[str]
Expand Down
4 changes: 3 additions & 1 deletion src/pip/_internal/commands/install.py
Original file line number Diff line number Diff line change
Expand Up @@ -216,6 +216,7 @@ def add_options(self):
self.cmd_opts.add_option(cmdoptions.prefer_binary())
self.cmd_opts.add_option(cmdoptions.require_hashes())
self.cmd_opts.add_option(cmdoptions.progress_bar())
self.cmd_opts.add_option(cmdoptions.disable_backtracking())

index_opts = cmdoptions.make_option_group(
cmdoptions.index_group,
Expand Down Expand Up @@ -318,7 +319,8 @@ def run(self, options, args):
self.trace_basic_info(finder)

requirement_set = resolver.resolve(
reqs, check_supported_wheels=not options.target_dir
reqs, check_supported_wheels=not options.target_dir,
should_backtrack=not options.disable_backtracking
)

try:
Expand Down
4 changes: 3 additions & 1 deletion src/pip/_internal/commands/wheel.py
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,7 @@ def add_options(self):
self.cmd_opts.add_option(cmdoptions.no_deps())
self.cmd_opts.add_option(cmdoptions.build_dir())
self.cmd_opts.add_option(cmdoptions.progress_bar())
self.cmd_opts.add_option(cmdoptions.disable_backtracking())

self.cmd_opts.add_option(
'--global-option',
Expand Down Expand Up @@ -152,7 +153,8 @@ def run(self, options, args):
self.trace_basic_info(finder)

requirement_set = resolver.resolve(
reqs, check_supported_wheels=True
reqs, check_supported_wheels=True,
should_backtrack=not options.disable_backtracking
)

reqs_to_build = [] # type: List[InstallRequirement]
Expand Down
4 changes: 2 additions & 2 deletions src/pip/_internal/resolution/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,8 @@


class BaseResolver(object):
def resolve(self, root_reqs, check_supported_wheels):
# type: (List[InstallRequirement], bool) -> RequirementSet
def resolve(self, root_reqs, check_supported_wheels, should_backtrack):
# type: (List[InstallRequirement], bool, bool) -> RequirementSet
raise NotImplementedError()

def get_installation_order(self, req_set):
Expand Down
5 changes: 3 additions & 2 deletions src/pip/_internal/resolution/legacy/resolver.py
Original file line number Diff line number Diff line change
Expand Up @@ -149,8 +149,9 @@ def __init__(
self._discovered_dependencies = \
defaultdict(list) # type: DiscoveredDependencies

def resolve(self, root_reqs, check_supported_wheels):
# type: (List[InstallRequirement], bool) -> RequirementSet
def resolve(self, root_reqs, check_supported_wheels, should_backtrack):
# type: (List[InstallRequirement], bool, bool) -> RequirementSet
# pylint: disable=unused-argument
"""Resolve what operations need to be done

As a side-effect of this method, the packages (and their dependencies)
Expand Down
5 changes: 3 additions & 2 deletions src/pip/_internal/resolution/resolvelib/resolver.py
Original file line number Diff line number Diff line change
Expand Up @@ -75,8 +75,8 @@ def __init__(
self.upgrade_strategy = upgrade_strategy
self._result = None # type: Optional[Result]

def resolve(self, root_reqs, check_supported_wheels):
# type: (List[InstallRequirement], bool) -> RequirementSet
def resolve(self, root_reqs, check_supported_wheels, should_backtrack):
# type: (List[InstallRequirement], bool, bool) -> RequirementSet

constraints = {} # type: Dict[str, Constraint]
user_requested = set() # type: Set[str]
Expand Down Expand Up @@ -120,6 +120,7 @@ def resolve(self, root_reqs, check_supported_wheels):
try_to_avoid_resolution_too_deep = 2000000
self._result = resolver.resolve(
requirements, max_rounds=try_to_avoid_resolution_too_deep,
should_backtrack=should_backtrack,
)

except ResolutionImpossible as e:
Expand Down
2 changes: 1 addition & 1 deletion src/pip/_vendor/resolvelib/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@
"ResolutionTooDeep",
]

__version__ = "0.5.3"
__version__ = "0.5.4.dev0"


from .providers import AbstractProvider, AbstractResolver
Expand Down
12 changes: 8 additions & 4 deletions src/pip/_vendor/resolvelib/resolvers.py
Original file line number Diff line number Diff line change
Expand Up @@ -297,7 +297,7 @@ def _backtrack(self):
# No way to backtrack anymore.
return False

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

Expand Down Expand Up @@ -341,7 +341,7 @@ def resolve(self, requirements, max_rounds):
if failure_causes:
# Backtrack if pinning fails. The backtrack process puts us in
# an unpinned state, so we can work on it in the next round.
success = self._backtrack()
success = self._backtrack() if should_backtrack else False

# Dead ends everywhere. Give up.
if not success:
Expand Down Expand Up @@ -413,7 +413,7 @@ class Resolver(AbstractResolver):

base_exception = ResolverException

def resolve(self, requirements, max_rounds=100):
def resolve(self, requirements, max_rounds=100, should_backtrack=True):
"""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 @@ -442,5 +442,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,
should_backtrack=should_backtrack,
)
return _build_result(state)
2 changes: 1 addition & 1 deletion src/pip/_vendor/vendor.txt
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ requests==2.25.0
chardet==3.0.4
idna==2.10
urllib3==1.26.2
resolvelib==0.5.3
git+https://github.com/thekevjames/resolvelib.git@add-no-backtracking-option
retrying==1.3.3
setuptools==44.0.0
six==1.15.0
Expand Down
49 changes: 49 additions & 0 deletions tests/functional/test_install.py
Original file line number Diff line number Diff line change
Expand Up @@ -1726,6 +1726,55 @@ def test_install_conflict_warning_can_be_suppressed(script, data):
assert "Successfully installed pkgB-2.0" in result2.stdout, str(result2)


def test_install_incompatiblity_can_failfast(script, data):
# Build the following package dependency graph, which is impossible to
# resolve:
#
# /- pkgB == 1.0 -> pkgD == 1.0
# / - pkgB == 1.1 -> pkgD == 1.0
# pkgA==1.0 -
# \ - pkgC == 1.0 -> pkgD == 1.1
# \- pkgC == 1.1 -> pkgD == 1.1
pkgA_path = create_basic_wheel_for_package(
script,
name='pkgA', version='1.0', depends=[
'pkgb > 1.0, < 2.0',
'pkgc > 1.0, < 2.0',
],
)
for minor in {0, 1}:
create_basic_wheel_for_package(
script,
name='pkgB', version='1.%s' % minor,
depends=['pkgd == 1.0'],
)
create_basic_wheel_for_package(
script,
name='pkgC', version='1.%s' % minor,
depends=['pkgd == 1.1'],
)
create_basic_wheel_for_package(
script,
name='pkgD', version='1.%s' % minor,
)

# Installing normally should run backtracking...
result1 = script.pip(
'install', '--no-index', '--find-links', script.scratch_path,
pkgA_path, expect_error=True
)
assert 'pip is looking at multiple versions of pkg' in result1.stdout
assert 'ResolutionImpossible' in result1.stderr

# ...but the ``--no-backtracking`` flag will prevent this behaviour
result2 = script.pip(
'install', '--no-index', '--find-links', script.scratch_path,
'--no-backtracking', pkgA_path, expect_error=True
)
assert 'pip is looking at multiple versions of pkg' not in result2.stdout
assert 'ResolutionImpossible' in result2.stderr


def test_target_install_ignores_distutils_config_install_prefix(script):
prefix = script.scratch_path / 'prefix'
distutils_config = Path(os.path.expanduser('~'),
Expand Down
8 changes: 7 additions & 1 deletion tests/unit/test_req.py
Original file line number Diff line number Diff line change
Expand Up @@ -121,6 +121,7 @@ def test_no_reuse_existing_build_dir(self, data):
resolver.resolve,
reqset.all_requirements,
True,
True,
)

# TODO: Update test when Python 2.7 is dropped.
Expand All @@ -137,7 +138,7 @@ def test_environment_marker_extras(self, data):
reqset.add_requirement(req)
finder = make_test_finder(find_links=[data.find_links])
with self._basic_resolver(finder) as resolver:
reqset = resolver.resolve(reqset.all_requirements, True)
reqset = resolver.resolve(reqset.all_requirements, True, True)
# This is hacky but does test both case in py2 and py3
if sys.version_info[:2] == (2, 7):
assert reqset.has_requirement('simple')
Expand Down Expand Up @@ -165,6 +166,7 @@ def test_missing_hash_with_require_hashes(self, data):
resolver.resolve,
reqset.all_requirements,
True,
True,
)

def test_missing_hash_with_require_hashes_in_reqs_file(self, data, tmpdir):
Expand Down Expand Up @@ -217,6 +219,7 @@ def test_unsupported_hashes(self, data):
resolver.resolve,
reqset.all_requirements,
True,
True,
)

def test_unpinned_hash_checking(self, data):
Expand Down Expand Up @@ -246,6 +249,7 @@ def test_unpinned_hash_checking(self, data):
resolver.resolve,
reqset.all_requirements,
True,
True,
)

def test_hash_mismatch(self, data):
Expand All @@ -268,6 +272,7 @@ def test_hash_mismatch(self, data):
resolver.resolve,
reqset.all_requirements,
True,
True,
)

def test_unhashed_deps_on_require_hashes(self, data):
Expand All @@ -291,6 +296,7 @@ def test_unhashed_deps_on_require_hashes(self, data):
resolver.resolve,
reqset.all_requirements,
True,
True,
)

def test_hashed_deps_on_require_hashes(self):
Expand Down