Skip to content

Commit 8dbbb2e

Browse files
authored
Convert more record classes to dataclasses (#12659)
- Removes BestCandidateResult's iter_all() and iter_applicable() methods as they were redundant - Removes ParsedLine's is_requirement attribute as it was awkward to use (to please mypy, you would need to add asserts on .requirement) - Removes ParsedRequirement's defaults as they conflict with slots (Python 3.10 dataclasses have a built-in workaround that we can't use yet...)
1 parent 07c7a14 commit 8dbbb2e

File tree

6 files changed

+75
-91
lines changed

6 files changed

+75
-91
lines changed

news/c33bb4df-d6ab-4d9b-8113-55c27a237dfd.trivial.rst

Whitespace-only changes.

src/pip/_internal/index/package_finder.py

Lines changed: 18 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -334,44 +334,30 @@ class CandidatePreferences:
334334
allow_all_prereleases: bool = False
335335

336336

337+
@dataclass(frozen=True)
337338
class BestCandidateResult:
338339
"""A collection of candidates, returned by `PackageFinder.find_best_candidate`.
339340
340341
This class is only intended to be instantiated by CandidateEvaluator's
341342
`compute_best_candidate()` method.
342-
"""
343-
344-
def __init__(
345-
self,
346-
candidates: List[InstallationCandidate],
347-
applicable_candidates: List[InstallationCandidate],
348-
best_candidate: Optional[InstallationCandidate],
349-
) -> None:
350-
"""
351-
:param candidates: A sequence of all available candidates found.
352-
:param applicable_candidates: The applicable candidates.
353-
:param best_candidate: The most preferred candidate found, or None
354-
if no applicable candidates were found.
355-
"""
356-
assert set(applicable_candidates) <= set(candidates)
357-
358-
if best_candidate is None:
359-
assert not applicable_candidates
360-
else:
361-
assert best_candidate in applicable_candidates
362343
363-
self._applicable_candidates = applicable_candidates
364-
self._candidates = candidates
344+
:param all_candidates: A sequence of all available candidates found.
345+
:param applicable_candidates: The applicable candidates.
346+
:param best_candidate: The most preferred candidate found, or None
347+
if no applicable candidates were found.
348+
"""
365349

366-
self.best_candidate = best_candidate
350+
all_candidates: List[InstallationCandidate]
351+
applicable_candidates: List[InstallationCandidate]
352+
best_candidate: Optional[InstallationCandidate]
367353

368-
def iter_all(self) -> Iterable[InstallationCandidate]:
369-
"""Iterate through all candidates."""
370-
return iter(self._candidates)
354+
def __post_init__(self) -> None:
355+
assert set(self.applicable_candidates) <= set(self.all_candidates)
371356

372-
def iter_applicable(self) -> Iterable[InstallationCandidate]:
373-
"""Iterate through the applicable candidates."""
374-
return iter(self._applicable_candidates)
357+
if self.best_candidate is None:
358+
assert not self.applicable_candidates
359+
else:
360+
assert self.best_candidate in self.applicable_candidates
375361

376362

377363
class CandidateEvaluator:
@@ -929,7 +915,7 @@ def _format_versions(cand_iter: Iterable[InstallationCandidate]) -> str:
929915
"Could not find a version that satisfies the requirement %s "
930916
"(from versions: %s)",
931917
req,
932-
_format_versions(best_candidate_result.iter_all()),
918+
_format_versions(best_candidate_result.all_candidates),
933919
)
934920

935921
raise DistributionNotFound(f"No matching distribution found for {req}")
@@ -963,15 +949,15 @@ def _should_install_candidate(
963949
logger.debug(
964950
"Using version %s (newest of versions: %s)",
965951
best_candidate.version,
966-
_format_versions(best_candidate_result.iter_applicable()),
952+
_format_versions(best_candidate_result.applicable_candidates),
967953
)
968954
return best_candidate
969955

970956
# We have an existing version, and its the best version
971957
logger.debug(
972958
"Installed version (%s) is most up-to-date (past versions: %s)",
973959
installed_version,
974-
_format_versions(best_candidate_result.iter_applicable()),
960+
_format_versions(best_candidate_result.applicable_candidates),
975961
)
976962
raise BestVersionAlreadyInstalled
977963

src/pip/_internal/operations/freeze.py

Lines changed: 11 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,10 @@
11
import collections
22
import logging
33
import os
4+
from dataclasses import dataclass, field
45
from typing import Container, Dict, Generator, Iterable, List, NamedTuple, Optional, Set
56

6-
from pip._vendor.packaging.utils import canonicalize_name
7+
from pip._vendor.packaging.utils import NormalizedName, canonicalize_name
78
from pip._vendor.packaging.version import InvalidVersion
89

910
from pip._internal.exceptions import BadCommand, InstallationError
@@ -220,19 +221,16 @@ def _get_editable_info(dist: BaseDistribution) -> _EditableInfo:
220221
)
221222

222223

224+
@dataclass(frozen=True)
223225
class FrozenRequirement:
224-
def __init__(
225-
self,
226-
name: str,
227-
req: str,
228-
editable: bool,
229-
comments: Iterable[str] = (),
230-
) -> None:
231-
self.name = name
232-
self.canonical_name = canonicalize_name(name)
233-
self.req = req
234-
self.editable = editable
235-
self.comments = comments
226+
name: str
227+
req: str
228+
editable: bool
229+
comments: Iterable[str] = field(default_factory=tuple)
230+
231+
@property
232+
def canonical_name(self) -> NormalizedName:
233+
return canonicalize_name(self.name)
236234

237235
@classmethod
238236
def from_dist(cls, dist: BaseDistribution) -> "FrozenRequirement":

src/pip/_internal/req/req_file.py

Lines changed: 41 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
import re
99
import shlex
1010
import urllib.parse
11+
from dataclasses import dataclass
1112
from optparse import Values
1213
from typing import (
1314
TYPE_CHECKING,
@@ -84,49 +85,48 @@
8485
logger = logging.getLogger(__name__)
8586

8687

88+
@dataclass(frozen=True)
8789
class ParsedRequirement:
88-
def __init__(
89-
self,
90-
requirement: str,
91-
is_editable: bool,
92-
comes_from: str,
93-
constraint: bool,
94-
options: Optional[Dict[str, Any]] = None,
95-
line_source: Optional[str] = None,
96-
) -> None:
97-
self.requirement = requirement
98-
self.is_editable = is_editable
99-
self.comes_from = comes_from
100-
self.options = options
101-
self.constraint = constraint
102-
self.line_source = line_source
90+
# TODO: replace this with slots=True when dropping Python 3.9 support.
91+
__slots__ = (
92+
"requirement",
93+
"is_editable",
94+
"comes_from",
95+
"constraint",
96+
"options",
97+
"line_source",
98+
)
99+
100+
requirement: str
101+
is_editable: bool
102+
comes_from: str
103+
constraint: bool
104+
options: Optional[Dict[str, Any]]
105+
line_source: Optional[str]
103106

104107

108+
@dataclass(frozen=True)
105109
class ParsedLine:
106-
def __init__(
107-
self,
108-
filename: str,
109-
lineno: int,
110-
args: str,
111-
opts: Values,
112-
constraint: bool,
113-
) -> None:
114-
self.filename = filename
115-
self.lineno = lineno
116-
self.opts = opts
117-
self.constraint = constraint
118-
119-
if args:
120-
self.is_requirement = True
121-
self.is_editable = False
122-
self.requirement = args
123-
elif opts.editables:
124-
self.is_requirement = True
125-
self.is_editable = True
110+
__slots__ = ("filename", "lineno", "args", "opts", "constraint")
111+
112+
filename: str
113+
lineno: int
114+
args: str
115+
opts: Values
116+
constraint: bool
117+
118+
@property
119+
def is_editable(self) -> bool:
120+
return bool(self.opts.editables)
121+
122+
@property
123+
def requirement(self) -> Optional[str]:
124+
if self.args:
125+
return self.args
126+
elif self.is_editable:
126127
# We don't support multiple -e on one line
127-
self.requirement = opts.editables[0]
128-
else:
129-
self.is_requirement = False
128+
return self.opts.editables[0]
129+
return None
130130

131131

132132
def parse_requirements(
@@ -179,7 +179,7 @@ def handle_requirement_line(
179179
line.lineno,
180180
)
181181

182-
assert line.is_requirement
182+
assert line.requirement is not None
183183

184184
# get the options that apply to requirements
185185
if line.is_editable:
@@ -301,7 +301,7 @@ def handle_line(
301301
affect the finder.
302302
"""
303303

304-
if line.is_requirement:
304+
if line.requirement is not None:
305305
parsed_req = handle_requirement_line(line, options)
306306
return parsed_req
307307
else:
@@ -340,7 +340,7 @@ def _parse_and_recurse(
340340
parsed_files_stack: List[Dict[str, Optional[str]]],
341341
) -> Generator[ParsedLine, None, None]:
342342
for line in self._parse_file(filename, constraint):
343-
if not line.is_requirement and (
343+
if line.requirement is None and (
344344
line.opts.requirements or line.opts.constraints
345345
):
346346
# parse a nested requirements file

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -309,7 +309,7 @@ def iter_index_candidate_infos() -> Iterator[IndexCandidateInfo]:
309309
specifier=specifier,
310310
hashes=hashes,
311311
)
312-
icans = list(result.iter_applicable())
312+
icans = result.applicable_candidates
313313

314314
# PEP 592: Yanked releases are ignored unless the specifier
315315
# explicitly pins a version (via '==' or '===') that can be

tests/unit/test_index.py

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -467,13 +467,13 @@ def test_compute_best_candidate(self) -> None:
467467
)
468468
result = evaluator.compute_best_candidate(candidates)
469469

470-
assert result._candidates == candidates
470+
assert result.all_candidates == candidates
471471
expected_applicable = candidates[:2]
472472
assert [str(c.version) for c in expected_applicable] == [
473473
"1.10",
474474
"1.11",
475475
]
476-
assert result._applicable_candidates == expected_applicable
476+
assert result.applicable_candidates == expected_applicable
477477

478478
assert result.best_candidate is expected_applicable[1]
479479

@@ -490,8 +490,8 @@ def test_compute_best_candidate__none_best(self) -> None:
490490
)
491491
result = evaluator.compute_best_candidate(candidates)
492492

493-
assert result._candidates == candidates
494-
assert result._applicable_candidates == []
493+
assert result.all_candidates == candidates
494+
assert result.applicable_candidates == []
495495
assert result.best_candidate is None
496496

497497
@pytest.mark.parametrize(

0 commit comments

Comments
 (0)