Skip to content

Commit

Permalink
Convert more record classes to dataclasses (#12659)
Browse files Browse the repository at this point in the history
- 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...)
  • Loading branch information
ichard26 authored Dec 7, 2024
1 parent 07c7a14 commit 8dbbb2e
Show file tree
Hide file tree
Showing 6 changed files with 75 additions and 91 deletions.
Empty file.
50 changes: 18 additions & 32 deletions src/pip/_internal/index/package_finder.py
Original file line number Diff line number Diff line change
Expand Up @@ -334,44 +334,30 @@ class CandidatePreferences:
allow_all_prereleases: bool = False


@dataclass(frozen=True)
class BestCandidateResult:
"""A collection of candidates, returned by `PackageFinder.find_best_candidate`.
This class is only intended to be instantiated by CandidateEvaluator's
`compute_best_candidate()` method.
"""

def __init__(
self,
candidates: List[InstallationCandidate],
applicable_candidates: List[InstallationCandidate],
best_candidate: Optional[InstallationCandidate],
) -> None:
"""
:param candidates: A sequence of all available candidates found.
:param applicable_candidates: The applicable candidates.
:param best_candidate: The most preferred candidate found, or None
if no applicable candidates were found.
"""
assert set(applicable_candidates) <= set(candidates)

if best_candidate is None:
assert not applicable_candidates
else:
assert best_candidate in applicable_candidates
self._applicable_candidates = applicable_candidates
self._candidates = candidates
:param all_candidates: A sequence of all available candidates found.
:param applicable_candidates: The applicable candidates.
:param best_candidate: The most preferred candidate found, or None
if no applicable candidates were found.
"""

self.best_candidate = best_candidate
all_candidates: List[InstallationCandidate]
applicable_candidates: List[InstallationCandidate]
best_candidate: Optional[InstallationCandidate]

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

def iter_applicable(self) -> Iterable[InstallationCandidate]:
"""Iterate through the applicable candidates."""
return iter(self._applicable_candidates)
if self.best_candidate is None:
assert not self.applicable_candidates
else:
assert self.best_candidate in self.applicable_candidates


class CandidateEvaluator:
Expand Down Expand Up @@ -929,7 +915,7 @@ def _format_versions(cand_iter: Iterable[InstallationCandidate]) -> str:
"Could not find a version that satisfies the requirement %s "
"(from versions: %s)",
req,
_format_versions(best_candidate_result.iter_all()),
_format_versions(best_candidate_result.all_candidates),
)

raise DistributionNotFound(f"No matching distribution found for {req}")
Expand Down Expand Up @@ -963,15 +949,15 @@ def _should_install_candidate(
logger.debug(
"Using version %s (newest of versions: %s)",
best_candidate.version,
_format_versions(best_candidate_result.iter_applicable()),
_format_versions(best_candidate_result.applicable_candidates),
)
return best_candidate

# We have an existing version, and its the best version
logger.debug(
"Installed version (%s) is most up-to-date (past versions: %s)",
installed_version,
_format_versions(best_candidate_result.iter_applicable()),
_format_versions(best_candidate_result.applicable_candidates),
)
raise BestVersionAlreadyInstalled

Expand Down
24 changes: 11 additions & 13 deletions src/pip/_internal/operations/freeze.py
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
import collections
import logging
import os
from dataclasses import dataclass, field
from typing import Container, Dict, Generator, Iterable, List, NamedTuple, Optional, Set

from pip._vendor.packaging.utils import canonicalize_name
from pip._vendor.packaging.utils import NormalizedName, canonicalize_name
from pip._vendor.packaging.version import InvalidVersion

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


@dataclass(frozen=True)
class FrozenRequirement:
def __init__(
self,
name: str,
req: str,
editable: bool,
comments: Iterable[str] = (),
) -> None:
self.name = name
self.canonical_name = canonicalize_name(name)
self.req = req
self.editable = editable
self.comments = comments
name: str
req: str
editable: bool
comments: Iterable[str] = field(default_factory=tuple)

@property
def canonical_name(self) -> NormalizedName:
return canonicalize_name(self.name)

@classmethod
def from_dist(cls, dist: BaseDistribution) -> "FrozenRequirement":
Expand Down
82 changes: 41 additions & 41 deletions src/pip/_internal/req/req_file.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
import re
import shlex
import urllib.parse
from dataclasses import dataclass
from optparse import Values
from typing import (
TYPE_CHECKING,
Expand Down Expand Up @@ -84,49 +85,48 @@
logger = logging.getLogger(__name__)


@dataclass(frozen=True)
class ParsedRequirement:
def __init__(
self,
requirement: str,
is_editable: bool,
comes_from: str,
constraint: bool,
options: Optional[Dict[str, Any]] = None,
line_source: Optional[str] = None,
) -> None:
self.requirement = requirement
self.is_editable = is_editable
self.comes_from = comes_from
self.options = options
self.constraint = constraint
self.line_source = line_source
# TODO: replace this with slots=True when dropping Python 3.9 support.
__slots__ = (
"requirement",
"is_editable",
"comes_from",
"constraint",
"options",
"line_source",
)

requirement: str
is_editable: bool
comes_from: str
constraint: bool
options: Optional[Dict[str, Any]]
line_source: Optional[str]


@dataclass(frozen=True)
class ParsedLine:
def __init__(
self,
filename: str,
lineno: int,
args: str,
opts: Values,
constraint: bool,
) -> None:
self.filename = filename
self.lineno = lineno
self.opts = opts
self.constraint = constraint

if args:
self.is_requirement = True
self.is_editable = False
self.requirement = args
elif opts.editables:
self.is_requirement = True
self.is_editable = True
__slots__ = ("filename", "lineno", "args", "opts", "constraint")

filename: str
lineno: int
args: str
opts: Values
constraint: bool

@property
def is_editable(self) -> bool:
return bool(self.opts.editables)

@property
def requirement(self) -> Optional[str]:
if self.args:
return self.args
elif self.is_editable:
# We don't support multiple -e on one line
self.requirement = opts.editables[0]
else:
self.is_requirement = False
return self.opts.editables[0]
return None


def parse_requirements(
Expand Down Expand Up @@ -179,7 +179,7 @@ def handle_requirement_line(
line.lineno,
)

assert line.is_requirement
assert line.requirement is not None

# get the options that apply to requirements
if line.is_editable:
Expand Down Expand Up @@ -301,7 +301,7 @@ def handle_line(
affect the finder.
"""

if line.is_requirement:
if line.requirement is not None:
parsed_req = handle_requirement_line(line, options)
return parsed_req
else:
Expand Down Expand Up @@ -340,7 +340,7 @@ def _parse_and_recurse(
parsed_files_stack: List[Dict[str, Optional[str]]],
) -> Generator[ParsedLine, None, None]:
for line in self._parse_file(filename, constraint):
if not line.is_requirement and (
if line.requirement is None and (
line.opts.requirements or line.opts.constraints
):
# parse a nested requirements file
Expand Down
2 changes: 1 addition & 1 deletion src/pip/_internal/resolution/resolvelib/factory.py
Original file line number Diff line number Diff line change
Expand Up @@ -309,7 +309,7 @@ def iter_index_candidate_infos() -> Iterator[IndexCandidateInfo]:
specifier=specifier,
hashes=hashes,
)
icans = list(result.iter_applicable())
icans = result.applicable_candidates

# PEP 592: Yanked releases are ignored unless the specifier
# explicitly pins a version (via '==' or '===') that can be
Expand Down
8 changes: 4 additions & 4 deletions tests/unit/test_index.py
Original file line number Diff line number Diff line change
Expand Up @@ -467,13 +467,13 @@ def test_compute_best_candidate(self) -> None:
)
result = evaluator.compute_best_candidate(candidates)

assert result._candidates == candidates
assert result.all_candidates == candidates
expected_applicable = candidates[:2]
assert [str(c.version) for c in expected_applicable] == [
"1.10",
"1.11",
]
assert result._applicable_candidates == expected_applicable
assert result.applicable_candidates == expected_applicable

assert result.best_candidate is expected_applicable[1]

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

assert result._candidates == candidates
assert result._applicable_candidates == []
assert result.all_candidates == candidates
assert result.applicable_candidates == []
assert result.best_candidate is None

@pytest.mark.parametrize(
Expand Down

0 comments on commit 8dbbb2e

Please sign in to comment.