diff --git a/news/12657.feature.rst b/news/12657.feature.rst new file mode 100644 index 00000000000..27e4966b9a3 --- /dev/null +++ b/news/12657.feature.rst @@ -0,0 +1 @@ +Further improve resolution performance of large dependency trees, by caching hash calculations. diff --git a/src/pip/_internal/resolution/resolvelib/candidates.py b/src/pip/_internal/resolution/resolvelib/candidates.py index 9d15b8fda0a..019ff60a0a5 100644 --- a/src/pip/_internal/resolution/resolvelib/candidates.py +++ b/src/pip/_internal/resolution/resolvelib/candidates.py @@ -154,6 +154,7 @@ def __init__( self._name = name self._version = version self.dist = self._prepare() + self._hash: Optional[int] = None def __str__(self) -> str: return f"{self.name} {self.version}" @@ -162,7 +163,11 @@ def __repr__(self) -> str: return f"{self.__class__.__name__}({str(self._link)!r})" def __hash__(self) -> int: - return hash((self.__class__, self._link)) + if self._hash is not None: + return self._hash + + self._hash = hash((self.__class__, self._link)) + return self._hash def __eq__(self, other: Any) -> bool: if isinstance(other, self.__class__): diff --git a/src/pip/_internal/resolution/resolvelib/requirements.py b/src/pip/_internal/resolution/resolvelib/requirements.py index f980a356f18..b04f41b2191 100644 --- a/src/pip/_internal/resolution/resolvelib/requirements.py +++ b/src/pip/_internal/resolution/resolvelib/requirements.py @@ -1,4 +1,4 @@ -from typing import Any +from typing import Any, Optional from pip._vendor.packaging.specifiers import SpecifierSet from pip._vendor.packaging.utils import NormalizedName, canonicalize_name @@ -51,8 +51,18 @@ class SpecifierRequirement(Requirement): def __init__(self, ireq: InstallRequirement) -> None: assert ireq.link is None, "This is a link, not a specifier" self._ireq = ireq + self._equal_cache: Optional[str] = None + self._hash: Optional[int] = None self._extras = frozenset(canonicalize_name(e) for e in self._ireq.extras) + @property + def _equal(self) -> str: + if self._equal_cache is not None: + return self._equal_cache + + self._equal_cache = str(self._ireq) + return self._equal_cache + def __str__(self) -> str: return str(self._ireq.req) @@ -62,10 +72,14 @@ def __repr__(self) -> str: def __eq__(self, other: object) -> bool: if not isinstance(other, SpecifierRequirement): return NotImplemented - return str(self._ireq) == str(other._ireq) + return self._equal == other._equal def __hash__(self) -> int: - return hash(str(self._ireq)) + if self._hash is not None: + return self._hash + + self._hash = hash(self._equal) + return self._hash @property def project_name(self) -> NormalizedName: @@ -114,15 +128,29 @@ class SpecifierWithoutExtrasRequirement(SpecifierRequirement): def __init__(self, ireq: InstallRequirement) -> None: assert ireq.link is None, "This is a link, not a specifier" self._ireq = install_req_drop_extras(ireq) + self._equal_cache: Optional[str] = None + self._hash: Optional[int] = None self._extras = frozenset(canonicalize_name(e) for e in self._ireq.extras) + @property + def _equal(self) -> str: + if self._equal_cache is not None: + return self._equal_cache + + self._equal_cache = str(self._ireq) + return self._equal_cache + def __eq__(self, other: object) -> bool: if not isinstance(other, SpecifierWithoutExtrasRequirement): return NotImplemented - return str(self._ireq) == str(other._ireq) + return self._equal == other._equal def __hash__(self) -> int: - return hash(str(self._ireq)) + if self._hash is not None: + return self._hash + + self._hash = hash(self._equal) + return self._hash class RequiresPythonRequirement(Requirement): @@ -131,6 +159,7 @@ class RequiresPythonRequirement(Requirement): def __init__(self, specifier: SpecifierSet, match: Candidate) -> None: self.specifier = specifier self._specifier_string = str(specifier) # for faster __eq__ + self._hash: Optional[int] = None self._candidate = match def __str__(self) -> str: @@ -140,7 +169,11 @@ def __repr__(self) -> str: return f"{self.__class__.__name__}({str(self.specifier)!r})" def __hash__(self) -> int: - return hash((self._specifier_string, self._candidate)) + if self._hash is not None: + return self._hash + + self._hash = hash((self._specifier_string, self._candidate)) + return self._hash def __eq__(self, other: Any) -> bool: if not isinstance(other, RequiresPythonRequirement):