Skip to content

Commit

Permalink
Prefer conflicting causes
Browse files Browse the repository at this point in the history
  • Loading branch information
notatallshaw committed Jan 5, 2024
1 parent 954075f commit 5d2be0e
Showing 1 changed file with 126 additions and 1 deletion.
127 changes: 126 additions & 1 deletion src/pip/_internal/resolution/resolvelib/provider.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
from .factory import Factory

if TYPE_CHECKING:
from pip._vendor.packaging.specifiers import SpecifierSet
from pip._vendor.resolvelib.providers import Preference
from pip._vendor.resolvelib.resolvers import RequirementInformation

Expand Down Expand Up @@ -75,6 +76,117 @@ def _get_with_identifier(
return default


def causes_with_conflicting_parent(
causes: Sequence["PreferenceInformation"],
) -> Sequence["PreferenceInformation"]:
"""Given causes return which causes conflict because their parent
is not satisfied by another cause, or another causes's parent is
not satisfied by them
"""
# To avoid duplication keeps track of already found conflicting cause by it's id
conflicting_causes_by_id: dict[int, "PreferenceInformation"] = {}
all_causes_by_id = {id(c): c for c in causes}

# Build a relationship between causes, cause ids, and cause parent names
causes_ids_and_parents_by_parent_name: dict[
str, list[tuple[int, Candidate]]
] = collections.defaultdict(list)
for cause_id, cause in all_causes_by_id.items():
if cause.parent:
causes_ids_and_parents_by_parent_name[cause.parent.name].append(
(cause_id, cause.parent)
)

# Check each cause and see if conflicts with the parent of another cause
for cause_id, cause in all_causes_by_id.items():
if cause_id in conflicting_causes_by_id:
continue

cause_id_and_parents = causes_ids_and_parents_by_parent_name.get(
cause.requirement.name
)
if not cause_id_and_parents:
continue

for other_cause_id, parent in cause_id_and_parents:
if not cause.requirement.is_satisfied_by(parent):
conflicting_causes_by_id[cause_id] = cause
conflicting_causes_by_id[other_cause_id] = all_causes_by_id[
other_cause_id
]

return list(conflicting_causes_by_id.values())


def _check_conflict_between_specifier_sets(
specifier_set1: "SpecifierSet", specifier_set2: "SpecifierSet"
) -> bool:
for specifier1 in specifier_set1:
if specifier_set2.contains(specifier1.version):
return True
return False


def _is_mutually_exclusive_specifier_sets(
specifier_set1: "SpecifierSet", specifier_set2: "SpecifierSet"
) -> bool:
return not (
_check_conflict_between_specifier_sets(specifier_set1, specifier_set2)
or _check_conflict_between_specifier_sets(specifier_set2, specifier_set1)
)


def causes_with_conflicting_specifiers(
causes: Sequence["PreferenceInformation"],
) -> Sequence["PreferenceInformation"]:
"""Given causes return which causes conflict because theierspecifier
conflict with another specifier"""
# Group causes by name first to avoid large O(n^2) comparison
causes_by_name: dict[str, list["PreferenceInformation"]] = collections.defaultdict(
list
)
for cause in causes:
causes_by_name[cause.requirement.project_name].append(cause)

# Check each cause that has the same name, and check if their specifiers conflict
conflicting_causes: list["PreferenceInformation"] = []
for causes_list in causes_by_name.values():
if len(causes_list) < 2:
continue

while causes_list:
cause = causes_list.pop()
candidate = cause.requirement.get_candidate_lookup()[1]
if candidate is None:
continue

# If either specifier set provies no restrictions they can be skipped
if len(candidate.specifier) == 0:
continue

cause_had_conflict = False
for i, other_cause in enumerate(causes_list):
other_candidate = other_cause.requirement.get_candidate_lookup()[1]
if other_candidate is None:
continue

if len(other_candidate.specifier) == 0:
continue

# If specifier set are mutually exclusive they can not both
# be fufilled and therefore are a conflict
if _is_mutually_exclusive_specifier_sets(
candidate.specifier, other_candidate.specifier
):
conflicting_causes.append(causes_list.pop(i))
cause_had_conflict = True

if cause_had_conflict:
conflicting_causes.append(cause)

return conflicting_causes


class PipProvider(_ProviderBase):
"""Pip's provider implementation for resolvelib.
Expand Down Expand Up @@ -243,11 +355,24 @@ def filter_unsatisfied_names(
causes: Sequence["PreferenceInformation"],
) -> Iterable[str]:
"""
Prefer backtracking on unsatisfied names that are causes
Prefer backtracking on unsatisfied names that are conficting
causes, or secondly are causes
"""
if not causes:
return unsatisfied_names

# Check if causes are conflicting, prefer cause specifier
# conflict first, and a cause parent conflicts second
if len(causes) > 2:
_conflicting_causes = causes_with_conflicting_specifiers(causes)
if _conflicting_causes:
causes = _conflicting_causes
else:
_conflicting_causes = causes_with_conflicting_parent(causes)
if _conflicting_causes:
causes = _conflicting_causes
del _conflicting_causes

# Extract the causes and parents names
causes_names = set()
for cause in causes:
Expand Down

0 comments on commit 5d2be0e

Please sign in to comment.