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
8 changes: 8 additions & 0 deletions docs/concepts/resolver-architecture.rst
Original file line number Diff line number Diff line change
Expand Up @@ -94,9 +94,17 @@ CLI commands interact with providers through a common
Packages with ``source:`` configuration now select their provider through
the configured source resolver.

.. versionchanged:: 0.96.0
The ``hook-sdist`` and ``hook-prebuilt`` profiles create providers through
the required ``get_resolver_provider`` override hook. They do not fall back
to PyPI, and hook-backed artifact downloading remains a separate feature.

Per-package settings in YAML can select which provider to use and
configure its parameters (index URL, tag pattern, etc.). When a package
has a ``source:`` resolver configured, that resolver creates the provider.
The ``hook-sdist`` and ``hook-prebuilt`` profiles delegate provider creation
to the package's required ``get_resolver_provider`` hook and validate that it
returns a :class:`~fromager.resolver.BaseProvider`.
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.
Expand Down
29 changes: 25 additions & 4 deletions docs/reference/hooks.rst
Original file line number Diff line number Diff line change
Expand Up @@ -138,10 +138,28 @@ Resolver hooks
The arguments are the ``WorkContext``, the ``Requirement`` being
evaluated, a boolean indicating whether source distributions should be
included, a boolean indicating whether built wheels should be
included, and the URL for the sdist server.
included, and the URL for the sdist server. The hook also receives
``req_type`` and ``ignore_platform`` when those parameters are supported
by its signature.

The return value must be an instance of a class that implements the
``resolvelib.providers.AbstractProvider`` API.
The ``hook-sdist`` and ``hook-prebuilt`` source profiles require this hook.
They pass the following keyword arguments:

* ``ctx``
* ``req``
* ``include_sdists``
* ``include_wheels``
* ``sdist_server_url``
* ``req_type``
* ``ignore_platform``

Older hooks may omit newer arguments; unsupported keyword arguments are
filtered for compatibility. A missing hook or a return value that is not a
:class:`~fromager.resolver.BaseProvider` is an error. Hook profiles never
fall back to the default PyPI provider.

The return value must be an instance of
:class:`~fromager.resolver.BaseProvider`.

The expectation is that it acts as an engine for any sort of package resolution
whether it is for wheels or sources. The provider can
Expand Down Expand Up @@ -169,7 +187,10 @@ Resolver hooks
return VERSIONS.items()


def get_resolver_provider(ctx, req, include_sdists, include_wheels, sdist_server_url):
def get_resolver_provider(
ctx, req, include_sdists, include_wheels, sdist_server_url,
req_type=None, ignore_platform=False,
):
return resolver.GenericProvider(version_source=_version_source, constraints=ctx.constraints)

``GenericProvider``, ``GitHubTagProvider``, and ``GitLabTagProvider`` take
Expand Down
108 changes: 105 additions & 3 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 @@ -68,6 +68,8 @@ def resolver_provider(
ctx: context.WorkContext,
req: Requirement,
req_type: requirements_file.RequirementType | None,
*,
sdist_server_url: str = resolver.PYPI_SERVER_URL,
) -> resolver.BaseProvider:
"""Return a resolver provider for the given requirement."""
raise NotImplementedError
Expand Down Expand Up @@ -199,6 +201,8 @@ def resolver_provider(
ctx: context.WorkContext,
req: Requirement,
req_type: requirements_file.RequirementType | None,
*,
sdist_server_url: str = resolver.PYPI_SERVER_URL,
) -> resolver.PyPIProvider:
return resolver.PyPIProvider(
include_sdists=True,
Expand Down Expand Up @@ -250,6 +254,8 @@ def resolver_provider(
ctx: context.WorkContext,
req: Requirement,
req_type: requirements_file.RequirementType | None,
*,
sdist_server_url: str = resolver.PYPI_SERVER_URL,
) -> resolver.PyPIProvider:
return resolver.PyPIProvider(
include_sdists=False,
Expand Down Expand Up @@ -323,6 +329,8 @@ def resolver_provider(
ctx: context.WorkContext,
req: Requirement,
req_type: requirements_file.RequirementType | None,
*,
sdist_server_url: str = resolver.PYPI_SERVER_URL,
) -> resolver.PyPIProvider:
return resolver.PyPIProvider(
include_sdists=True,
Expand Down Expand Up @@ -405,6 +413,8 @@ def resolver_provider(
ctx: context.WorkContext,
req: Requirement,
req_type: requirements_file.RequirementType | None,
*,
sdist_server_url: str = resolver.PYPI_SERVER_URL,
) -> resolver.PyPIProvider:
download_url = f"git+{self.clone_url}@refs/tags/{self.tag}"
return resolver.PyPIProvider(
Expand Down Expand Up @@ -572,6 +582,8 @@ def resolver_provider(
ctx: context.WorkContext,
req: Requirement,
req_type: requirements_file.RequirementType | None,
*,
sdist_server_url: str = resolver.PYPI_SERVER_URL,
) -> resolver.GitHubTagProvider:
return self._github_provider(
ctx=ctx,
Expand Down Expand Up @@ -613,6 +625,8 @@ def resolver_provider(
ctx: context.WorkContext,
req: Requirement,
req_type: requirements_file.RequirementType | None,
*,
sdist_server_url: str = resolver.PYPI_SERVER_URL,
) -> resolver.GitHubTagProvider:
return self._github_provider(
ctx=ctx,
Expand Down Expand Up @@ -652,6 +666,8 @@ def resolver_provider(
ctx: context.WorkContext,
req: Requirement,
req_type: requirements_file.RequirementType | None,
*,
sdist_server_url: str = resolver.PYPI_SERVER_URL,
) -> resolver.GitLabTagProvider:
return self._gitlab_provider(
ctx=ctx,
Expand Down Expand Up @@ -693,6 +709,8 @@ def resolver_provider(
ctx: context.WorkContext,
req: Requirement,
req_type: requirements_file.RequirementType | None,
*,
sdist_server_url: str = resolver.PYPI_SERVER_URL,
) -> resolver.GitLabTagProvider:
return self._gitlab_provider(
ctx=ctx,
Expand Down Expand Up @@ -720,6 +738,8 @@ def resolver_provider(
ctx: context.WorkContext,
req: Requirement,
req_type: requirements_file.RequirementType | None,
*,
sdist_server_url: str = resolver.PYPI_SERVER_URL,
) -> resolver.BaseProvider:
raise ValueError(f"package {req.name} is not available")

Expand All @@ -739,14 +759,58 @@ class AbstractHookResolver(AbstractResolver, CooldownMixin):
supports_override_hooks: typing.ClassVar[bool] = True
"""Hook resolvers support override hooks."""

def _resolver_provider_from_hook(
self,
*,
ctx: context.WorkContext,
req: Requirement,
req_type: requirements_file.RequirementType | None,
sdist_server_url: str,
include_sdists: bool,
include_wheels: bool,
ignore_platform: bool,
) -> resolver.BaseProvider:
"""Invoke the required package resolver hook and validate its result."""
hook = overrides.find_override_method(req.name, "get_resolver_provider")
if hook is None:
raise ValueError(
f"{req.name}: source resolver {self.provider!r} requires a "
"get_resolver_provider override hook"
)

try:
provider = overrides.invoke(
hook,
ctx=ctx,
req=req,
include_sdists=include_sdists,
include_wheels=include_wheels,
sdist_server_url=sdist_server_url,
req_type=req_type,
ignore_platform=ignore_platform,
)
except Exception as err:
raise RuntimeError(
f"{req.name}: {self.provider!r} get_resolver_provider hook failed"
) from err

if not isinstance(provider, resolver.BaseProvider):
raise TypeError(
f"{req.name}: {self.provider!r} get_resolver_provider hook "
f"returned {type(provider).__name__}, expected "
"fromager.resolver.BaseProvider"
)
return provider

def resolver_provider(
self,
ctx: context.WorkContext,
req: Requirement,
req_type: requirements_file.RequirementType | None,
*,
sdist_server_url: str = resolver.PYPI_SERVER_URL,
) -> resolver.BaseProvider:
# TODO
raise NotImplementedError("Hook resolver needs a hook")
raise NotImplementedError

def download(
self,
Expand Down Expand Up @@ -776,6 +840,25 @@ class HookSDistResolver(AbstractHookResolver):
{DownloadKind.sdist, DownloadKind.tarball, DownloadKind.git_checkout}
)

def resolver_provider(
self,
ctx: context.WorkContext,
req: Requirement,
req_type: requirements_file.RequirementType | None,
*,
sdist_server_url: str = resolver.PYPI_SERVER_URL,
) -> resolver.BaseProvider:
"""Return a source provider from the package resolver hook."""
return self._resolver_provider_from_hook(
ctx=ctx,
req=req,
req_type=req_type,
sdist_server_url=sdist_server_url,
include_sdists=True,
include_wheels=False,
ignore_platform=False,
)


class HookPrebuiltResolver(AbstractHookResolver):
"""Call resolver_provider and download_source hook, use pre-built wheel
Expand All @@ -795,6 +878,25 @@ class HookPrebuiltResolver(AbstractHookResolver):
)
resolves_prebuilt_wheel: typing.ClassVar[bool] = True

def resolver_provider(
self,
ctx: context.WorkContext,
req: Requirement,
req_type: requirements_file.RequirementType | None,
*,
sdist_server_url: str = resolver.PYPI_SERVER_URL,
) -> resolver.BaseProvider:
"""Return a pre-built wheel provider from the package resolver hook."""
return self._resolver_provider_from_hook(
ctx=ctx,
req=req,
req_type=req_type,
sdist_server_url=sdist_server_url,
include_sdists=False,
include_wheels=True,
ignore_platform=False,
)


SourceResolver = typing.Annotated[
PyPISDistResolver
Expand Down
7 changes: 6 additions & 1 deletion src/fromager/sources.py
Original file line number Diff line number Diff line change
Expand Up @@ -125,7 +125,12 @@ def get_source_provider(

source_resolver = pbi.source_resolver
if source_resolver is not None:
provider = source_resolver.resolver_provider(ctx, req, req_type)
provider = source_resolver.resolver_provider(
ctx,
req,
req_type,
sdist_server_url=sdist_server_url,
)
if req_type == RequirementType.TOP_LEVEL and resolver._has_equality_pin(req):
provider.cooldown = None
else:
Expand Down
Loading
Loading