From 308b11b4fd1cdfbfb17f8480d9a202a239295271 Mon Sep 17 00:00:00 2001 From: Lalatendu Mohanty Date: Thu, 13 Aug 2026 10:10:15 -0400 Subject: [PATCH] feat(packagesettings): activate `source` field and add `source_resolver` PBI property Uncomment the `source: SourceResolver | None` field on both `PackageSettings` and `VariantInfo`, add `SourceResolver` to the public API, and wire up a `PackageBuildInfo.source_resolver` property with variant-then-package fallback. This is the foundation for all downstream resolver-config wiring (Phases 2-9 of the new resolver config epic). Closes: #1291 Co-Authored-By: Claude Signed-off-by: Lalatendu Mohanty --- src/fromager/packagesettings/__init__.py | 2 + src/fromager/packagesettings/_models.py | 12 ++- src/fromager/packagesettings/_pbi.py | 14 ++++ tests/test_packagesettings.py | 96 ++++++++++++++++++++++++ 4 files changed, 117 insertions(+), 7 deletions(-) diff --git a/src/fromager/packagesettings/__init__.py b/src/fromager/packagesettings/__init__.py index abca2f21..484ef57f 100644 --- a/src/fromager/packagesettings/__init__.py +++ b/src/fromager/packagesettings/__init__.py @@ -28,6 +28,7 @@ PyPIGitResolver, PyPIPrebuiltResolver, PyPISDistResolver, + SourceResolver, pep440_tag_matcher, ) from ._settings import Settings, SettingsFile @@ -84,6 +85,7 @@ "SbomSettings", "Settings", "SettingsFile", + "SourceResolver", "Template", "Variant", "VariantChangelog", diff --git a/src/fromager/packagesettings/_models.py b/src/fromager/packagesettings/_models.py index e8723061..5e646d89 100644 --- a/src/fromager/packagesettings/_models.py +++ b/src/fromager/packagesettings/_models.py @@ -16,7 +16,7 @@ from pydantic import AnyUrl, Field, PrivateAttr, StringConstraints from pydantic_core import core_schema -# from ._resolver import SourceResolver +from ._resolver import SourceResolver from ._typedefs import ( MODEL_CONFIG, BuildDirectory, @@ -480,9 +480,8 @@ class VariantInfo(pydantic.BaseModel): pre_built: bool = False """Use pre-built wheel from index server?""" - # TODO - # source: SourceResolver | None - # """Source resolver and downloader""" + source: SourceResolver | None = None + """Source resolver and downloader""" class GitOptions(pydantic.BaseModel): @@ -608,9 +607,8 @@ class PackageSettings(pydantic.BaseModel): project_override: ProjectOverride = Field(default_factory=ProjectOverride) """Patch project settings""" - # TODO - # source: SourceResolver | None - # """Source resolver and downloader""" + source: SourceResolver | None = None + """Source resolver and downloader""" variants: Mapping[Variant, VariantInfo] = Field(default_factory=dict) """Variant configuration""" diff --git a/src/fromager/packagesettings/_pbi.py b/src/fromager/packagesettings/_pbi.py index 2e262004..ba62f9bf 100644 --- a/src/fromager/packagesettings/_pbi.py +++ b/src/fromager/packagesettings/_pbi.py @@ -26,6 +26,7 @@ if typing.TYPE_CHECKING: from .. import build_environment + from ._resolver import SourceResolver from ._settings import Settings logger = logging.getLogger(__name__) @@ -176,6 +177,19 @@ def wheel_server_url(self) -> str | None: return str(vi.wheel_server_url) return None + @property + def source_resolver(self) -> SourceResolver | None: + """Source resolver for the package variant. + + Returns the variant-level source resolver if configured, + otherwise falls back to the package-level source resolver, + or ``None`` if neither is set. + """ + vi = self._ps.variants.get(self.variant) + if vi is not None and vi.source is not None: + return vi.source + return self._ps.source + @property def override_module_name(self) -> str: """Override module name from package name""" diff --git a/tests/test_packagesettings.py b/tests/test_packagesettings.py index a5107ad9..47826f49 100644 --- a/tests/test_packagesettings.py +++ b/tests/test_packagesettings.py @@ -18,9 +18,12 @@ Package, PackageBuildInfo, PackageSettings, + PyPIPrebuiltResolver, + PyPISDistResolver, ResolverDist, Settings, SettingsFile, + SourceResolver, Variant, substitute_template, ) @@ -90,6 +93,7 @@ "use_pypi_org_metadata": True, "min_release_age": None, }, + "source": None, "variants": { "cpu": { "annotations": { @@ -98,6 +102,7 @@ "env": {"EGG": "spam ${EGG}", "EGG_AGAIN": "$EGG"}, "wheel_server_url": "https://wheel.test/simple", "pre_built": False, + "source": None, }, "rocm": { "annotations": { @@ -106,12 +111,14 @@ "env": {"SPAM": ""}, "wheel_server_url": None, "pre_built": True, + "source": None, }, "cuda": { "annotations": None, "env": {}, "wheel_server_url": None, "pre_built": False, + "source": None, }, }, } @@ -152,6 +159,7 @@ "use_pypi_org_metadata": None, "min_release_age": None, }, + "source": None, "variants": {}, } @@ -193,11 +201,13 @@ "use_pypi_org_metadata": None, "min_release_age": None, }, + "source": None, "variants": { "cpu": { "annotations": None, "env": {}, "pre_built": True, + "source": None, "wheel_server_url": None, }, }, @@ -1152,3 +1162,89 @@ def test_filter_env( ) -> None: ec = ExternalCommands(keep_env=keep, delete_env=delete) assert ec.filter_env(env) == expected + + +# -- source field and source_resolver PBI property ---------------------------- + + +def test_source_resolver_importable() -> None: + """``SourceResolver`` is importable from ``fromager.packagesettings``.""" + assert SourceResolver is not None + + +def test_package_settings_with_source() -> None: + """``PackageSettings`` loads YAML with ``source:`` correctly.""" + yaml_str = "source:\n provider: pypi-sdist\n" + ps = PackageSettings.from_string("test-source-pkg", yaml_str) + assert ps.source is not None + assert isinstance(ps.source, PyPISDistResolver) + assert ps.source.provider == "pypi-sdist" + + +def test_variant_info_with_source() -> None: + """``VariantInfo`` with ``source:`` works.""" + yaml_str = "variants:\n cpu:\n source:\n provider: pypi-prebuilt\n" + ps = PackageSettings.from_string("test-variant-source-pkg", yaml_str) + vi = ps.variants[Variant("cpu")] + assert vi.source is not None + assert isinstance(vi.source, PyPIPrebuiltResolver) + + +def test_package_settings_without_source() -> None: + """``PackageSettings`` without ``source:`` has ``source=None``.""" + ps = PackageSettings.from_default("test-pkg") + assert ps.source is None + + +def test_pbi_source_resolver_none(tmp_path: pathlib.Path) -> None: + """``pbi.source_resolver`` returns ``None`` when no source configured.""" + ps = PackageSettings.from_default("test-pkg") + settings = Settings( + settings=SettingsFile(), + package_settings=[ps], + variant="cpu", + patches_dir=tmp_path, + max_jobs=1, + ) + pbi = settings.package_build_info("test-pkg") + assert pbi.source_resolver is None + + +def test_pbi_source_resolver_package_level(tmp_path: pathlib.Path) -> None: + """``pbi.source_resolver`` returns package-level source.""" + yaml_str = "source:\n provider: pypi-sdist\n" + ps = PackageSettings.from_string("test-pkg", yaml_str) + settings = Settings( + settings=SettingsFile(), + package_settings=[ps], + variant="cpu", + patches_dir=tmp_path, + max_jobs=1, + ) + pbi = settings.package_build_info("test-pkg") + assert pbi.source_resolver is not None + assert isinstance(pbi.source_resolver, PyPISDistResolver) + + +def test_pbi_source_resolver_variant_overrides_package( + tmp_path: pathlib.Path, +) -> None: + """``pbi.source_resolver`` returns variant source over package source.""" + yaml_str = ( + "source:\n" + " provider: pypi-sdist\n" + "variants:\n" + " cpu:\n" + " source:\n" + " provider: pypi-prebuilt\n" + ) + ps = PackageSettings.from_string("test-pkg", yaml_str) + settings = Settings( + settings=SettingsFile(), + package_settings=[ps], + variant="cpu", + patches_dir=tmp_path, + max_jobs=1, + ) + pbi = settings.package_build_info("test-pkg") + assert isinstance(pbi.source_resolver, PyPIPrebuiltResolver)