Skip to content

feat: source resolver configuration#1052

Open
tiran wants to merge 1 commit intopython-wheel-build:mainfrom
tiran:new-resolver-config2
Open

feat: source resolver configuration#1052
tiran wants to merge 1 commit intopython-wheel-build:mainfrom
tiran:new-resolver-config2

Conversation

@tiran
Copy link
Copy Markdown
Collaborator

@tiran tiran commented Apr 13, 2026

Pull Request Description

What

Introduce new configuration for source resolver and downloads. The new system uses profiles for common tasks:

  • pypi-sdist: resolve versions of sdists from PyPI, download sdist
  • pypi-prebuilt: resolve versions of platform wheels from PyPI, download pre-built wheel from PyPI.
  • pypi-download: resolve versions of any package from PyPI, download from external URL (with {version} variable)
  • pypi-git: resolve versions for any package from PyPI, git clone (with {version} variable)
  • versionmap-git: resolve with a version map, git clone
  • github-tag-download: resolve from GitHub tag, download tarball
  • github-tag-git: resolve from GitHub tag, git clone
  • gitlab-tag-download: resolve from GitLab tag, download tarball
  • gitlab-tag-git: resolve from GitLab tag, git clone
  • not-available: block resolving
  • hook: call Fromager hooks

The new settings will eventually replace download_source, resolver_dist, and git_options top-level options as well as wheel_server_url and pre_built flags for variants.

Why

Summary by CodeRabbit

  • New Features

    • Added configurable source resolvers for package acquisition, supporting PyPI, GitHub, GitLab, and version map-based sources.
  • Documentation

    • Extended configuration reference with comprehensive source resolver documentation and available resolver types.

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented Apr 13, 2026

📝 Walkthrough

Walkthrough

Adds a source-resolver subsystem: a new Pydantic-backed _resolver module with multiple resolver models and BuildSDist/DEFAULT_TAG_MATCHER; re-exports resolver symbols from packagesettings.__init__; introduces optional discriminated source fields on VariantInfo and PackageSettings; adds PackageBuildInfo.source_resolver; updates tests and YAML fixtures; adjusts VersionMapProvider to accept package_name=None and disable caching.

Changes

Source-resolver subsystem & wiring

Layer / File(s) Summary
Data Shape
src/fromager/packagesettings/_models.py
Adds optional discriminated `source: SourceResolver
New Models / Core Implementation
src/fromager/packagesettings/_resolver.py
Adds typed resolver models and enums: BuildSDist, VERSION_QUOTED, DEFAULT_TAG_MATCHER, AbstractResolver, CooldownMixin, PyPI resolvers (PyPISDistResolver, PyPIPrebuiltResolver, PyPIDownloadResolver, PyPIGitResolver), VersionMapResolver, AbstractGitSourceResolver, GitHub/GitLab tag resolvers (download/clone), NotAvailableResolver, HookResolver, and SourceResolver union.
Public API Export
src/fromager/packagesettings/__init__.py
Re-exports resolver symbols and expands __all__ to include DEFAULT_TAG_MATCHER, BuildSDist, all resolver classes, and VersionMapResolver.
Wiring / Runtime Access
src/fromager/packagesettings/_pbi.py
Adds source_resolver property returning variant-level vi.source or package-level self._ps.source.
Provider Behavior Adjustment
src/fromager/resolver.py
VersionMapProvider.__init__ accepts `package_name: str
Tests / Fixtures
tests/test_packagesettings.py, tests/testdata/context/overrides/settings/*
Adds YAML fixtures for each resolver variant, updates serialized expectations to include "source": None, and adds parametrized tests validating resolver providers, serialization, and variant-index overrides.
Docs
docs/reference/config-reference.rst
Adds currentmodule directive and a new “Source Resolver” section documenting resolver models and BuildSDist.

Sequence Diagram(s)

sequenceDiagram
    participant PBI as PackageBuildInfo
    participant PS as PackageSettings
    participant SR as SourceResolver (Pydantic model)
    participant Prov as Resolver Provider (PyPI/GitHub/GitLab/VersionMap)
    PBI->>PS: read package settings
    PBI->>SR: resolve source_resolver (variant or package-level)
    SR->>Prov: resolver_provider(ctx, req_type)
    Prov->>Prov: prepare/provider-specific URL or clone info
    Prov-->>PBI: provider instance for fetching versions/artifacts
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly related PRs

Suggested reviewers

  • rd4398
  • LalatenduMohanty
  • dhellmann

Poem

"A rabbit hops with code in paw,
I stitched resolvers, neat and raw.
Tags and wheels, sdist in sight,
Variants choose what’s just right.
Pip, Git, or map — hop!—they fetch tonight 🐇"

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 45.45% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title 'feat: source resolver configuration' accurately describes the main change - introducing a new configuration system for source resolvers. It is concise, specific, and clearly conveys the primary objective of the PR.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Tip

💬 Introducing Slack Agent: The best way for teams to turn conversations into code.

Slack Agent is built on CodeRabbit's deep understanding of your code, so your team can collaborate across the entire SDLC without losing context.

  • Generate code and open pull requests
  • Plan features and break down work
  • Investigate incidents and troubleshoot customer tickets together
  • Automate recurring tasks and respond to alerts with triggers
  • Summarize progress and report instantly

Built for teams:

  • Shared memory across your entire org—no repeating context
  • Per-thread sandboxes to safely plan and execute work
  • Governance built-in—scoped access, auditability, and budget controls

One agent for your entire SDLC. Right inside Slack.

👉 Get started


Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@tiran
Copy link
Copy Markdown
Collaborator Author

tiran commented Apr 13, 2026

The tests are going to fail. I have not updated the test cases and test configs.

@tiran tiran force-pushed the new-resolver-config2 branch from 599609d to 976deff Compare April 13, 2026 14:22
@mergify mergify Bot added the ci label Apr 13, 2026
@tiran tiran force-pushed the new-resolver-config2 branch from 976deff to 19a9602 Compare May 5, 2026 07:44
@tiran tiran marked this pull request as ready for review May 5, 2026 07:44
@tiran tiran requested a review from a team as a code owner May 5, 2026 07:44
@tiran tiran requested a review from rd4398 May 5, 2026 07:44
Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

🧹 Nitpick comments (2)
src/fromager/packagesettings/_models.py (1)

338-342: 💤 Low value

source field missing Python-level = None — may confuse type checkers

Pydantic v2 reads the default=None from Field() inside Annotated, so runtime behavior is correct. However, unlike all other nullable fields in VariantInfo and PackageSettings (which use = None explicitly), this field has no Python-level default assignment. Static type checkers (e.g., pyright) may flag it as a required field.

♻️ Proposed fix (apply to both `VariantInfo` and `PackageSettings`)
-    source: typing.Annotated[
-        SourceResolver | None,
-        pydantic.Field(default=None, discriminator="provider"),
-    ]
+    source: typing.Annotated[
+        SourceResolver | None,
+        pydantic.Field(default=None, discriminator="provider"),
+    ] = None
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/fromager/packagesettings/_models.py` around lines 338 - 342, The
Annotated field declaration for source in _models.py (the "source" field on
VariantInfo and PackageSettings) lacks a Python-level default assignment which
can make static type checkers treat it as required; update the field declaration
to include an explicit Python default (add " = None" to the Annotated[...]
expression for the source attribute in both VariantInfo and PackageSettings)
while keeping the existing pydantic.Field(default=None,
discriminator="provider") inside the Annotated so runtime behavior remains
unchanged.
src/fromager/packagesettings/__init__.py (1)

16-30: 💤 Low value

Consider exporting SourceResolver for external type annotations

The SourceResolver discriminated union is used as the field type in VariantInfo and PackageSettings, but it's not re-exported from the package's public API. External callers who want to annotate against it would have to reach into fromager.packagesettings._resolver.

♻️ Proposed addition
 from ._resolver import (
     DEFAULT_TAG_MATCHER,
     BuildSDist,
     GitHubTagCloneResolver,
     GitHubTagDownloadResolver,
     GitLabTagCloneResolver,
     GitLabTagDownloadResolver,
     HookResolver,
     NotAvailableResolver,
     PyPIDownloadResolver,
     PyPIGitResolver,
     PyPIPrebuiltResolver,
     PyPISDistResolver,
+    SourceResolver,
     VersionMapResolver,
 )

And add "SourceResolver" to __all__.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/fromager/packagesettings/__init__.py` around lines 16 - 30, The package
does not re-export the SourceResolver discriminated union used by VariantInfo
and PackageSettings, forcing consumers to import from
fromager.packagesettings._resolver; update
src/fromager/packagesettings/__init__.py to import SourceResolver from
._resolver (alongside DEFAULT_TAG_MATCHER, BuildSDist, etc.) and add
"SourceResolver" to the module's __all__ so callers can annotate against
SourceResolver via fromager.packagesettings.SourceResolver.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@src/fromager/packagesettings/_resolver.py`:
- Around line 247-281: VersionMapResolver currently relies on
AbstractResolver.resolver_provider which raises NotImplementedError; add an
explicit override on the VersionMapResolver class that either implements the
provider or immediately raises NotImplementedError("versionmap-git not yet
implemented") to make behavior explicit (i.e., define a resolver_provider method
on VersionMapResolver that raises the clear NotImplementedError string or
implements the provider logic), referencing the VersionMapResolver class and
AbstractResolver.resolver_provider to locate where to add the
stub/implementation.
- Around line 404-432: The resolver_provider methods currently pass the literal
"FIXME" as override_download_url which silently produces broken providers;
instead, update each resolver_provider (e.g.,
GitHubTagCloneResolver.resolver_provider and
GitHubTagDownloadResolver.resolver_provider, and do the same for
GitLabTagDownloadResolver and GitLabTagCloneResolver) to fail fast by raising
NotImplementedError with a clear message like "override_download_url template
not implemented for <ResolverClassName>" rather than calling
self._github_provider(...) (or self._gitlab_provider(...)) with the "FIXME"
string.

In `@tests/test_packagesettings.py`:
- Around line 944-975: The four commented-out assertions for matcher_factory
leave test coverage incomplete for the tag-based resolver fixtures (see
TEST_GITHUB_DOWNLOAD, TEST_GITHUB_GIT, TEST_GITLAB_DOWNLOAD, TEST_GITLAB_GIT);
either uncomment the matcher_factory checks or explicitly assert the serialized
JSON omits matcher_factory depending on intended behavior: if matcher_factory
should be present assert it equals
"fromager.packagesettings:DEFAULT_TAG_MATCHER" for each of those fixtures,
otherwise update the test to assert the key is absent/None and remove the
commented lines to reflect the intended serialization.

---

Nitpick comments:
In `@src/fromager/packagesettings/__init__.py`:
- Around line 16-30: The package does not re-export the SourceResolver
discriminated union used by VariantInfo and PackageSettings, forcing consumers
to import from fromager.packagesettings._resolver; update
src/fromager/packagesettings/__init__.py to import SourceResolver from
._resolver (alongside DEFAULT_TAG_MATCHER, BuildSDist, etc.) and add
"SourceResolver" to the module's __all__ so callers can annotate against
SourceResolver via fromager.packagesettings.SourceResolver.

In `@src/fromager/packagesettings/_models.py`:
- Around line 338-342: The Annotated field declaration for source in _models.py
(the "source" field on VariantInfo and PackageSettings) lacks a Python-level
default assignment which can make static type checkers treat it as required;
update the field declaration to include an explicit Python default (add " =
None" to the Annotated[...] expression for the source attribute in both
VariantInfo and PackageSettings) while keeping the existing
pydantic.Field(default=None, discriminator="provider") inside the Annotated so
runtime behavior remains unchanged.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 42e22219-5705-4c79-b697-560c94d2a617

📥 Commits

Reviewing files that changed from the base of the PR and between bc300d4 and 19a9602.

📒 Files selected for processing (14)
  • docs/reference/config-reference.rst
  • src/fromager/packagesettings/__init__.py
  • src/fromager/packagesettings/_models.py
  • src/fromager/packagesettings/_pbi.py
  • src/fromager/packagesettings/_resolver.py
  • tests/test_packagesettings.py
  • tests/testdata/context/overrides/settings/test_github-download.yaml
  • tests/testdata/context/overrides/settings/test_github-git.yaml
  • tests/testdata/context/overrides/settings/test_gitlab-download.yaml
  • tests/testdata/context/overrides/settings/test_gitlab-git.yaml
  • tests/testdata/context/overrides/settings/test_pypidownload.yaml
  • tests/testdata/context/overrides/settings/test_pypigit.yaml
  • tests/testdata/context/overrides/settings/test_pypiprebuilt.yaml
  • tests/testdata/context/overrides/settings/test_pypisdist.yaml

Comment thread src/fromager/packagesettings/_resolver.py
Comment thread src/fromager/packagesettings/_resolver.py
Comment thread tests/test_packagesettings.py
@tiran tiran force-pushed the new-resolver-config2 branch from 19a9602 to 4c9f9fb Compare May 5, 2026 12:03
Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🧹 Nitpick comments (1)
src/fromager/packagesettings/_resolver.py (1)

519-523: HookResolver is unimplemented — offer to help

The TODO and raise NotImplementedError mean any config using provider: hook will always fail.

Happy to implement the hook-dispatch logic if you want to open a tracking issue for it.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/fromager/packagesettings/_resolver.py` around lines 519 - 523, The
resolver_provider method currently raises NotImplementedError for provider
dispatch; implement it to return the hook provider when config requests
provider: "hook" by instantiating and returning the HookResolver (or the
project's resolver.HookResolver) with the provided ctx and req_type, otherwise
delegate to the existing default provider factory/logic—replace the TODO/raise
with a conditional that constructs resolver.HookResolver(ctx, req_type) for hook
providers and falls back to the existing resolver selection path (returning an
instance of resolver.BaseProvider).
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@src/fromager/packagesettings/_resolver.py`:
- Around line 439-446: The override_download_url passed from
GitHubTagCloneResolver.resolver_provider and
GitLabTagCloneResolver.resolver_provider currently uses a plain string with
"{self.project_url}" / "{self.clone_url}" which causes str.format to look up a
nonexistent "self" key; change those literals to f-strings (prefix with f) so
self.project_url or self.clone_url is interpolated first (e.g.,
f"git+{self.project_url}@{{tagname}}" and f"git+{self.clone_url}@{{tagname}}")
before the provider calls .format(...).

---

Nitpick comments:
In `@src/fromager/packagesettings/_resolver.py`:
- Around line 519-523: The resolver_provider method currently raises
NotImplementedError for provider dispatch; implement it to return the hook
provider when config requests provider: "hook" by instantiating and returning
the HookResolver (or the project's resolver.HookResolver) with the provided ctx
and req_type, otherwise delegate to the existing default provider
factory/logic—replace the TODO/raise with a conditional that constructs
resolver.HookResolver(ctx, req_type) for hook providers and falls back to the
existing resolver selection path (returning an instance of
resolver.BaseProvider).
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 8fe16787-0813-4bdb-9b85-a04d5522f92d

📥 Commits

Reviewing files that changed from the base of the PR and between 19a9602 and 4c9f9fb.

📒 Files selected for processing (15)
  • docs/reference/config-reference.rst
  • src/fromager/packagesettings/__init__.py
  • src/fromager/packagesettings/_models.py
  • src/fromager/packagesettings/_pbi.py
  • src/fromager/packagesettings/_resolver.py
  • src/fromager/resolver.py
  • tests/test_packagesettings.py
  • tests/testdata/context/overrides/settings/test_github-download.yaml
  • tests/testdata/context/overrides/settings/test_github-git.yaml
  • tests/testdata/context/overrides/settings/test_gitlab-download.yaml
  • tests/testdata/context/overrides/settings/test_gitlab-git.yaml
  • tests/testdata/context/overrides/settings/test_pypidownload.yaml
  • tests/testdata/context/overrides/settings/test_pypigit.yaml
  • tests/testdata/context/overrides/settings/test_pypiprebuilt.yaml
  • tests/testdata/context/overrides/settings/test_pypisdist.yaml
✅ Files skipped from review due to trivial changes (11)
  • tests/testdata/context/overrides/settings/test_gitlab-download.yaml
  • tests/testdata/context/overrides/settings/test_pypidownload.yaml
  • tests/testdata/context/overrides/settings/test_gitlab-git.yaml
  • tests/testdata/context/overrides/settings/test_github-git.yaml
  • tests/testdata/context/overrides/settings/test_pypisdist.yaml
  • tests/testdata/context/overrides/settings/test_pypiprebuilt.yaml
  • src/fromager/packagesettings/_pbi.py
  • tests/testdata/context/overrides/settings/test_pypigit.yaml
  • docs/reference/config-reference.rst
  • src/fromager/packagesettings/_models.py
  • tests/test_packagesettings.py
🚧 Files skipped from review as they are similar to previous changes (2)
  • tests/testdata/context/overrides/settings/test_github-download.yaml
  • src/fromager/packagesettings/init.py

Comment thread src/fromager/packagesettings/_resolver.py
@tiran tiran force-pushed the new-resolver-config2 branch from 4c9f9fb to 5d955ff Compare May 5, 2026 12:16
Copy link
Copy Markdown
Contributor

@rd4398 rd4398 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This looks mostly okay. I have left a few comments / suggestions.
I will prefer if @LalatenduMohanty and / or @dhellmann takes a look as well

Comment thread src/fromager/packagesettings/_resolver.py Outdated
raise ValueError("package is not available")


class HookResolver(pydantic.BaseModel):
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same here,
it implements resolver_provider() with the same signature but aren't part of the type hierarchy
Can we make it inherit from AbstractResolver for consistency?

Comment thread src/fromager/packagesettings/_resolver.py Outdated
Comment thread src/fromager/packagesettings/_resolver.py Outdated
def resolver_provider(
self, ctx: context.WorkContext, req_type: requirements_file.RequirementType
) -> resolver.BaseProvider:
# TODO
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Users who configure provider: hook will get an unhelpful crash. How about we add an error message something like "hook resolver is not yet implemented"?

Comment thread src/fromager/packagesettings/_resolver.py Outdated
Introduce new configuration for source resolver and downloads. The new
system uses profiles for common tasks:

- `pypi-sdist`: resolve versions of sdists from PyPI, download sdist
- `pypi-prebuilt`: resolve versions of platform wheels from PyPI,
  download pre-built wheel from PyPI.
- `pypi-download`: resolve versions of any package from PyPI, download
  from external URL (with `{version}` variable)
- `pypi-git`: resolve versions for any package from PyPI, git clone
  (with `{version}` variable)
- `versionmap-git`: resolve with a version map, git clone
- `github-tag-download`: resolve from GitHub tag, download tarball
- `github-tag-git`: resolve from GitHub tag, git clone
- `gitlab-tag-download`: resolve from GitLab tag, download tarball
- `gitlab-tag-git`: resolve from GitLab tag, git clone
- `not-available`: block resolving
- `hook`: call Fromager hooks

The new settings will eventually replace `download_source`,
`resolver_dist`, and `git_options` top-level options as well as
`wheel_server_url` and `pre_built` flags for variants.

Signed-off-by: Christian Heimes <cheimes@redhat.com>
@tiran tiran force-pushed the new-resolver-config2 branch from 5d955ff to ac069e7 Compare May 6, 2026 12:37
Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 4

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@src/fromager/packagesettings/_models.py`:
- Around line 338-342: The new "source" field must be made mutually exclusive
with the legacy fields (download_source, resolver_dist, wheel_server_url,
pre_built): add a model-level validator (e.g., a pydantic root_validator or
`@model_validator`) on the class that declares the "source" field to check if
source is not None and any of
download_source/resolver_dist/wheel_server_url/pre_built are also set, and if so
raise a ValueError with a clear message; implement the same validator logic for
the other occurrence of the "source" field in this file so both model variants
reject mixed configs.

In `@src/fromager/packagesettings/_resolver.py`:
- Around line 376-392: The server_url built in _gitlab_provider drops an
explicit port because it uses self.project_url.host; update the server_url
construction so it preserves any configured port (e.g., use
self.project_url.port or self.project_url.netloc to include host:port) when
creating the resolver.GitLabTagProvider; keep project_path set via
self.project_url.path.lstrip("/") and pass the rest of the same arguments to
GitLabTagProvider.
- Around line 272-294: The version_map entries are being passed as raw refs
(e.g., "refs/tags/1.1") so they become Candidate.url values that aren't
fetchable and the default build_sdist=tarball doesn't match a git checkout flow;
update resolver_provider to transform version_map values that look like git refs
(e.g., start with "refs/" or match a tag/commit pattern) into concrete fetchable
Git URLs using the validated clone_url (combine clone_url and the ref into a
git+<clone_url>@<ref> URL or otherwise construct the provider-specific URL)
before creating versionmap.VersionMap(self.version_map), or use a Git-specific
provider that accepts clone_url + ref (e.g., switch to resolver.GitProvider or
pass package_name plus clone_url/ref into VersionMapProvider); also change the
class default build_sdist (build_sdist: typing.ClassVar[BuildSDist | None]) from
BuildSDist.tarball to a value consistent with git checkouts (e.g., None or the
git/checkout option) so sdist building behavior matches the git resolution flow.
- Around line 506-516: HookResolver currently advertises provider: "hook" but
leaves resolver_provider unimplemented, causing valid configs to pass validation
but crash at runtime; update HookResolver so the settings system rejects "hook"
at validation time by implementing a validation entry that raises a clear
ConfigError (or ValueError) during config parsing instead of
NotImplementedError. Locate the HookResolver class and its resolver_provider
method and either (a) remove/register it from the public SourceResolver union so
it isn't accepted, or (b) add a class-level validation hook (e.g., __post_init__
/ validate / from_config depending on your config framework) that checks
provider == "hook" and raises a configuration error with a helpful message
indicating the provider is not yet supported. Ensure references to HookResolver,
resolver_provider, and provider are used so the fix is applied in the right
place.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: f2a3dfa4-708b-48ab-a138-e0ac21eb10f5

📥 Commits

Reviewing files that changed from the base of the PR and between 4c9f9fb and ac069e7.

📒 Files selected for processing (15)
  • docs/reference/config-reference.rst
  • src/fromager/packagesettings/__init__.py
  • src/fromager/packagesettings/_models.py
  • src/fromager/packagesettings/_pbi.py
  • src/fromager/packagesettings/_resolver.py
  • src/fromager/resolver.py
  • tests/test_packagesettings.py
  • tests/testdata/context/overrides/settings/test_github-download.yaml
  • tests/testdata/context/overrides/settings/test_github-git.yaml
  • tests/testdata/context/overrides/settings/test_gitlab-download.yaml
  • tests/testdata/context/overrides/settings/test_gitlab-git.yaml
  • tests/testdata/context/overrides/settings/test_pypidownload.yaml
  • tests/testdata/context/overrides/settings/test_pypigit.yaml
  • tests/testdata/context/overrides/settings/test_pypiprebuilt.yaml
  • tests/testdata/context/overrides/settings/test_pypisdist.yaml
🚧 Files skipped from review as they are similar to previous changes (3)
  • tests/testdata/context/overrides/settings/test_github-download.yaml
  • tests/testdata/context/overrides/settings/test_pypiprebuilt.yaml
  • tests/testdata/context/overrides/settings/test_pypisdist.yaml

Comment on lines +338 to +342
source: typing.Annotated[
SourceResolver | None,
pydantic.Field(default=None, discriminator="provider"),
]
"""Source resolver and downloader"""
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Make source mutually exclusive with the legacy source settings.

These models now allow source alongside download_source, resolver_dist, wheel_server_url, and pre_built. Since src/fromager/packagesettings/_pbi.py Line 181 reads the new resolver while the older accessors still exist, mixed configs can silently disagree about how a package should be resolved or downloaded.

Proposed fix
 class VariantInfo(pydantic.BaseModel):
@@
     source: typing.Annotated[
         SourceResolver | None,
         pydantic.Field(default=None, discriminator="provider"),
     ]
     """Source resolver and downloader"""
+
+    `@pydantic.model_validator`(mode="after")
+    def validate_source_conflicts(self) -> typing.Self:
+        if self.source is not None and (
+            self.wheel_server_url is not None or self.pre_built
+        ):
+            raise ValueError(
+                "'source' cannot be combined with 'wheel_server_url' or 'pre_built'"
+            )
+        return self
@@
 class PackageSettings(pydantic.BaseModel):
@@
     source: typing.Annotated[
         SourceResolver | None,
         pydantic.Field(default=None, discriminator="provider"),
     ]
     """Source resolver and downloader"""
+
+    `@pydantic.model_validator`(mode="after")
+    def validate_source_conflicts(self) -> typing.Self:
+        if self.source is not None:
+            if (
+                self.download_source.url is not None
+                or self.download_source.destination_filename is not None
+                or self.resolver_dist != ResolverDist()
+            ):
+                raise ValueError(
+                    "'source' cannot be combined with legacy source settings"
+                )
+        return self

Also applies to: 468-472

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/fromager/packagesettings/_models.py` around lines 338 - 342, The new
"source" field must be made mutually exclusive with the legacy fields
(download_source, resolver_dist, wheel_server_url, pre_built): add a model-level
validator (e.g., a pydantic root_validator or `@model_validator`) on the class
that declares the "source" field to check if source is not None and any of
download_source/resolver_dist/wheel_server_url/pre_built are also set, and if so
raise a ValueError with a clear message; implement the same validator logic for
the other occurrence of the "source" field in this file so both model variants
reject mixed configs.

Comment on lines +272 to +294
version_map: dict[PackageVersion, str]

build_sdist: typing.ClassVar[BuildSDist | None] = BuildSDist.tarball

@pydantic.field_validator("clone_url", mode="after")
@classmethod
def validate_clone_url(cls, value: pydantic.AnyUrl) -> pydantic.AnyUrl:
if value.scheme not in {"https", "ssh"}:
raise ValueError(f"invalid scheme in url {value}")
if not value.path:
raise ValueError(f"url {value} has an empty path")
return value

def resolver_provider(
self, ctx: context.WorkContext, req_type: requirements_file.RequirementType
) -> resolver.BaseProvider:
return resolver.VersionMapProvider(
# PackageVersion is a subclass of Version
version_map=versionmap.VersionMap(self.version_map), # type: ignore[arg-type]
package_name=None,
constraints=ctx.constraints,
req_type=req_type,
)
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical | ⚡ Quick win

versionmap-git currently resolves to refs, not fetchable source URLs.

resolver.VersionMapProvider treats each map value as the final Candidate.url in src/fromager/resolver.py Line 1327, but this resolver passes raw commit/tag refs from version_map and never uses clone_url. That means a config like refs/tags/1.1 becomes the download URL, so the new profile cannot fetch sources. The default build_sdist=tarball is also inconsistent with a git checkout flow.

Proposed fix
 class VersionMapResolver(AbstractResolver):
@@
-    build_sdist: typing.ClassVar[BuildSDist | None] = BuildSDist.tarball
+    build_sdist: typing.ClassVar[BuildSDist | None] = BuildSDist.pep517
@@
     def resolver_provider(
         self, ctx: context.WorkContext, req_type: requirements_file.RequirementType
     ) -> resolver.BaseProvider:
+        mapped_urls = {
+            version: f"git+{self.clone_url}@{ref}"
+            for version, ref in self.version_map.items()
+        }
         return resolver.VersionMapProvider(
-            # PackageVersion is a subclass of Version
-            version_map=versionmap.VersionMap(self.version_map),  # type: ignore[arg-type]
+            version_map=versionmap.VersionMap(mapped_urls),
             package_name=None,
             constraints=ctx.constraints,
             req_type=req_type,
         )
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/fromager/packagesettings/_resolver.py` around lines 272 - 294, The
version_map entries are being passed as raw refs (e.g., "refs/tags/1.1") so they
become Candidate.url values that aren't fetchable and the default
build_sdist=tarball doesn't match a git checkout flow; update resolver_provider
to transform version_map values that look like git refs (e.g., start with
"refs/" or match a tag/commit pattern) into concrete fetchable Git URLs using
the validated clone_url (combine clone_url and the ref into a
git+<clone_url>@<ref> URL or otherwise construct the provider-specific URL)
before creating versionmap.VersionMap(self.version_map), or use a Git-specific
provider that accepts clone_url + ref (e.g., switch to resolver.GitProvider or
pass package_name plus clone_url/ref into VersionMapProvider); also change the
class default build_sdist (build_sdist: typing.ClassVar[BuildSDist | None]) from
BuildSDist.tarball to a value consistent with git checkouts (e.g., None or the
git/checkout option) so sdist building behavior matches the git resolution flow.

Comment on lines +376 to +392
def _gitlab_provider(
self,
*,
ctx: context.WorkContext,
req_type: requirements_file.RequirementType,
override_download_url: str | None = None,
) -> resolver.GitLabTagProvider:
if not self.project_url.path:
raise ValueError(f"Empty path in {self.project_url}")
return resolver.GitLabTagProvider(
project_path=self.project_url.path.lstrip("/"),
server_url=f"https://{self.project_url.host}",
constraints=ctx.constraints,
matcher=self._get_matcher(ctx),
req_type=req_type,
override_download_url=override_download_url,
)
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Preserve the configured GitLab port when building server_url.

self.project_url.host drops any explicit port. A config like https://gitlab.example:8443/group/proj will therefore query https://gitlab.example/api/v4/... and generate tarball URLs on the wrong endpoint.

Proposed fix
         return resolver.GitLabTagProvider(
             project_path=self.project_url.path.lstrip("/"),
-            server_url=f"https://{self.project_url.host}",
+            server_url=(
+                f"https://{self.project_url.host}:{self.project_url.port}"
+                if self.project_url.port is not None
+                else f"https://{self.project_url.host}"
+            ),
             constraints=ctx.constraints,
             matcher=self._get_matcher(ctx),
             req_type=req_type,
             override_download_url=override_download_url,
         )
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/fromager/packagesettings/_resolver.py` around lines 376 - 392, The
server_url built in _gitlab_provider drops an explicit port because it uses
self.project_url.host; update the server_url construction so it preserves any
configured port (e.g., use self.project_url.port or self.project_url.netloc to
include host:port) when creating the resolver.GitLabTagProvider; keep
project_path set via self.project_url.path.lstrip("/") and pass the rest of the
same arguments to GitLabTagProvider.

Comment on lines +506 to +516
class HookResolver(AbstractResolver):
"""Call resolver_provider and download_source hook"""

provider: typing.Literal["hook"]

def resolver_provider(
self, ctx: context.WorkContext, req_type: requirements_file.RequirementType
) -> resolver.BaseProvider:
# TODO
raise NotImplementedError("Hook resolver needs a hook")

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Don’t accept provider: hook until the resolver exists.

This class is part of the public SourceResolver union, so config validation succeeds, but every use still ends in NotImplementedError. That turns a valid-looking settings file into a runtime crash instead of a validation error.

Proposed fix
 class HookResolver(AbstractResolver):
     """Call resolver_provider and download_source hook"""

     provider: typing.Literal["hook"]

+    `@pydantic.model_validator`(mode="after")
+    def validate_supported(self) -> typing.Self:
+        raise ValueError("'hook' source resolver is not implemented yet")
+
     def resolver_provider(
         self, ctx: context.WorkContext, req_type: requirements_file.RequirementType
     ) -> resolver.BaseProvider:
-        # TODO
-        raise NotImplementedError("Hook resolver needs a hook")
+        raise AssertionError("unreachable")
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/fromager/packagesettings/_resolver.py` around lines 506 - 516,
HookResolver currently advertises provider: "hook" but leaves resolver_provider
unimplemented, causing valid configs to pass validation but crash at runtime;
update HookResolver so the settings system rejects "hook" at validation time by
implementing a validation entry that raises a clear ConfigError (or ValueError)
during config parsing instead of NotImplementedError. Locate the HookResolver
class and its resolver_provider method and either (a) remove/register it from
the public SourceResolver union so it isn't accepted, or (b) add a class-level
validation hook (e.g., __post_init__ / validate / from_config depending on your
config framework) that checks provider == "hook" and raises a configuration
error with a helpful message indicating the provider is not yet supported.
Ensure references to HookResolver, resolver_provider, and provider are used so
the fix is applied in the right place.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants