Skip to content

Commit 81d7024

Browse files
authored
Merge pull request #867 from tiran/candidate-extra-info
feat: improve resolver candidate
2 parents f327d62 + 50d342b commit 81d7024

3 files changed

Lines changed: 90 additions & 28 deletions

File tree

src/fromager/candidate.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import dataclasses
2+
import datetime
23
import logging
34
import typing
45
from email.message import EmailMessage, Message
@@ -32,6 +33,11 @@ class Candidate:
3233
extras: tuple[str, ...] = dataclasses.field(default=(), compare=False)
3334
build_tag: BuildTag = dataclasses.field(default=(), compare=False)
3435
has_metadata: bool = dataclasses.field(default=False, compare=False)
36+
remote_tag: str | None = dataclasses.field(default=None, compare=False)
37+
remote_commit: str | None = dataclasses.field(default=None, compare=False)
38+
upload_time: datetime.datetime | None = dataclasses.field(
39+
default=None, compare=False
40+
)
3541

3642
_metadata: Metadata | None = dataclasses.field(
3743
default=None, init=False, compare=False

src/fromager/resolver.py

Lines changed: 75 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
#
66
from __future__ import annotations
77

8+
import datetime
89
import functools
910
import logging
1011
import os
@@ -40,6 +41,7 @@
4041
if typing.TYPE_CHECKING:
4142
from . import context
4243

44+
4345
logger = logging.getLogger(__name__)
4446

4547
PYTHON_VERSION = Version(python_version())
@@ -339,6 +341,10 @@ def get_project_from_pypi(
339341
ignored_candidates.add(dp.filename)
340342
continue
341343

344+
upload_time = dp.upload_time
345+
if upload_time is not None:
346+
upload_time = upload_time.astimezone(datetime.UTC)
347+
342348
c = Candidate(
343349
name=name,
344350
version=version,
@@ -347,6 +353,7 @@ def get_project_from_pypi(
347353
is_sdist=is_sdist,
348354
build_tag=build_tag,
349355
has_metadata=bool(dp.has_metadata),
356+
upload_time=upload_time,
350357
)
351358
if DEBUG_RESOLVER:
352359
logger.debug("candidate %s (%s) %s", dp.filename, c, dp.url)
@@ -367,7 +374,7 @@ def get_project_from_pypi(
367374
]
368375
VersionSource: typing.TypeAlias = typing.Callable[
369376
[str],
370-
typing.Iterable[tuple[str, str | Version]],
377+
typing.Iterable[Candidate | tuple[str, str | Version]],
371378
]
372379

373380

@@ -714,17 +721,28 @@ def cache_key(self) -> str:
714721
def find_candidates(self, identifier) -> Candidates:
715722
candidates: list[Candidate] = []
716723
version: Version | None
717-
for url, item in self._version_source(identifier):
718-
if isinstance(item, Version):
719-
version = item
724+
for item in self._version_source(identifier):
725+
if isinstance(item, Candidate):
726+
candidate = item
720727
else:
721-
version = self._match_function(identifier, item)
722-
if version is None:
723-
logger.debug(f"{identifier}: match function ignores {item}")
724-
continue
725-
assert isinstance(version, Version)
726-
version = version
727-
candidates.append(Candidate(name=identifier, version=version, url=url))
728+
# TODO: deprecate (url, version_or_string)
729+
url, version_or_string = item
730+
if isinstance(version_or_string, Version):
731+
version = version_or_string
732+
else:
733+
match_result = self._match_function(identifier, version_or_string)
734+
if match_result is None:
735+
logger.debug(
736+
f"{identifier}: match function ignores {version_or_string}"
737+
)
738+
continue
739+
assert isinstance(match_result, Version)
740+
version = match_result
741+
742+
candidate = Candidate(name=identifier, version=version, url=url)
743+
744+
candidates.append(candidate)
745+
728746
return candidates
729747

730748

@@ -770,7 +788,7 @@ def cache_key(self) -> str:
770788
def _find_tags(
771789
self,
772790
identifier: str,
773-
) -> Iterable[tuple[str, Version]]:
791+
) -> Iterable[Candidate]:
774792
headers = {"accept": "application/vnd.github+json"}
775793

776794
# Add GitHub authentication if available
@@ -784,13 +802,24 @@ def _find_tags(
784802
resp.raise_for_status()
785803

786804
for entry in resp.json():
787-
name = entry["name"]
788-
result = self._match_function(identifier, name)
789-
if result is None:
790-
logger.debug(f"{identifier}: match function ignores {name}")
805+
tagname = entry["name"]
806+
version = self._match_function(identifier, tagname)
807+
if version is None:
808+
logger.debug(f"{identifier}: match function ignores {tagname}")
791809
continue
792-
assert isinstance(result, Version)
793-
yield entry["tarball_url"], result
810+
assert isinstance(version, Version)
811+
url = entry["tarball_url"]
812+
813+
# Github tag API endpoint does not include commit date information.
814+
# It would be too expensive to query every commit API endpoint.
815+
yield Candidate(
816+
name=identifier,
817+
version=version,
818+
url=url,
819+
remote_tag=tagname,
820+
remote_commit=entry["commit"]["sha"],
821+
upload_time=None,
822+
)
794823

795824
# pagination links
796825
nexturl = resp.links.get("next", {}).get("url")
@@ -841,25 +870,43 @@ def cache_key(self) -> str:
841870
def _find_tags(
842871
self,
843872
identifier: str,
844-
) -> Iterable[tuple[str, Version]]:
873+
) -> Iterable[Candidate]:
845874
nexturl: str = self.api_url
875+
created_at: datetime.datetime | None
846876
while nexturl:
847877
resp: Response = session.get(nexturl)
848878
resp.raise_for_status()
849879
for entry in resp.json():
850-
name = entry["name"]
851-
version = self._match_function(identifier, name)
880+
tagname = entry["name"]
881+
version = self._match_function(identifier, tagname)
852882
if version is None:
853-
logger.debug(f"{identifier}: match function ignores {name}")
883+
logger.debug(f"{identifier}: match function ignores {tagname}")
854884
continue
855885
assert isinstance(version, Version)
856886

857-
# GitLab provides a download URL for the archive, so return it
858-
# in case prepare_source wants to download it instead of cloning
859-
# the repository.
860-
archive_path: str = f"{self.project_path}/-/archive/{name}/{self.project_path.split('/')[-1]}-{name}.tar.gz"
861-
archive_url: str = urljoin(self.server_url, archive_path)
862-
yield archive_url, version
887+
archive_path: str = f"{self.project_path}/-/archive/{tagname}/{self.project_path.split('/')[-1]}-{tagname}.tar.gz"
888+
url = urljoin(self.server_url, archive_path)
889+
890+
# get tag creation time, fall back to commit creation time
891+
created_at_str: str | None = entry.get("created_at")
892+
if created_at_str is None:
893+
created_at_str = entry["commit"].get("created_at")
894+
895+
if created_at_str is not None:
896+
created_at = datetime.datetime.fromisoformat(
897+
created_at_str
898+
).astimezone(datetime.UTC)
899+
else:
900+
created_at = None
901+
902+
yield Candidate(
903+
name=identifier,
904+
version=version,
905+
url=url,
906+
remote_tag=tagname,
907+
remote_commit=entry["commit"]["id"],
908+
upload_time=created_at,
909+
)
863910

864911
# GitLab API uses Link headers for pagination
865912
nexturl = resp.links.get("next", {}).get("url")

tests/test_resolver.py

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import datetime
12
import re
23
import typing
34

@@ -704,6 +705,9 @@ def test_resolve_github() -> None:
704705

705706
candidate = result.mapping["fromager"]
706707
assert str(candidate.version) == "0.9.0"
708+
assert candidate.remote_tag == "0.9.0"
709+
assert candidate.remote_commit == "5fbdab491e983152f7e5c8200b4f7f62f714aedf"
710+
assert candidate.upload_time is None
707711
# check the "URL" in case tag syntax does not match version syntax
708712
assert (
709713
str(candidate.url)
@@ -911,6 +915,11 @@ def test_resolve_gitlab() -> None:
911915
str(candidate.url)
912916
== "https://gitlab.com/mirrors/github/decile-team/submodlib/-/archive/v0.0.3/submodlib-v0.0.3.tar.gz"
913917
)
918+
assert candidate.remote_tag == "v0.0.3"
919+
assert candidate.remote_commit == "72ae33a1ead9761e7240c2e095873047339ada7c"
920+
assert candidate.upload_time == datetime.datetime(
921+
2025, 5, 14, 15, 43, 0, tzinfo=datetime.UTC
922+
)
914923

915924

916925
def test_gitlab_constraint_mismatch() -> None:

0 commit comments

Comments
 (0)