Skip to content

Commit f4d72f1

Browse files
Josef Skladankaclaude
andcommitted
feat(resolver): add versionmap-git source resolver
Add `VersionMapGitResolver` config model for the `versionmap-git` provider from the new-resolver-config proposal. Maps version numbers to git refs (commit SHAs or ref paths) with a separate `clone_url`, replacing the need for handwritten Python plugins. - Add `VersionMapGitResolver` pydantic model with `clone_url`, `build_sdist`, and `versionmap` fields - Register in `SourceResolver` discriminated union - Add to config reference docs - Add tests for config parsing, provider output, and validation Closes: AIPCC-17610 Co-Authored-By: Claude <claude@anthropic.com> Signed-off-by: Josef Skladanka <jskladan@redhat.com>
1 parent 1d2876b commit f4d72f1

5 files changed

Lines changed: 165 additions & 0 deletions

File tree

docs/reference/config-reference.rst

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,8 @@ Source Resolver
4141
.. autopydantic_model:: PyPIGitResolver
4242
:inherited-members: AbstractPyPIResolver, CooldownMixin
4343

44+
.. autopydantic_model:: VersionMapGitResolver
45+
4446
.. autopydantic_model:: GitHubTagDownloadResolver
4547
:inherited-members: AbstractGitSourceResolver, CooldownMixin
4648

src/fromager/packagesettings/__init__.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@
2525
PyPIGitResolver,
2626
PyPIPrebuiltResolver,
2727
PyPISDistResolver,
28+
VersionMapGitResolver,
2829
pep440_tag_matcher,
2930
)
3031
from ._settings import Settings, SettingsFile
@@ -82,6 +83,7 @@
8283
"Variant",
8384
"VariantChangelog",
8485
"VariantInfo",
86+
"VersionMapGitResolver",
8587
"default_update_extra_environ",
8688
"get_extra_environ",
8789
"pep440_tag_matcher",

src/fromager/packagesettings/_resolver.py

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -482,6 +482,63 @@ def resolver_provider(
482482
)
483483

484484

485+
class VersionMapGitResolver(AbstractResolver):
486+
"""Resolve version from a version map, build sdist from git clone.
487+
488+
The ``versionmap-git`` provider maps known version numbers to known git
489+
refs (commit SHAs or ref paths such as ``refs/tags/1.1``). It clones a
490+
git repo at the configured ref and builds an sdist with PEP 517.
491+
492+
Example::
493+
494+
provider: versionmap-git
495+
clone_url: https://git.test/project/repo.git
496+
build_sdist: pep517
497+
versionmap:
498+
'1.0': abad1dea
499+
'1.1': refs/tags/1.1
500+
501+
.. versionadded:: 0.79.0
502+
"""
503+
504+
provider: typing.Literal["versionmap-git"]
505+
506+
clone_url: pydantic.AnyUrl
507+
"""Git clone URL (``https`` or ``ssh`` scheme)."""
508+
509+
build_sdist: BuildSDist = BuildSDist.pep517
510+
"""Source distribution build method."""
511+
512+
versionmap: dict[str, str]
513+
"""Mapping of version strings to git refs."""
514+
515+
@pydantic.field_validator("clone_url", mode="after")
516+
@classmethod
517+
def validate_clone_url(cls, value: pydantic.AnyUrl) -> pydantic.AnyUrl:
518+
if value.scheme not in {"https", "ssh"}:
519+
raise ValueError(f"invalid scheme in url {value}")
520+
if not value.path:
521+
raise ValueError(f"url {value} has an empty path")
522+
return value
523+
524+
def resolver_provider(
525+
self, ctx: context.WorkContext, req_type: requirements_file.RequirementType
526+
) -> resolver.VersionMapProvider:
527+
from ..versionmap import VersionMap
528+
529+
clone_url = str(self.clone_url)
530+
url_map = {
531+
ver: f"git+{clone_url}@{ref}" for ver, ref in self.versionmap.items()
532+
}
533+
version_map = VersionMap(url_map) # type: ignore[arg-type]
534+
return resolver.VersionMapProvider(
535+
version_map=version_map,
536+
package_name=None,
537+
constraints=ctx.constraints,
538+
req_type=req_type,
539+
)
540+
541+
485542
class NotAvailableResolver(AbstractResolver):
486543
"""Prevent resolve and download"""
487544

@@ -510,6 +567,7 @@ def resolver_provider(
510567
| PyPIPrebuiltResolver
511568
| PyPIDownloadResolver
512569
| PyPIGitResolver
570+
| VersionMapGitResolver
513571
| GitHubTagCloneResolver
514572
| GitHubTagDownloadResolver
515573
| GitLabTagCloneResolver

tests/test_packagesettings_resolver.py

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@
2525
PyPIPrebuiltResolver,
2626
PyPISDistResolver,
2727
SourceResolver,
28+
VersionMapGitResolver,
2829
)
2930
from fromager.packagesettings._typedefs import MODEL_CONFIG
3031
from fromager.requirements_file import RequirementType
@@ -426,6 +427,50 @@ def test_resolver_provider(self, tmp_context: WorkContext) -> None:
426427
# -- Special resolvers --------------------------------------------------------
427428

428429

430+
class TestVersionMapGitResolver:
431+
YAML = """\
432+
source:
433+
provider: versionmap-git
434+
clone_url: https://git.test/project/repo.git
435+
build_sdist: pep517
436+
versionmap:
437+
'1.0': abad1dea
438+
'1.1': refs/tags/1.1
439+
"""
440+
441+
def test_parse(self) -> None:
442+
r = _parse(self.YAML)
443+
assert isinstance(r, VersionMapGitResolver)
444+
assert r.provider == "versionmap-git"
445+
assert str(r.clone_url) == "https://git.test/project/repo.git"
446+
assert r.build_sdist == BuildSDist.pep517
447+
assert r.versionmap == {"1.0": "abad1dea", "1.1": "refs/tags/1.1"}
448+
449+
def test_resolver_provider(self, tmp_context: WorkContext) -> None:
450+
r = _parse(self.YAML)
451+
p = r.resolver_provider(tmp_context, _REQ_TYPE)
452+
assert isinstance(p, resolver.VersionMapProvider)
453+
clone_url = "https://git.test/project/repo.git"
454+
assert p.version_map["1.0"] == f"git+{clone_url}@abad1dea"
455+
assert p.version_map["1.1"] == f"git+{clone_url}@refs/tags/1.1"
456+
457+
def test_clone_url_rejects_http(self) -> None:
458+
with pytest.raises(pydantic.ValidationError):
459+
VersionMapGitResolver(
460+
provider="versionmap-git",
461+
clone_url="http://git.test/project/repo.git", # type: ignore[arg-type]
462+
versionmap={"1.0": "abc123"},
463+
)
464+
465+
def test_clone_url_rejects_empty_path(self) -> None:
466+
with pytest.raises(pydantic.ValidationError):
467+
VersionMapGitResolver(
468+
provider="versionmap-git",
469+
clone_url="https://git.test", # type: ignore[arg-type]
470+
versionmap={"1.0": "abc123"},
471+
)
472+
473+
429474
class TestNotAvailableResolver:
430475
YAML = """\
431476
source:

tests/test_resolver.py

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -920,6 +920,64 @@ def test_resolve_versionmap_no_match() -> None:
920920
rslvr.resolve([Requirement("testpkg>=2.0")])
921921

922922

923+
def test_resolve_versionmap_git() -> None:
924+
from fromager.versionmap import VersionMap
925+
926+
clone_url = "https://git.test/project/repo.git"
927+
version_map = VersionMap(
928+
{
929+
"1.0": f"git+{clone_url}@abad1dea",
930+
"1.1": f"git+{clone_url}@refs/tags/1.1",
931+
"1.2": f"git+{clone_url}@d3adb33f",
932+
}
933+
)
934+
935+
provider = resolver.VersionMapProvider(
936+
version_map=version_map,
937+
package_name="testpkg",
938+
)
939+
reporter: resolvelib.BaseReporter = resolvelib.BaseReporter()
940+
rslvr = resolvelib.Resolver(provider, reporter)
941+
942+
result = rslvr.resolve([Requirement("testpkg")])
943+
assert "testpkg" in result.mapping
944+
945+
candidate = result.mapping["testpkg"]
946+
assert str(candidate.version) == "1.2"
947+
assert candidate.url == f"git+{clone_url}@d3adb33f"
948+
949+
950+
def test_resolve_versionmap_git_with_constraint() -> None:
951+
from fromager.versionmap import VersionMap
952+
953+
clone_url = "https://git.test/project/repo.git"
954+
version_map = VersionMap(
955+
{
956+
"1.0": f"git+{clone_url}@abad1dea",
957+
"1.1": f"git+{clone_url}@refs/tags/1.1",
958+
"1.2": f"git+{clone_url}@d3adb33f",
959+
}
960+
)
961+
962+
c = constraints.Constraints()
963+
c.add_constraint("testpkg<1.2")
964+
965+
provider = resolver.VersionMapProvider(
966+
version_map=version_map,
967+
package_name="testpkg",
968+
constraints=c,
969+
)
970+
reporter: resolvelib.BaseReporter = resolvelib.BaseReporter()
971+
rslvr = resolvelib.Resolver(provider, reporter)
972+
973+
result = rslvr.resolve([Requirement("testpkg")])
974+
assert "testpkg" in result.mapping
975+
976+
candidate = result.mapping["testpkg"]
977+
assert str(candidate.version) == "1.1"
978+
assert candidate.url == f"git+{clone_url}@refs/tags/1.1"
979+
980+
923981
_gitlab_submodlib_repo_response = """
924982
[
925983
{

0 commit comments

Comments
 (0)