Skip to content

Commit

Permalink
Convert more record classes to dataclasses
Browse files Browse the repository at this point in the history
- Removes BestCandidateResult's iter_all() and iter_applicable()
  methods as they were redundant

- Looking closer at SelectionPreferences, it only used slots to prevent
  accidental new attribute assignment (not for reduced memory usage as I
  previously thought) so this is safe to convert

- Removes ParsedLine's is_requirement attribute as it was awkward to use
  (to please mypy, you would need to add asserts on .requirement)
  • Loading branch information
ichard26 committed Apr 29, 2024
1 parent f18bebd commit 748aff7
Show file tree
Hide file tree
Showing 6 changed files with 69 additions and 116 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 @@ -927,7 +913,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 @@ -961,15 +947,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
55 changes: 17 additions & 38 deletions src/pip/_internal/models/selection_prefs.py
Original file line number Diff line number Diff line change
@@ -1,53 +1,32 @@
from dataclasses import dataclass
from typing import Optional

from pip._internal.models.format_control import FormatControl


# TODO: This needs Python 3.10's improved slots support for dataclasses
# to be converted into a dataclass.
@dataclass(frozen=True)
class SelectionPreferences:
"""
Encapsulates the candidate selection preferences for downloading
and installing files.
"""
__slots__ = [
"allow_yanked",
"allow_all_prereleases",
"format_control",
"prefer_binary",
"ignore_requires_python",
]
:param allow_yanked: Whether files marked as yanked (in the sense
of PEP 592) are permitted to be candidates for install.
:param format_control: A FormatControl object or None. Used to control
the selection of source packages / binary packages when consulting
the index and links.
:param prefer_binary: Whether to prefer an old, but valid, binary
dist over a new source dist.
:param ignore_requires_python: Whether to ignore incompatible
"Requires-Python" values in links. Defaults to False.
"""

# Don't include an allow_yanked default value to make sure each call
# site considers whether yanked releases are allowed. This also causes
# that decision to be made explicit in the calling code, which helps
# people when reading the code.
def __init__(
self,
allow_yanked: bool,
allow_all_prereleases: bool = False,
format_control: Optional[FormatControl] = None,
prefer_binary: bool = False,
ignore_requires_python: Optional[bool] = None,
) -> None:
"""Create a SelectionPreferences object.
:param allow_yanked: Whether files marked as yanked (in the sense
of PEP 592) are permitted to be candidates for install.
:param format_control: A FormatControl object or None. Used to control
the selection of source packages / binary packages when consulting
the index and links.
:param prefer_binary: Whether to prefer an old, but valid, binary
dist over a new source dist.
:param ignore_requires_python: Whether to ignore incompatible
"Requires-Python" values in links. Defaults to False.
"""
if ignore_requires_python is None:
ignore_requires_python = False

self.allow_yanked = allow_yanked
self.allow_all_prereleases = allow_all_prereleases
self.format_control = format_control
self.prefer_binary = prefer_binary
self.ignore_requires_python = ignore_requires_python
allow_yanked: bool
allow_all_prereleases: bool = False
format_control: Optional[FormatControl] = None
prefer_binary: bool = False
ignore_requires_python: Optional[bool] = False
70 changes: 29 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,36 @@
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
requirement: str
is_editable: bool
comes_from: str
constraint: bool
options: Optional[Dict[str, Any]] = None
line_source: Optional[str] = None


@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
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 +167,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 +289,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 @@ -335,7 +323,7 @@ def _parse_and_recurse(
self, filename: str, constraint: bool
) -> 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 @@ -300,7 +300,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 @@ -465,13 +465,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 @@ -488,8 +488,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 748aff7

Please sign in to comment.