Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 9 additions & 3 deletions docs/concepts/resolver-architecture.rst
Original file line number Diff line number Diff line change
Expand Up @@ -90,10 +90,16 @@ CLI commands interact with providers through a common
Used when versions are already known, e.g. from a prior
resolution or a settings-provided URL template.

.. versionchanged:: 0.95.0
Packages with ``source:`` configuration now select their provider through
the configured source resolver.

Per-package settings in YAML can select which provider to use and
configure its parameters (index URL, tag pattern, etc.). Override
plugins can replace the provider entirely for a specific package via
the ``get_resolver_provider`` hook.
configure its parameters (index URL, tag pattern, etc.). When a package
has a ``source:`` resolver configured, that resolver creates the provider.
Otherwise, override plugins can replace the provider for a specific package
via the ``get_resolver_provider`` hook, after which the legacy resolver
settings are used.

Version Filtering Window
-------------------------
Expand Down
26 changes: 13 additions & 13 deletions src/fromager/packagesettings/_resolver.py
Original file line number Diff line number Diff line change
Expand Up @@ -67,7 +67,7 @@ def resolver_provider(
self,
ctx: context.WorkContext,
req: Requirement,
req_type: requirements_file.RequirementType,
req_type: requirements_file.RequirementType | None,
) -> resolver.BaseProvider:
"""Return a resolver provider for the given requirement."""
raise NotImplementedError
Expand Down Expand Up @@ -198,7 +198,7 @@ def resolver_provider(
self,
ctx: context.WorkContext,
req: Requirement,
req_type: requirements_file.RequirementType,
req_type: requirements_file.RequirementType | None,
) -> resolver.PyPIProvider:
return resolver.PyPIProvider(
include_sdists=True,
Expand Down Expand Up @@ -249,7 +249,7 @@ def resolver_provider(
self,
ctx: context.WorkContext,
req: Requirement,
req_type: requirements_file.RequirementType,
req_type: requirements_file.RequirementType | None,
) -> resolver.PyPIProvider:
return resolver.PyPIProvider(
include_sdists=False,
Expand Down Expand Up @@ -322,7 +322,7 @@ def resolver_provider(
self,
ctx: context.WorkContext,
req: Requirement,
req_type: requirements_file.RequirementType,
req_type: requirements_file.RequirementType | None,
) -> resolver.PyPIProvider:
return resolver.PyPIProvider(
include_sdists=True,
Expand Down Expand Up @@ -404,7 +404,7 @@ def resolver_provider(
self,
ctx: context.WorkContext,
req: Requirement,
req_type: requirements_file.RequirementType,
req_type: requirements_file.RequirementType | None,
) -> resolver.PyPIProvider:
download_url = f"git+{self.clone_url}@refs/tags/{self.tag}"
return resolver.PyPIProvider(
Expand Down Expand Up @@ -510,7 +510,7 @@ def _github_provider(
self,
*,
ctx: context.WorkContext,
req_type: requirements_file.RequirementType,
req_type: requirements_file.RequirementType | None,
override_download_url: str | None = None,
) -> resolver.GitHubTagProvider:
if self.project_url.host != "github.com":
Expand All @@ -534,7 +534,7 @@ def _gitlab_provider(
self,
*,
ctx: context.WorkContext,
req_type: requirements_file.RequirementType,
req_type: requirements_file.RequirementType | None,
override_download_url: str | None = None,
) -> resolver.GitLabTagProvider:
if not self.project_url.path:
Expand Down Expand Up @@ -571,7 +571,7 @@ def resolver_provider(
self,
ctx: context.WorkContext,
req: Requirement,
req_type: requirements_file.RequirementType,
req_type: requirements_file.RequirementType | None,
) -> resolver.GitHubTagProvider:
return self._github_provider(
ctx=ctx,
Expand Down Expand Up @@ -612,7 +612,7 @@ def resolver_provider(
self,
ctx: context.WorkContext,
req: Requirement,
req_type: requirements_file.RequirementType,
req_type: requirements_file.RequirementType | None,
) -> resolver.GitHubTagProvider:
return self._github_provider(
ctx=ctx,
Expand Down Expand Up @@ -651,7 +651,7 @@ def resolver_provider(
self,
ctx: context.WorkContext,
req: Requirement,
req_type: requirements_file.RequirementType,
req_type: requirements_file.RequirementType | None,
) -> resolver.GitLabTagProvider:
return self._gitlab_provider(
ctx=ctx,
Expand Down Expand Up @@ -692,7 +692,7 @@ def resolver_provider(
self,
ctx: context.WorkContext,
req: Requirement,
req_type: requirements_file.RequirementType,
req_type: requirements_file.RequirementType | None,
) -> resolver.GitLabTagProvider:
return self._gitlab_provider(
ctx=ctx,
Expand All @@ -719,7 +719,7 @@ def resolver_provider(
self,
ctx: context.WorkContext,
req: Requirement,
req_type: requirements_file.RequirementType,
req_type: requirements_file.RequirementType | None,
) -> resolver.BaseProvider:
raise ValueError(f"package {req.name} is not available")

Expand All @@ -743,7 +743,7 @@ def resolver_provider(
self,
ctx: context.WorkContext,
req: Requirement,
req_type: requirements_file.RequirementType,
req_type: requirements_file.RequirementType | None,
) -> resolver.BaseProvider:
# TODO
raise NotImplementedError("Hook resolver needs a hook")
Expand Down
47 changes: 31 additions & 16 deletions src/fromager/resolver.py
Original file line number Diff line number Diff line change
Expand Up @@ -157,6 +157,36 @@ def _has_equality_pin(req: Requirement) -> bool:
return len(specs) == 1 and specs[0].operator == "==" and "*" not in specs[0].version


def _effective_cooldown(
global_cooldown: Cooldown | None,
per_package_days: int | None,
) -> Cooldown | None:
"""Merge a per-package cooldown override with the global cooldown.

Args:
global_cooldown: The global cooldown from ``ctx.cooldown``.
per_package_days: Per-package override in days — ``None`` inherits
the global value, ``0`` disables cooldown, a positive integer
overrides it.

Returns:
The effective cooldown, or ``None`` if disabled.
"""
if per_package_days is None:
return global_cooldown
if per_package_days == 0:
return None
bootstrap_time = (
global_cooldown.bootstrap_time
if global_cooldown is not None
else datetime.datetime.now(datetime.UTC)
)
return Cooldown(
min_age=datetime.timedelta(days=per_package_days),
bootstrap_time=bootstrap_time,
)


def resolve_package_cooldown(
ctx: context.WorkContext,
req: Requirement,
Expand All @@ -178,22 +208,7 @@ def resolve_package_cooldown(
return None

per_package_days = ctx.package_build_info(req).resolver_min_release_age
global_cooldown = ctx.cooldown
if per_package_days is None:
return global_cooldown
if per_package_days == 0:
return None
# Per-package positive override: inherit bootstrap_time from global so all
# resolutions in a single run share the same fixed cutoff point.
bootstrap_time = (
global_cooldown.bootstrap_time
if global_cooldown is not None
else datetime.datetime.now(datetime.UTC)
)
return Cooldown(
min_age=datetime.timedelta(days=per_package_days),
bootstrap_time=bootstrap_time,
)
return _effective_cooldown(ctx.cooldown, per_package_days)


def _compute_max_age_cutoff(
Expand Down
17 changes: 17 additions & 0 deletions src/fromager/sources.py
Original file line number Diff line number Diff line change
Expand Up @@ -116,8 +116,25 @@ def get_source_provider(

Returns a provider configured according to the package's resolver settings
(sdist/wheel inclusion, platform matching, server URL override).

Packages with a ``source:`` resolver use that resolver's provider
configuration. Packages without one retain the legacy override-hook and
``resolver_dist`` behavior.
"""
pbi = ctx.package_build_info(req)

source_resolver = pbi.source_resolver
if source_resolver is not None:
provider = source_resolver.resolver_provider(ctx, req, req_type)
if req_type == RequirementType.TOP_LEVEL and resolver._has_equality_pin(req):
provider.cooldown = None
else:
per_package_days = getattr(source_resolver, "min_release_age", None)
provider.cooldown = resolver._effective_cooldown(
ctx.cooldown, per_package_days
)
return provider

override_sdist_server_url = pbi.resolver_sdist_server_url(sdist_server_url)

provider = typing.cast(
Expand Down
23 changes: 16 additions & 7 deletions tests/test_bootstrap_requirement_resolver.py
Original file line number Diff line number Diff line change
Expand Up @@ -494,23 +494,32 @@ def test_resolve_auto_routes_to_source(
mock_pbi.resolver_min_release_age = None

with patch.object(tmp_context, "package_build_info", return_value=mock_pbi):
resolver = BootstrapRequirementResolver(tmp_context)
brr = BootstrapRequirementResolver(tmp_context)

# Mock source resolution to return expected result (as list)
mock_resolve.return_value = [
("https://files.pythonhosted.org/mypackage-2.0.tar.gz", Version("2.0"))
]

# Call resolve with pre_built=None (should auto-detect)
results = resolver.resolve(
req=req,
req_type=RequirementType.INSTALL,
parent_req=None,
pre_built=None,
)
with patch(
"fromager.bootstrap_requirement_resolver.sources.get_source_provider"
) as get_source_provider:
results = brr.resolve(
req=req,
req_type=RequirementType.INSTALL,
parent_req=None,
pre_built=None,
)

# Verify resolution was called
mock_resolve.assert_called_once()
get_source_provider.assert_called_once_with(
ctx=tmp_context,
req=req,
sdist_server_url="https://pypi.org/simple",
req_type=RequirementType.INSTALL,
)
assert len(results) == 1
url, version = results[0]
assert url == "https://files.pythonhosted.org/mypackage-2.0.tar.gz"
Expand Down
Loading
Loading