Skip to content
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/10732.bugfix.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Optimize performance of conflict cause resolution while the dependency resolver backtracks.
22 changes: 21 additions & 1 deletion src/pip/_internal/resolution/resolvelib/base.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
from typing import FrozenSet, Iterable, Optional, Tuple, Union
from typing import Any, FrozenSet, Iterable, Optional, Sequence, Tuple, Union

from pip._vendor.packaging.specifiers import SpecifierSet
from pip._vendor.packaging.utils import NormalizedName, canonicalize_name
from pip._vendor.packaging.version import LegacyVersion, Version
from pip._vendor.resolvelib.resolvers import RequirementInformation

from pip._internal.models.link import Link, links_equivalent
from pip._internal.req.req_install import InstallRequirement
Expand Down Expand Up @@ -139,3 +140,22 @@ def get_install_requirement(self) -> Optional[InstallRequirement]:

def format_for_error(self) -> str:
raise NotImplementedError("Subclass should override")


class Causes:
@property
def names(self) -> set[str]:
raise NotImplementedError("Override in subclass")

@property
def information(self) -> Sequence[RequirementInformation[Requirement, Candidate]]:
raise NotImplementedError("Override in subclass")

def __copy__(self) -> "Causes":
raise NotImplementedError("Override in subclass")

def __eq__(self, other: Any) -> bool:
raise NotImplementedError("Override in subclass")

def __bool__(self) -> bool:
raise NotImplementedError("Override in subclass")
47 changes: 47 additions & 0 deletions src/pip/_internal/resolution/resolvelib/causes.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
from .base import Causes, Requirement, Candidate
from typing import Generator, List, Sequence, Set, Any
from pip._vendor.resolvelib.resolvers import Criterion
from pip._vendor.resolvelib.resolvers import RequirementInformation

PreferenceInformation = RequirementInformation[Requirement, Candidate]



class BacktrackCauses(Causes):
def __init__(self, causes: List[Any]) -> None:
self.causes = causes
self._names: Set[str] = set()
self._information: Sequence[PreferenceInformation] = []

@property
def names(self) -> Set[str]:
if self._names:
return self._names

self._names = set(self._causes_to_names())
return self._names

@property
def information(self) -> Sequence[PreferenceInformation]:
if self._information:
return self._information

self._information = [i for c in self.causes for i in c.information]
return self._information

def _causes_to_names(self) -> Generator[str, None, None]:
for c in self.information:
yield c.requirement.name
if c.parent:
yield c.parent.name

def __copy__(self) -> "BacktrackCauses":
return BacktrackCauses(causes=self.causes.copy())

def __eq__(self, other: Any) -> bool:
if not isinstance(other, BacktrackCauses):
return NotImplemented
return self.causes == other.causes

def __bool__(self) -> bool:
return bool(self.causes)
13 changes: 7 additions & 6 deletions src/pip/_internal/resolution/resolvelib/factory.py
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,7 @@
RequiresPythonCandidate,
as_base_candidate,
)
from .causes import BacktrackCauses
from .found_candidates import FoundCandidates, IndexCandidateInfo
from .requirements import (
ExplicitRequirement,
Expand Down Expand Up @@ -599,7 +600,7 @@ def _report_single_requirement_conflict(

def get_installation_error(
self,
e: "ResolutionImpossible[Requirement, Candidate]",
e: "ResolutionImpossible[Requirement, BacktrackCauses]",
constraints: Dict[str, Constraint],
) -> InstallationError:

Expand All @@ -609,7 +610,7 @@ def get_installation_error(
# that is what we report.
requires_python_causes = [
cause
for cause in e.causes
for cause in e.causes.information
if isinstance(cause.requirement, RequiresPythonRequirement)
and not cause.requirement.is_satisfied_by(self._python_candidate)
]
Expand All @@ -625,8 +626,8 @@ def get_installation_error(

# The simplest case is when we have *one* cause that can't be
# satisfied. We just report that case.
if len(e.causes) == 1:
req, parent = e.causes[0]
if len(e.causes.information) == 1:
req, parent = e.causes.information[0]
if req.name not in constraints:
return self._report_single_requirement_conflict(req, parent)

Expand All @@ -649,7 +650,7 @@ def describe_trigger(parent: Candidate) -> str:
return str(ireq.comes_from)

triggers = set()
for req, parent in e.causes:
for req, parent in e.causes.information:
if parent is None:
# This is a root requirement, so we can report it directly
trigger = req.format_for_error()
Expand All @@ -670,7 +671,7 @@ def describe_trigger(parent: Candidate) -> str:
msg = "\nThe conflict is caused by:"

relevant_constraints = set()
for req, parent in e.causes:
for req, parent in e.causes.information:
if req.name in constraints:
relevant_constraints.add(req.name)
msg = msg + "\n "
Expand Down
22 changes: 9 additions & 13 deletions src/pip/_internal/resolution/resolvelib/provider.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,11 @@
import math
from typing import (
TYPE_CHECKING,
Any,
Dict,
Iterable,
Iterator,
List,
Mapping,
Sequence,
TypeVar,
Expand All @@ -13,8 +15,9 @@

from pip._vendor.resolvelib.providers import AbstractProvider

from .base import Candidate, Constraint, Requirement
from .base import Candidate, Causes, Constraint, Requirement
from .candidates import REQUIRES_PYTHON_IDENTIFIER
from .causes import BacktrackCauses
from .factory import Factory

if TYPE_CHECKING:
Expand Down Expand Up @@ -104,13 +107,13 @@ def __init__(
def identify(self, requirement_or_candidate: Union[Requirement, Candidate]) -> str:
return requirement_or_candidate.name

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

Expand Down Expand Up @@ -174,7 +177,7 @@ def get_preference( # type: ignore
# 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)
backtrack_cause = identifier in backtrack_causes.names

return (
not requires_python,
Expand Down Expand Up @@ -237,12 +240,5 @@ def get_dependencies(self, candidate: Candidate) -> Sequence[Requirement]:
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
def causes(causes: List[Any]) -> BacktrackCauses:
return BacktrackCauses(causes=causes)
3 changes: 2 additions & 1 deletion src/pip/_internal/resolution/resolvelib/resolver.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
from pip._internal.req.req_install import InstallRequirement
from pip._internal.req.req_set import RequirementSet
from pip._internal.resolution.base import BaseResolver, InstallRequirementProvider
from pip._internal.resolution.resolvelib.causes import BacktrackCauses
from pip._internal.resolution.resolvelib.provider import PipProvider
from pip._internal.resolution.resolvelib.reporter import (
PipDebuggingReporter,
Expand Down Expand Up @@ -95,7 +96,7 @@ def resolve(

except ResolutionImpossible as e:
error = self.factory.get_installation_error(
cast("ResolutionImpossible[Requirement, Candidate]", e),
cast("ResolutionImpossible[Requirement, BacktrackCauses]", e),
collected.constraints,
)
raise error from e
Expand Down
4 changes: 2 additions & 2 deletions tests/unit/resolution_resolvelib/test_provider.py
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@ def test_provider_known_depths(factory: Factory) -> None:
resolutions={},
candidates={},
information={root_requirement_name: root_requirement_information},
backtrack_causes=[],
backtrack_causes=provider.causes(causes=[]),
)
assert provider._known_depths == {root_requirement_name: 1.0}

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