Skip to content

Commit 24f4600

Browse files
authored
Remove LRU cache from methods [ruff rule cached-instance-method] (#13306)
Enables ruff rule cached-instance-method (B019). By caching instance methods, the instances are kept alive until they are evicted out of the LRU cache. If the cached instance is itself is referencing other memory-heavy objects, this can lead to hard-to-debug high memory usage. Most of the objects fixed here are fairly long lived, but we don't necessarily want them to be kept alive until the very end of the process. Hypothetically, if the build isolation became in-process there will be multiple instances of these classes that should only live for the lifetime of any particular build isolation.
1 parent d852ebd commit 24f4600

File tree

5 files changed

+54
-14
lines changed

5 files changed

+54
-14
lines changed

pyproject.toml

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -166,7 +166,6 @@ extend-exclude = [
166166

167167
[tool.ruff.lint]
168168
ignore = [
169-
"B019",
170169
"B020",
171170
"B904", # Ruff enables opinionated warnings by default
172171
"B905", # Ruff enables opinionated warnings by default

src/pip/_internal/index/package_finder.py

Lines changed: 36 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,17 @@
66
import logging
77
import re
88
from dataclasses import dataclass
9-
from typing import TYPE_CHECKING, FrozenSet, Iterable, List, Optional, Set, Tuple, Union
9+
from typing import (
10+
TYPE_CHECKING,
11+
Dict,
12+
FrozenSet,
13+
Iterable,
14+
List,
15+
Optional,
16+
Set,
17+
Tuple,
18+
Union,
19+
)
1020

1121
from pip._vendor.packaging import specifiers
1222
from pip._vendor.packaging.tags import Tag
@@ -601,6 +611,13 @@ def __init__(
601611
# These are boring links that have already been logged somehow.
602612
self._logged_links: Set[Tuple[Link, LinkType, str]] = set()
603613

614+
# Cache of the result of finding candidates
615+
self._all_candidates: Dict[str, List[InstallationCandidate]] = {}
616+
self._best_candidates: Dict[
617+
Tuple[str, Optional[specifiers.BaseSpecifier], Optional[Hashes]],
618+
BestCandidateResult,
619+
] = {}
620+
604621
# Don't include an allow_yanked default value to make sure each call
605622
# site considers whether yanked releases are allowed. This also causes
606623
# that decision to be made explicit in the calling code, which helps
@@ -795,7 +812,6 @@ def process_project_url(
795812

796813
return package_links
797814

798-
@functools.lru_cache(maxsize=None)
799815
def find_all_candidates(self, project_name: str) -> List[InstallationCandidate]:
800816
"""Find all available InstallationCandidate for project_name
801817
@@ -805,6 +821,9 @@ def find_all_candidates(self, project_name: str) -> List[InstallationCandidate]:
805821
See LinkEvaluator.evaluate_link() for details on which files
806822
are accepted.
807823
"""
824+
if project_name in self._all_candidates:
825+
return self._all_candidates[project_name]
826+
808827
link_evaluator = self.make_link_evaluator(project_name)
809828

810829
collected_sources = self._link_collector.collect_sources(
@@ -846,7 +865,9 @@ def find_all_candidates(self, project_name: str) -> List[InstallationCandidate]:
846865
logger.debug("Local files found: %s", ", ".join(paths))
847866

848867
# This is an intentional priority ordering
849-
return file_candidates + page_candidates
868+
self._all_candidates[project_name] = file_candidates + page_candidates
869+
870+
return self._all_candidates[project_name]
850871

851872
def make_candidate_evaluator(
852873
self,
@@ -865,7 +886,6 @@ def make_candidate_evaluator(
865886
hashes=hashes,
866887
)
867888

868-
@functools.lru_cache(maxsize=None)
869889
def find_best_candidate(
870890
self,
871891
project_name: str,
@@ -880,13 +900,20 @@ def find_best_candidate(
880900
881901
:return: A `BestCandidateResult` instance.
882902
"""
903+
if (project_name, specifier, hashes) in self._best_candidates:
904+
return self._best_candidates[project_name, specifier, hashes]
905+
883906
candidates = self.find_all_candidates(project_name)
884907
candidate_evaluator = self.make_candidate_evaluator(
885908
project_name=project_name,
886909
specifier=specifier,
887910
hashes=hashes,
888911
)
889-
return candidate_evaluator.compute_best_candidate(candidates)
912+
self._best_candidates[project_name, specifier, hashes] = (
913+
candidate_evaluator.compute_best_candidate(candidates)
914+
)
915+
916+
return self._best_candidates[project_name, specifier, hashes]
890917

891918
def find_requirement(
892919
self, req: InstallRequirement, upgrade: bool
@@ -897,9 +924,12 @@ def find_requirement(
897924
Returns a InstallationCandidate if found,
898925
Raises DistributionNotFound or BestVersionAlreadyInstalled otherwise
899926
"""
927+
name = req.name
928+
assert name is not None, "find_requirement() called with no name"
929+
900930
hashes = req.hashes(trust_internet=False)
901931
best_candidate_result = self.find_best_candidate(
902-
req.name,
932+
name,
903933
specifier=req.specifier,
904934
hashes=hashes,
905935
)

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

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,6 @@
88
something.
99
"""
1010

11-
import functools
1211
import logging
1312
from collections.abc import Sequence
1413
from typing import Any, Callable, Iterator, Optional, Set, Tuple
@@ -129,6 +128,7 @@ def __init__(
129128
self._installed = installed
130129
self._prefers_installed = prefers_installed
131130
self._incompatible_ids = incompatible_ids
131+
self._bool: Optional[bool] = None
132132

133133
def __getitem__(self, index: Any) -> Any:
134134
# Implemented to satisfy the ABC check. This is not needed by the
@@ -152,8 +152,13 @@ def __len__(self) -> int:
152152
# performance reasons).
153153
raise NotImplementedError("don't do this")
154154

155-
@functools.lru_cache(maxsize=1)
156155
def __bool__(self) -> bool:
156+
if self._bool is not None:
157+
return self._bool
158+
157159
if self._prefers_installed and self._installed:
160+
self._bool = True
158161
return True
159-
return any(self)
162+
163+
self._bool = any(self)
164+
return self._bool

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

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -270,8 +270,9 @@ def _eligible_for_upgrade(identifier: str) -> bool:
270270
is_satisfied_by=self.is_satisfied_by,
271271
)
272272

273+
@staticmethod
273274
@lru_cache(maxsize=None)
274-
def is_satisfied_by(self, requirement: Requirement, candidate: Candidate) -> bool:
275+
def is_satisfied_by(requirement: Requirement, candidate: Candidate) -> bool:
275276
return requirement.is_satisfied_by(candidate)
276277

277278
def get_dependencies(self, candidate: Candidate) -> Iterable[Requirement]:

tests/unit/test_finder.py

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -319,7 +319,10 @@ def test_finder_priority_file_over_page(data: TestData) -> None:
319319
find_links=[data.find_links],
320320
index_urls=["http://pypi.org/simple/"],
321321
)
322-
all_versions = finder.find_all_candidates(req.name)
322+
name = req.name
323+
assert name == "gmpy"
324+
325+
all_versions = finder.find_all_candidates(name)
323326
# 1 file InstallationCandidate followed by all https ones
324327
assert all_versions[0].link.scheme == "file"
325328
assert all(
@@ -335,9 +338,11 @@ def test_finder_priority_nonegg_over_eggfragments() -> None:
335338
"""Test PackageFinder prefers non-egg links over "#egg=" links"""
336339
req = install_req_from_line("bar==1.0")
337340
links = ["http://foo/bar.py#egg=bar-1.0", "http://foo/bar-1.0.tar.gz"]
341+
name = req.name
342+
assert name == "bar"
338343

339344
finder = make_test_finder(links)
340-
all_versions = finder.find_all_candidates(req.name)
345+
all_versions = finder.find_all_candidates(name)
341346
assert all_versions[0].link.url.endswith("tar.gz")
342347
assert all_versions[1].link.url.endswith("#egg=bar-1.0")
343348

@@ -349,7 +354,7 @@ def test_finder_priority_nonegg_over_eggfragments() -> None:
349354
links.reverse()
350355

351356
finder = make_test_finder(links)
352-
all_versions = finder.find_all_candidates(req.name)
357+
all_versions = finder.find_all_candidates(name)
353358
assert all_versions[0].link.url.endswith("tar.gz")
354359
assert all_versions[1].link.url.endswith("#egg=bar-1.0")
355360
found = finder.find_requirement(req, False)

0 commit comments

Comments
 (0)