diff --git a/src/pip/_internal/resolution/resolvelib/provider.py b/src/pip/_internal/resolution/resolvelib/provider.py index b554ce83862..ec611f0855b 100644 --- a/src/pip/_internal/resolution/resolvelib/provider.py +++ b/src/pip/_internal/resolution/resolvelib/provider.py @@ -75,6 +75,100 @@ def _get_with_identifier( return default +def conflicting_causes( + causes: Sequence["PreferenceInformation"], +) -> Sequence["PreferenceInformation"]: + """Given causes return which causes conflict with each other + For each cause check one of two things: + 1. If it's specifier conflicts with another causes parent version + 2. If it's specifier conflicts with another causes specifier + + Any causes which match this criteria are returned as conflicting causes + """ + conflicting_ids: set[int] = set() + + # Build a relationship between causes, cause ids, and cause parent names + causes_id_and_parents_by_name: dict[ + str, list[tuple[int, Candidate]] + ] = collections.defaultdict(list) + causes_by_id = {id(c): c for c in causes} + for cause_id, cause in causes_by_id.items(): + if cause.parent: + causes_id_and_parents_by_name[cause.parent.name].append( + (cause_id, cause.parent) + ) + + # From 1, check if each cause's specifier conflicts + # with another causes parent's version + for cause_id, cause in causes_by_id.items(): + if cause_id in conflicting_ids: + continue + + cause_id_and_parents = causes_id_and_parents_by_name.get(cause.requirement.name) + if not cause_id_and_parents: + continue + + conflicting_alternative_cause_ids: set[int] = set() + for alternative_cause_id, parent in cause_id_and_parents: + if not cause.requirement.is_satisfied_by(parent): + conflicting_alternative_cause_ids.add(alternative_cause_id) + + if conflicting_alternative_cause_ids: + conflicting_ids.add(cause_id) + conflicting_ids.update(conflicting_alternative_cause_ids) + + # For comparing if two specifiers conflict first group causes + # by name, as comparing specifiers is O(n^2) so comparing the + # smaller groups is more efficent + causes_by_name: dict[str, list["PreferenceInformation"]] = collections.defaultdict( + list + ) + for cause in causes: + causes_by_name[cause.requirement.name].append(cause) + + # From 2, check if each cause's specifier conflicts + # with another cause specifier + for causes_list in causes_by_name.values(): + if len(causes_list) < 2: + continue + + while causes_list: + cause = causes_list.pop() + for i, alternative_cause in enumerate(causes_list): + candidate = cause.requirement.get_candidate_lookup()[1] + if candidate is None: + continue + specifier = candidate.specifier + + # Specifiers which provide no restrictions can be skipped + if len(specifier) == 0: + continue + + alternative_candidate = ( + alternative_cause.requirement.get_candidate_lookup()[1] + ) + if alternative_candidate is None: + continue + + alternative_specifier = alternative_candidate.specifier + + # Alternative specifiers which provide no + # restrictions can be skipped + if len(alternative_specifier) == 0: + continue + + # If intersection of specifiers are empty they are + # impossibe to fill and therefore conflicting + specifier_intersection = specifier and alternative_specifier + if len(specifier_intersection) == 0: + conflicting_ids.add(id(cause)) + conflicting_ids.add(id(causes_list.pop(i))) + + return [ + cause for cause_id, cause in causes_by_id.items() if cause_id in conflicting_ids + ] + + class PipProvider(_ProviderBase): """Pip's provider implementation for resolvelib. @@ -243,11 +337,18 @@ 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 backtrack causes are conflicting and prefer them + if len(causes) > 2: + _conflicting_causes = conflicting_causes(causes) + if len(_conflicting_causes) > 1: + causes = _conflicting_causes + # Extract the causes and parents names causes_names = set() for cause in causes: