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

Prefer backtracking on dependencies involved in the most recent conflict #10481

Merged
merged 5 commits into from
Oct 9, 2021
Merged
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/10479.feature.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
When backtracking during dependency resolution, prefer the dependencies which are involved in the most recent conflict. This can significantly reduce the amount of backtracking required.
1 change: 1 addition & 0 deletions news/resolvelib.vendor.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Upgrade resolvelib to 0.8.0
2 changes: 1 addition & 1 deletion noxfile.py
Original file line number Diff line number Diff line change
Expand Up @@ -174,7 +174,7 @@ def vendoring(session: nox.Session) -> None:
session.install("vendoring~=1.0.0")

if "--upgrade" not in session.posargs:
session.run("vendoring", "sync", ".", "-v")
session.run("vendoring", "sync", "-v")
return

def pinned_requirements(path: Path) -> Iterator[Tuple[str, str]]:
Expand Down
20 changes: 19 additions & 1 deletion src/pip/_internal/resolution/resolvelib/provider.py
Original file line number Diff line number Diff line change
Expand Up @@ -66,12 +66,13 @@ def __init__(
def identify(self, requirement_or_candidate: Union[Requirement, Candidate]) -> str:
return requirement_or_candidate.name

def get_preference(
def get_preference( # type: ignore
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We need to update resolvelib's type annotations, to fix this.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Apologies I missed this. I've never worked on a project before that splits the components in to different repos like this, I should of tried to run the test cases all together before merge in to resolvelib.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No worries. The fact that this was missed is more a symptom of a bad process, and that's not something you need to apologise for. :)

self,
identifier: str,
resolutions: Mapping[str, Candidate],
candidates: Mapping[str, Iterator[Candidate]],
information: Mapping[str, Iterable["PreferenceInformation"]],
backtrack_causes: Sequence["PreferenceInformation"],
) -> "Preference":
"""Produce a sort key for given requirement based on preference.

Expand Down Expand Up @@ -132,11 +133,17 @@ def get_preference(
# while we work on "proper" branch pruning techniques.
delay_this = identifier == "setuptools"

# Prefer the causes of backtracking on the assumption that the problem
# resolving the dependency tree is related to the failures that caused
# the backtracking
backtrack_cause = self.is_backtrack_cause(identifier, backtrack_causes)

return (
not requires_python,
delay_this,
not direct,
not pinned,
not backtrack_cause,
inferred_depth,
requested_order,
not unfree,
Expand Down Expand Up @@ -195,3 +202,14 @@ def is_satisfied_by(self, requirement: Requirement, candidate: Candidate) -> boo
def get_dependencies(self, candidate: Candidate) -> Sequence[Requirement]:
with_requires = not self._ignore_dependencies
return [r for r in candidate.iter_dependencies(with_requires) if r is not None]

@staticmethod
def is_backtrack_cause(
identifier: str, backtrack_causes: Sequence["PreferenceInformation"]
) -> bool:
for backtrack_cause in backtrack_causes:
if identifier == backtrack_cause.requirement.name:
return True
if backtrack_cause.parent and identifier == backtrack_cause.parent.name:
return True
return False
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.7.1"
__version__ = "0.8.0"


from .providers import AbstractProvider, AbstractResolver
Expand Down
11 changes: 10 additions & 1 deletion src/pip/_vendor/resolvelib/providers.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,14 @@ def identify(self, requirement_or_candidate):
"""
raise NotImplementedError

def get_preference(self, identifier, resolutions, candidates, information):
def get_preference(
self,
identifier,
resolutions,
candidates,
information,
backtrack_causes,
):
"""Produce a sort key for given requirement based on preference.

The preference is defined as "I think this requirement should be
Expand All @@ -25,6 +32,8 @@ def get_preference(self, identifier, resolutions, candidates, information):
Each value is an iterator of candidates.
:param information: Mapping of requirement information of each package.
Each value is an iterator of *requirement information*.
:param backtrack_causes: Sequence of requirement information that were
the requirements that caused the resolver to most recently backtrack.

A *requirement information* instance is a named tuple with two members:

Expand Down
18 changes: 14 additions & 4 deletions src/pip/_vendor/resolvelib/resolvers.py
Original file line number Diff line number Diff line change
Expand Up @@ -99,7 +99,7 @@ def __init__(self, round_count):


# Resolution state in a round.
State = collections.namedtuple("State", "mapping criteria")
State = collections.namedtuple("State", "mapping criteria backtrack_causes")


class Resolution(object):
Expand Down Expand Up @@ -131,6 +131,7 @@ def _push_new_state(self):
state = State(
mapping=base.mapping.copy(),
criteria=base.criteria.copy(),
backtrack_causes=base.backtrack_causes[:],
)
self._states.append(state)

Expand Down Expand Up @@ -185,6 +186,7 @@ def _get_preference(self, name):
self.state.criteria,
operator.attrgetter("information"),
),
backtrack_causes=self.state.backtrack_causes,
)

def _is_current_pin_satisfying(self, name, criterion):
Expand Down Expand Up @@ -335,7 +337,13 @@ def resolve(self, requirements, max_rounds):
self._r.starting()

# Initialize the root state.
self._states = [State(mapping=collections.OrderedDict(), criteria={})]
self._states = [
State(
mapping=collections.OrderedDict(),
criteria={},
backtrack_causes=[],
)
]
for r in requirements:
try:
self._add_to_criteria(self.state.criteria, r, parent=None)
Expand Down Expand Up @@ -369,11 +377,13 @@ def resolve(self, requirements, max_rounds):
# 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()
self.state.backtrack_causes[:] = [
i for c in failure_causes for i in c.information
]

# Dead ends everywhere. Give up.
if not success:
causes = [i for c in failure_causes for i in c.information]
raise ResolutionImpossible(causes)
raise ResolutionImpossible(self.state.backtrack_causes)
else:
# Pinning was successful. Push a new state to do another pin.
self._push_new_state()
Expand Down
2 changes: 1 addition & 1 deletion src/pip/_vendor/vendor.txt
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ requests==2.26.0
chardet==4.0.0
idna==3.2
urllib3==1.26.7
resolvelib==0.7.1
resolvelib==0.8.0
setuptools==44.0.0
six==1.16.0
tenacity==8.0.1
Expand Down
4 changes: 3 additions & 1 deletion tests/unit/resolution_resolvelib/test_provider.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ def build_requirement_information(
install_requirement = install_req_from_req_string(name)
# RequirementInformation is typed as a tuple, but it is a namedtupled.
# https://github.com/sarugaku/resolvelib/blob/7bc025aa2a4e979597c438ad7b17d2e8a08a364e/src/resolvelib/resolvers.pyi#L20-L22
requirement_information: PreferenceInformation = RequirementInformation(
requirement_information: "PreferenceInformation" = RequirementInformation(
requirement=SpecifierRequirement(install_requirement), # type: ignore[call-arg]
parent=parent,
)
Expand All @@ -46,6 +46,7 @@ def test_provider_known_depths(factory: Factory) -> None:
resolutions={},
candidates={},
information={root_requirement_name: root_requirement_information},
backtrack_causes=[],
)
assert provider._known_depths == {root_requirement_name: 1.0}

Expand All @@ -69,6 +70,7 @@ def test_provider_known_depths(factory: Factory) -> None:
root_requirement_name: root_requirement_information,
transative_requirement_name: transative_package_information,
},
backtrack_causes=[],
)
assert provider._known_depths == {
transative_requirement_name: 2.0,
Expand Down