Skip to content

Commit fdd4c3f

Browse files
committed
chore: make Candidate a dataclass
Convert the `Candidate` class to an immutable dataclass. Instead of an URL to the metadata file, a candidate object now only holds an `has_metadata` flag. The metadata URL is always `self.url + 'metadata'`. `extras` is now a sorted tuple instead of `Iterable[str] | None`. This simplifies the logic, too. Signed-off-by: Christian Heimes <cheimes@redhat.com>
1 parent 60e8f11 commit fdd4c3f

3 files changed

Lines changed: 52 additions & 31 deletions

File tree

src/fromager/candidate.py

Lines changed: 35 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import dataclasses
12
import logging
23
import typing
34
from email.message import EmailMessage, Message
@@ -22,37 +23,47 @@
2223
Metadata = Message
2324

2425

26+
@dataclasses.dataclass(frozen=True, order=True, slots=True, repr=False, kw_only=True)
2527
class Candidate:
26-
def __init__(
27-
self,
28-
name: str,
29-
version: Version,
30-
url: str,
31-
extras: typing.Iterable[str] | None = None,
32-
is_sdist: bool | None = None,
33-
build_tag: BuildTag = (),
34-
metadata_url: str | None = None,
35-
):
36-
self.name = canonicalize_name(name)
37-
self.version = version
38-
self.url = url
39-
self.extras = extras
40-
self.is_sdist = is_sdist
41-
self.build_tag = build_tag
42-
self.metadata_url = metadata_url
43-
44-
self._metadata: Metadata | None = None
45-
self._dependencies: list[Requirement] | None = None
28+
name: str
29+
version: Version
30+
url: str
31+
is_sdist: bool | None = dataclasses.field(default=None)
32+
extras: tuple[str, ...] = dataclasses.field(default=(), compare=False)
33+
build_tag: BuildTag = dataclasses.field(default=(), compare=False)
34+
has_metadata: bool = dataclasses.field(default=False, compare=False)
35+
36+
_metadata: Metadata | None = dataclasses.field(
37+
default=None, init=False, compare=False
38+
)
39+
_dependencies: list[Requirement] | None = dataclasses.field(
40+
default=None, init=False, compare=False
41+
)
42+
43+
def __post_init__(self):
44+
# force normalized name
45+
object.__setattr__(self, "name", canonicalize_name(self.name))
4646

4747
def __repr__(self) -> str:
4848
if not self.extras:
4949
return f"<{self.name}=={self.version}>"
5050
return f"<{self.name}[{','.join(self.extras)}]=={self.version}>"
5151

52+
@property
53+
def metadata_url(self) -> str | None:
54+
"""PEP 658: metadata is available at {url}.metadata"""
55+
if self.has_metadata:
56+
return self.url + ".metadata"
57+
return None
58+
5259
@property
5360
def metadata(self) -> Metadata:
5461
if self._metadata is None:
55-
self._metadata = get_metadata_for_wheel(self.url, self.metadata_url)
62+
if not self.has_metadata:
63+
raise ValueError(f"{self.url} does not have metadata")
64+
metadata = get_metadata_for_wheel(self.url, self.metadata_url)
65+
object.__setattr__(self, "_metadata", metadata)
66+
assert self._metadata
5667
return self._metadata
5768

5869
def _get_dependencies(self) -> typing.Iterable[Requirement]:
@@ -71,7 +82,9 @@ def _get_dependencies(self) -> typing.Iterable[Requirement]:
7182
@property
7283
def dependencies(self) -> list[Requirement]:
7384
if self._dependencies is None:
74-
self._dependencies = list(self._get_dependencies())
85+
dependencies = list(self._get_dependencies())
86+
object.__setattr__(self, "_dependencies", dependencies)
87+
assert self._dependencies
7588
return self._dependencies
7689

7790
@property

src/fromager/resolver.py

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -339,13 +339,13 @@ def get_project_from_pypi(
339339
continue
340340

341341
c = Candidate(
342-
name,
343-
version,
342+
name=name,
343+
version=version,
344344
url=dp.url,
345-
extras=extras,
345+
extras=tuple(sorted(extras)),
346346
is_sdist=is_sdist,
347347
build_tag=build_tag,
348-
metadata_url=dp.metadata_url if dp.has_metadata else None,
348+
has_metadata=bool(dp.has_metadata),
349349
)
350350
if DEBUG_RESOLVER:
351351
logger.debug("candidate %s (%s) %s", dp.filename, c, dp.url)
@@ -723,7 +723,7 @@ def find_candidates(self, identifier) -> Candidates:
723723
continue
724724
assert isinstance(version, Version)
725725
version = version
726-
candidates.append(Candidate(identifier, version, url=url))
726+
candidates.append(Candidate(name=identifier, version=version, url=url))
727727
return candidates
728728

729729

tests/test_pep658_support.py

Lines changed: 12 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ def test_candidate_with_metadata_url(self):
1616
name="test-package",
1717
version=Version("1.0.0"),
1818
url="https://example.com/test-package-1.0.0-py3-none-any.whl",
19-
metadata_url="https://example.com/test-package-1.0.0-py3-none-any.whl.metadata",
19+
has_metadata=True,
2020
)
2121

2222
assert (
@@ -132,12 +132,15 @@ def test_candidate_repr_with_metadata_url(self):
132132
name="test-package",
133133
version=Version("1.0.0"),
134134
url="https://example.com/test-package-1.0.0-py3-none-any.whl",
135-
metadata_url="https://example.com/test-package-1.0.0-py3-none-any.whl.metadata",
135+
has_metadata=True,
136136
)
137137

138138
# The candidate should have the metadata URL attribute
139139
assert hasattr(candidate, "metadata_url")
140-
assert candidate.metadata_url is not None
140+
assert (
141+
candidate.metadata_url
142+
== "https://example.com/test-package-1.0.0-py3-none-any.whl.metadata"
143+
)
141144

142145
def test_metadata_url_construction(self):
143146
"""Test that metadata URLs are constructed correctly."""
@@ -157,7 +160,7 @@ def test_pep658_integration_with_resolver(self):
157160
name="test-package",
158161
version=Version("1.0.0"),
159162
url="https://example.com/test.whl",
160-
metadata_url="https://example.com/test.whl.metadata",
163+
has_metadata=True,
161164
)
162165

163166
candidate_without_metadata = Candidate(
@@ -166,6 +169,11 @@ def test_pep658_integration_with_resolver(self):
166169
url="https://example.com/test.whl",
167170
)
168171

172+
assert (
173+
candidate_with_metadata.metadata_url
174+
== "https://example.com/test.whl.metadata"
175+
)
176+
169177
# Verify PEP 658 metadata URL handling
170178
assert (
171179
candidate_with_metadata.metadata_url

0 commit comments

Comments
 (0)