diff --git a/news/10732.bugfix.rst b/news/10732.bugfix.rst new file mode 100644 index 00000000000..085ecda640a --- /dev/null +++ b/news/10732.bugfix.rst @@ -0,0 +1 @@ +Optimize performance of conflict cause resolution while the dependency resolver backtracks. \ No newline at end of file diff --git a/src/pip/_internal/resolution/resolvelib/base.py b/src/pip/_internal/resolution/resolvelib/base.py index b206692a0a9..0c4bf8461e2 100644 --- a/src/pip/_internal/resolution/resolvelib/base.py +++ b/src/pip/_internal/resolution/resolvelib/base.py @@ -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 @@ -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") diff --git a/src/pip/_internal/resolution/resolvelib/causes.py b/src/pip/_internal/resolution/resolvelib/causes.py new file mode 100644 index 00000000000..acc5f23c6aa --- /dev/null +++ b/src/pip/_internal/resolution/resolvelib/causes.py @@ -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) diff --git a/src/pip/_internal/resolution/resolvelib/factory.py b/src/pip/_internal/resolution/resolvelib/factory.py index c3aaa6957f5..147dec90def 100644 --- a/src/pip/_internal/resolution/resolvelib/factory.py +++ b/src/pip/_internal/resolution/resolvelib/factory.py @@ -58,6 +58,7 @@ RequiresPythonCandidate, as_base_candidate, ) +from .causes import BacktrackCauses from .found_candidates import FoundCandidates, IndexCandidateInfo from .requirements import ( ExplicitRequirement, @@ -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: @@ -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) ] @@ -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) @@ -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() @@ -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 " diff --git a/src/pip/_internal/resolution/resolvelib/provider.py b/src/pip/_internal/resolution/resolvelib/provider.py index e6ec9594f62..f81b6159f8c 100644 --- a/src/pip/_internal/resolution/resolvelib/provider.py +++ b/src/pip/_internal/resolution/resolvelib/provider.py @@ -2,9 +2,11 @@ import math from typing import ( TYPE_CHECKING, + Any, Dict, Iterable, Iterator, + List, Mapping, Sequence, TypeVar, @@ -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: @@ -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. @@ -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, @@ -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) diff --git a/src/pip/_internal/resolution/resolvelib/resolver.py b/src/pip/_internal/resolution/resolvelib/resolver.py index 8ee36d377d8..436b2fc134d 100644 --- a/src/pip/_internal/resolution/resolvelib/resolver.py +++ b/src/pip/_internal/resolution/resolvelib/resolver.py @@ -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, @@ -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 diff --git a/tests/unit/resolution_resolvelib/test_provider.py b/tests/unit/resolution_resolvelib/test_provider.py index a62808741d7..d0e20ac01cf 100644 --- a/tests/unit/resolution_resolvelib/test_provider.py +++ b/tests/unit/resolution_resolvelib/test_provider.py @@ -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} @@ -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,