Skip to content
Draft
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
10 changes: 10 additions & 0 deletions docs/proposals/new-resolver-config.md
Original file line number Diff line number Diff line change
Expand Up @@ -151,6 +151,16 @@ variants:
- The `hook-prebuilt` profile is similar to `hook-sdist` but the downloaded
artifact must be a pre-built wheel.

- The `legacy` profile preserves the old resolution and download behavior
as a typed resolver. It reads `resolver_dist` settings from the per-package
build info, invokes the `resolver_provider` and `download_source`
[hooks](../reference/hooks.rst) (falling back to their defaults), and
applies the per-package release-age cooldown. When `pre_built` is true for
the variant, the download returns a pre-built wheel; otherwise it returns
a tarball via the `download_source` hook. This profile is useful for
gradual migration: packages without an explicit typed resolver can use
`provider: legacy` to keep the existing behavior.

- The `not-available` profile raises an error. It can be used to block a
package and only enable it for a single variant.

Expand Down
3 changes: 3 additions & 0 deletions docs/reference/config-reference.rst
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,9 @@ Source Resolver
:inherited-members: AbstractGitSourceResolver, CooldownMixin
:members: download_kinds, supports_override_hooks, resolves_prebuilt_wheel

.. autopydantic_model:: LegacyResolver
:members: download_kinds, supports_override_hooks

.. autopydantic_model:: NotAvailableResolver
:members: download_kinds, supports_override_hooks, resolves_prebuilt_wheel

Expand Down
2 changes: 2 additions & 0 deletions src/fromager/packagesettings/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@
GitLabTagDownloadResolver,
HookPrebuiltResolver,
HookSDistResolver,
LegacyResolver,
NotAvailableResolver,
PyPIDownloadResolver,
PyPIGitResolver,
Expand Down Expand Up @@ -65,6 +66,7 @@
"GlobalChangelog",
"HookPrebuiltResolver",
"HookSDistResolver",
"LegacyResolver",
"NotAvailableResolver",
"Package",
"PackageBuildInfo",
Expand Down
102 changes: 100 additions & 2 deletions src/fromager/packagesettings/_resolver.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@

import pydantic

from .. import downloads, resolver
from .. import downloads, overrides, resolver
from ..candidate import Cooldown
from ._typedefs import MODEL_CONFIG

Expand Down Expand Up @@ -796,6 +796,103 @@ class HookPrebuiltResolver(AbstractHookResolver):
resolves_prebuilt_wheel: typing.ClassVar[bool] = True


class LegacyResolver(AbstractResolver):
"""Preserve the legacy resolution and download behavior as a typed resolver.

The ``legacy`` provider replicates the flow of
``sources.get_source_provider()`` and ``sources.download_source()``:
it reads ``resolver_dist`` settings from the per-package build info,
invokes the ``get_resolver_provider`` and ``download_source`` override
hooks (falling back to their defaults), and applies the per-package
release-age cooldown.

When ``pbi.pre_built`` is true the download returns a pre-built wheel;
otherwise it returns a tarball via the ``download_source`` hook.

Example::

provider: legacy
"""

provider: typing.Literal["legacy"]

supports_override_hooks: typing.ClassVar[bool] = True
download_kinds: typing.ClassVar[DownloadKindSet] = frozenset(
{DownloadKind.tarball, DownloadKind.prebuilt_wheel}
)

def resolver_provider(
self,
ctx: context.WorkContext,
req: Requirement,
req_type: requirements_file.RequirementType,
) -> resolver.BaseProvider:
"""Return a resolver provider using the legacy resolution flow.

Replicates ``sources.get_source_provider()``.
"""
pbi = ctx.package_build_info(req)
sdist_server_url = pbi.resolver_sdist_server_url(resolver.PYPI_SERVER_URL)

provider = typing.cast(
resolver.BaseProvider,
overrides.find_and_invoke(
req.name,
"get_resolver_provider",
resolver.default_resolver_provider,
ctx=ctx,
req=req,
include_sdists=pbi.resolver_include_sdists,
include_wheels=pbi.resolver_include_wheels,
sdist_server_url=sdist_server_url,
req_type=req_type,
ignore_platform=pbi.resolver_ignore_platform,
),
)
provider.cooldown = resolver.resolve_package_cooldown(
ctx, req, req_type=req_type
)
return provider

def download(
self,
ctx: context.WorkContext,
req: Requirement,
candidate: Candidate,
) -> tuple[pathlib.Path, DownloadKind]:
"""Download using the legacy flow.

Delegates to the pre-built wheel downloader when the package
variant is configured as pre-built (see
``bootstrapper._bg_prepare_prebuilt()``), otherwise invokes the
``download_source`` override hook (see
``sources.download_source()``).
"""
pbi = ctx.package_build_info(req)
if pbi.pre_built:
return self._download(ctx, req, candidate, DownloadKind.prebuilt_wheel)

# Local import required to avoid circular import:
# _resolver -> sources -> packagesettings -> _resolver
from fromager.sources import default_download_source

source_path = overrides.find_and_invoke(
req.name,
"download_source",
default_download_source,
ctx=ctx,
req=req,
version=candidate.version,
download_url=candidate.url,
sdists_downloads_dir=ctx.sdists_downloads,
)
if not isinstance(source_path, pathlib.Path):
raise ValueError(
f"expected a Path back to downloaded source, got {source_path}"
)
return source_path, DownloadKind.tarball


SourceResolver = typing.Annotated[
PyPISDistResolver
| PyPIPrebuiltResolver
Expand All @@ -807,6 +904,7 @@ class HookPrebuiltResolver(AbstractHookResolver):
| GitLabTagDownloadResolver
| NotAvailableResolver
| HookSDistResolver
| HookPrebuiltResolver,
| HookPrebuiltResolver
| LegacyResolver,
pydantic.Field(..., discriminator="provider"),
]
65 changes: 65 additions & 0 deletions tests/test_packagesettings_resolver.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@
GitLabTagDownloadResolver,
HookPrebuiltResolver,
HookSDistResolver,
LegacyResolver,
NotAvailableResolver,
PyPIDownloadResolver,
PyPIGitResolver,
Expand Down Expand Up @@ -715,6 +716,70 @@ def test_download(self, tmp_context: WorkContext) -> None:
r.download(tmp_context, _REQ, _CANDIDATE_WHEEL)


class TestLegacyResolver:
YAML = """\
source:
provider: legacy
"""

def test_parse(self) -> None:
r = _parse(self.YAML)
assert isinstance(r, LegacyResolver)
assert r.provider == "legacy"
assert r.supports_override_hooks is True
assert r.resolves_prebuilt_wheel is False
assert r.download_kinds == frozenset(
{DownloadKind.tarball, DownloadKind.prebuilt_wheel}
)

def test_resolver_provider(self, tmp_context: WorkContext) -> None:
r = _parse(self.YAML)
p = r.resolver_provider(tmp_context, _REQ, _REQ_TYPE)
assert isinstance(p, resolver.PyPIProvider)
assert p.include_sdists is True
assert p.include_wheels is False
assert p.sdist_server_url == "https://pypi.org/simple"
assert p.ignore_platform is False

@mock.patch("fromager.overrides.find_and_invoke")
def test_download_source(
self, mock_invoke: mock.MagicMock, tmp_context: WorkContext
) -> None:
expected = tmp_context.sdists_downloads / "test-pkg-1.2.3.tar.gz"
mock_invoke.return_value = expected
r = _parse(self.YAML)
path, kind = r.download(tmp_context, _REQ, _CANDIDATE_SDIST)
assert path == expected
assert kind is DownloadKind.tarball
mock_invoke.assert_called_once()
call_args = mock_invoke.call_args
assert call_args[0][0] == _REQ.name
assert call_args[0][1] == "download_source"
assert call_args[1]["version"] == _VERSION
assert call_args[1]["download_url"] == _CANDIDATE_SDIST.url
assert call_args[1]["sdists_downloads_dir"] == tmp_context.sdists_downloads

@mock.patch("fromager.downloads.download_wheel")
def test_download_prebuilt(
self, mock_dl: mock.MagicMock, tmp_context: WorkContext
) -> None:
expected = tmp_context.wheels_prebuilt / "test_pkg-1.2.3-py3-none-any.whl"
mock_dl.return_value = expected
# Configure pbi.pre_built to return True
pbi = tmp_context.package_build_info(_REQ)
with mock.patch.object(
type(pbi), "pre_built", new_callable=lambda: property(lambda self: True)
):
r = _parse(self.YAML)
path, kind = r.download(tmp_context, _REQ, _CANDIDATE_WHEEL)
assert path == expected
assert kind is DownloadKind.prebuilt_wheel
mock_dl.assert_called_once_with(
destination_dir=tmp_context.wheels_prebuilt,
url=_CANDIDATE_WHEEL.url,
)


# -- Discriminated union validation -------------------------------------------


Expand Down
Loading