From f1c1d9778d43545fdf89bac4723b6592ef0eddc2 Mon Sep 17 00:00:00 2001
From: Damian Shaw <damian.peter.shaw@gmail.com>
Date: Sat, 27 Apr 2024 23:02:01 -0400
Subject: [PATCH 1/2] Cache hashes for specifier requirements and
 _InstallRequirementBackedCandidate

---
 .../resolution/resolvelib/candidates.py       |  7 ++-
 .../resolution/resolvelib/requirements.py     | 45 ++++++++++++++++---
 2 files changed, 45 insertions(+), 7 deletions(-)

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):

From 1e510e3bb126f22550bb495361ec7e7f58e36f9d Mon Sep 17 00:00:00 2001
From: Damian Shaw <damian.peter.shaw@gmail.com>
Date: Sat, 27 Apr 2024 23:13:08 -0400
Subject: [PATCH 2/2] NEWS ENTRY

---
 news/12657.feature.rst | 1 +
 1 file changed, 1 insertion(+)
 create mode 100644 news/12657.feature.rst

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.