Skip to content

Commit 6522547

Browse files
authored
Merge pull request #12453 from sbidoul/cache-is_satified_by-sbi
2 parents e812942 + 38b5645 commit 6522547

File tree

6 files changed

+69
-7
lines changed

6 files changed

+69
-7
lines changed

news/12453.feature.rst

+1
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Improve performance of resolution of large dependency trees, with more caching.

src/pip/_internal/resolution/resolvelib/candidates.py

+6-6
Original file line numberDiff line numberDiff line change
@@ -352,13 +352,13 @@ def __str__(self) -> str:
352352
def __repr__(self) -> str:
353353
return f"{self.__class__.__name__}({self.dist!r})"
354354

355-
def __hash__(self) -> int:
356-
return hash((self.__class__, self.name, self.version))
355+
def __eq__(self, other: object) -> bool:
356+
if not isinstance(other, AlreadyInstalledCandidate):
357+
return NotImplemented
358+
return self.name == other.name and self.version == other.version
357359

358-
def __eq__(self, other: Any) -> bool:
359-
if isinstance(other, self.__class__):
360-
return self.name == other.name and self.version == other.version
361-
return False
360+
def __hash__(self) -> int:
361+
return hash((self.name, self.version))
362362

363363
@property
364364
def project_name(self) -> NormalizedName:

src/pip/_internal/resolution/resolvelib/factory.py

+3-1
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
import logging
44
from typing import (
55
TYPE_CHECKING,
6+
Callable,
67
Dict,
78
FrozenSet,
89
Iterable,
@@ -391,6 +392,7 @@ def find_candidates(
391392
incompatibilities: Mapping[str, Iterator[Candidate]],
392393
constraint: Constraint,
393394
prefers_installed: bool,
395+
is_satisfied_by: Callable[[Requirement, Candidate], bool],
394396
) -> Iterable[Candidate]:
395397
# Collect basic lookup information from the requirements.
396398
explicit_candidates: Set[Candidate] = set()
@@ -456,7 +458,7 @@ def find_candidates(
456458
for c in explicit_candidates
457459
if id(c) not in incompat_ids
458460
and constraint.is_satisfied_by(c)
459-
and all(req.is_satisfied_by(c) for req in requirements[identifier])
461+
and all(is_satisfied_by(req, c) for req in requirements[identifier])
460462
)
461463

462464
def _make_requirements_from_install_req(

src/pip/_internal/resolution/resolvelib/provider.py

+3
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import collections
22
import math
3+
from functools import lru_cache
34
from typing import (
45
TYPE_CHECKING,
56
Dict,
@@ -234,8 +235,10 @@ def _eligible_for_upgrade(identifier: str) -> bool:
234235
constraint=constraint,
235236
prefers_installed=(not _eligible_for_upgrade(identifier)),
236237
incompatibilities=incompatibilities,
238+
is_satisfied_by=self.is_satisfied_by,
237239
)
238240

241+
@lru_cache(maxsize=None)
239242
def is_satisfied_by(self, requirement: Requirement, candidate: Candidate) -> bool:
240243
return requirement.is_satisfied_by(candidate)
241244

src/pip/_internal/resolution/resolvelib/requirements.py

+46
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
from typing import Any
2+
13
from pip._vendor.packaging.specifiers import SpecifierSet
24
from pip._vendor.packaging.utils import NormalizedName, canonicalize_name
35

@@ -17,6 +19,14 @@ def __str__(self) -> str:
1719
def __repr__(self) -> str:
1820
return f"{self.__class__.__name__}({self.candidate!r})"
1921

22+
def __hash__(self) -> int:
23+
return hash(self.candidate)
24+
25+
def __eq__(self, other: Any) -> bool:
26+
if not isinstance(other, ExplicitRequirement):
27+
return False
28+
return self.candidate == other.candidate
29+
2030
@property
2131
def project_name(self) -> NormalizedName:
2232
# No need to canonicalize - the candidate did this
@@ -49,6 +59,14 @@ def __str__(self) -> str:
4959
def __repr__(self) -> str:
5060
return f"{self.__class__.__name__}({str(self._ireq.req)!r})"
5161

62+
def __eq__(self, other: object) -> bool:
63+
if not isinstance(other, SpecifierRequirement):
64+
return NotImplemented
65+
return str(self._ireq) == str(other._ireq)
66+
67+
def __hash__(self) -> int:
68+
return hash(str(self._ireq))
69+
5270
@property
5371
def project_name(self) -> NormalizedName:
5472
assert self._ireq.req, "Specifier-backed ireq is always PEP 508"
@@ -98,12 +116,21 @@ def __init__(self, ireq: InstallRequirement) -> None:
98116
self._ireq = install_req_drop_extras(ireq)
99117
self._extras = frozenset(canonicalize_name(e) for e in self._ireq.extras)
100118

119+
def __eq__(self, other: object) -> bool:
120+
if not isinstance(other, SpecifierWithoutExtrasRequirement):
121+
return NotImplemented
122+
return str(self._ireq) == str(other._ireq)
123+
124+
def __hash__(self) -> int:
125+
return hash(str(self._ireq))
126+
101127

102128
class RequiresPythonRequirement(Requirement):
103129
"""A requirement representing Requires-Python metadata."""
104130

105131
def __init__(self, specifier: SpecifierSet, match: Candidate) -> None:
106132
self.specifier = specifier
133+
self._specifier_string = str(specifier) # for faster __eq__
107134
self._candidate = match
108135

109136
def __str__(self) -> str:
@@ -112,6 +139,17 @@ def __str__(self) -> str:
112139
def __repr__(self) -> str:
113140
return f"{self.__class__.__name__}({str(self.specifier)!r})"
114141

142+
def __hash__(self) -> int:
143+
return hash((self._specifier_string, self._candidate))
144+
145+
def __eq__(self, other: Any) -> bool:
146+
if not isinstance(other, RequiresPythonRequirement):
147+
return False
148+
return (
149+
self._specifier_string == other._specifier_string
150+
and self._candidate == other._candidate
151+
)
152+
115153
@property
116154
def project_name(self) -> NormalizedName:
117155
return self._candidate.project_name
@@ -148,6 +186,14 @@ def __str__(self) -> str:
148186
def __repr__(self) -> str:
149187
return f"{self.__class__.__name__}({str(self._name)!r})"
150188

189+
def __eq__(self, other: object) -> bool:
190+
if not isinstance(other, UnsatisfiableRequirement):
191+
return NotImplemented
192+
return self._name == other._name
193+
194+
def __hash__(self) -> int:
195+
return hash(self._name)
196+
151197
@property
152198
def project_name(self) -> NormalizedName:
153199
return self._name

tests/unit/resolution_resolvelib/test_requirement.py

+10
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,14 @@
2323
# Editables
2424

2525

26+
def _is_satisfied_by(requirement: Requirement, candidate: Candidate) -> bool:
27+
"""A helper function to check if a requirement is satisfied by a candidate.
28+
29+
Used for mocking PipProvider.is_satified_by.
30+
"""
31+
return requirement.is_satisfied_by(candidate)
32+
33+
2634
@pytest.fixture
2735
def test_cases(data: TestData) -> Iterator[List[Tuple[str, str, int]]]:
2836
def _data_file(name: str) -> Path:
@@ -80,6 +88,7 @@ def test_new_resolver_correct_number_of_matches(
8088
{},
8189
Constraint.empty(),
8290
prefers_installed=False,
91+
is_satisfied_by=_is_satisfied_by,
8392
)
8493
assert sum(1 for _ in matches) == match_count
8594

@@ -98,6 +107,7 @@ def test_new_resolver_candidates_match_requirement(
98107
{},
99108
Constraint.empty(),
100109
prefers_installed=False,
110+
is_satisfied_by=_is_satisfied_by,
101111
)
102112
for c in candidates:
103113
assert isinstance(c, Candidate)

0 commit comments

Comments
 (0)