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
11 changes: 9 additions & 2 deletions docs/concepts/resolver-architecture.rst
Original file line number Diff line number Diff line change
Expand Up @@ -107,15 +107,22 @@ CLI commands interact with providers through a common
Hook profiles pass their ``index_url`` to the legacy
``get_resolver_provider`` hook as ``sdist_server_url``. ``hook-sdist``
defaults to PyPI, while ``hook-prebuilt`` requires an explicit index.
A hook profile without a ``get_resolver_provider`` hook finds versions on
its ``index_url`` and uses the ``download_source`` hook.

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`.
to the package's ``get_resolver_provider`` hook when it exists and validate
that it returns a :class:`~fromager.resolver.BaseProvider`.
The hook may ignore or replace the profile's index URL, but the profile's
value is stable across Fromager commands.
Without that hook, the profiles find versions on ``index_url``:
``hook-sdist`` looks at sdists and wheels for any platform, like
``pypi-git``, and ``hook-prebuilt`` looks at wheels for this platform, like
``pypi-prebuilt``. The package's ``download_source`` hook then gets the
artifact. A package with neither hook is an error.
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
9 changes: 7 additions & 2 deletions docs/proposals/new-resolver-config.md
Original file line number Diff line number Diff line change
Expand Up @@ -147,12 +147,17 @@ variants:
[hooks](../reference/hooks.rst). The `resolver_provider` hook receives the
`req` argument for access to the current requirement and
`sdist_server_url=str(index_url)`. `index_url` defaults to PyPI. Hooks can
return any source artifact (sdist, tarball, or git checkout).
return any source artifact (sdist, tarball, or git checkout). Without a
`resolver_provider` hook, Fromager finds versions on `index_url` in sdists
and wheels for any platform, and the `download_source` hook gets the
source. A package needs at least one of the two hooks.

- The `hook-prebuilt` profile is similar to `hook-sdist` but the downloaded
artifact must be a pre-built wheel. Its required `index_url` replaces the
legacy variant `wheel_server_url`; the hook receives it as
`sdist_server_url` and may use, ignore, or replace it.
`sdist_server_url` and may use, ignore, or replace it. Without a
`resolver_provider` hook, Fromager finds versions on `index_url` in
wheels for this platform.

- 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
30 changes: 23 additions & 7 deletions docs/reference/hooks.rst
Original file line number Diff line number Diff line change
Expand Up @@ -142,8 +142,8 @@ Resolver hooks
``req_type`` and ``ignore_platform`` when those parameters are supported
by its signature.

The ``hook-sdist`` and ``hook-prebuilt`` source profiles require this hook.
They pass the following keyword arguments:
The ``hook-sdist`` and ``hook-prebuilt`` source profiles call this hook
when it exists. They pass the following keyword arguments:

* ``ctx``
* ``req``
Expand All @@ -154,9 +154,17 @@ Resolver hooks
* ``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.
filtered for compatibility. A return value that is not a
:class:`~fromager.resolver.BaseProvider` is an error.

Without this hook, a hook profile finds versions on its ``index_url`` and
the ``download_source`` hook gets the artifact. A package with neither
hook is an error; use ``pypi-sdist`` or ``pypi-prebuilt`` instead.

.. versionchanged:: 0.102.0

``hook-sdist`` and ``hook-prebuilt`` no longer require this hook when
the package has a ``download_source`` hook.

For a configured hook profile, ``sdist_server_url`` is the profile's
``index_url``. ``hook-sdist`` defaults that field to PyPI;
Expand Down Expand Up @@ -275,8 +283,16 @@ Source hooks
URL as an sdist into ``ctx.sdists_downloads``. ``hook-prebuilt`` downloads
it as a wheel into ``ctx.wheels_prebuilt``. These defaults use the
standard archive and wheel validation. Custom tarball URLs and git
downloads require a download hook. The ``get_resolver_provider`` hook
remains required for both profiles.
downloads require a download hook.

Without a ``get_resolver_provider`` hook, ``hook-sdist`` finds
versions on its ``index_url`` in sdists and wheels for any platform,
and ``hook-prebuilt`` finds them in wheels for this platform. The
download hook then gets the artifact. ``download_url`` is the URL of
a matching file on the index, which can be a wheel, so the hook
should get the source from its own location, such as a git tag.
If the hook only clones a tag, use ``pypi-git`` instead and drop
the hook.

To migrate a raw-tarball hook to ``hook-sdist``, specify ``build_sdist``
in YAML and return its artifact kind explicitly:
Expand Down
85 changes: 68 additions & 17 deletions src/fromager/packagesettings/_resolver.py
Original file line number Diff line number Diff line change
Expand Up @@ -781,9 +781,30 @@ class AbstractHookResolver(AbstractResolver, CooldownMixin):
supports_override_hooks: typing.ClassVar[bool] = True
"""Hook resolvers support override hooks."""

hookless_provider: typing.ClassVar[str]
"""Profile to suggest for packages without hooks."""

def _find_resolver_hook(self, req: Requirement) -> typing.Callable | None:
"""Return the package resolver hook, or None to resolve on the index.

Resolving on the index needs a ``download_source`` hook to get the
source. Raise when the package has neither hook.
"""
hook = overrides.find_override_method(req.name, "get_resolver_provider")
if hook is None and not overrides.find_override_method(
req.name, "download_source"
):
raise ValueError(
f"{req.name}: source resolver {self.provider!r} requires a "
"get_resolver_provider or download_source override hook; "
f"use {self.hookless_provider!r} for a package without hooks"
)
return hook

def _resolver_provider_from_hook(
self,
*,
hook: typing.Callable,
ctx: context.WorkContext,
req: Requirement,
req_type: requirements_file.RequirementType | None,
Expand All @@ -792,14 +813,7 @@ def _resolver_provider_from_hook(
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"
)

"""Invoke the package resolver hook and validate its result."""
try:
provider = overrides.invoke(
hook,
Expand Down Expand Up @@ -903,9 +917,11 @@ class HookSDistResolver(AbstractHookResolver):
"""Call resolver_provider and download_source hook, build from source

The ``hook-sdist`` provider delegates resolution and download to
plugin hooks. The downloaded artifact can be a source distribution,
a tarball, or a git checkout. Download hooks can return ``DownloadedSource``
with a concrete kind, or a legacy archive ``Path`` treated as an sdist.
plugin hooks. Either hook is optional, but not both. Without a resolver
hook, versions come from ``index_url``. The downloaded artifact can be a
source distribution, a tarball, or a git checkout. Download hooks can
return ``DownloadedSource`` with a concrete kind, or a legacy archive
``Path`` treated as an sdist.
Without a download hook, the resolved URL is downloaded as an sdist.
Specify ``build_sdist`` explicitly: ``tarball`` to repack an existing
sdist, or ``pep517`` to build from a git checkout.
Expand All @@ -929,15 +945,32 @@ class HookSDistResolver(AbstractHookResolver):
download_kinds: typing.ClassVar[DownloadKindSet] = frozenset(
{DownloadKind.sdist, DownloadKind.tarball, DownloadKind.git_checkout}
)
hookless_provider: typing.ClassVar[str] = "pypi-sdist"

def resolver_provider(
self,
ctx: context.WorkContext,
req: Requirement,
req_type: requirements_file.RequirementType | None,
) -> resolver.BaseProvider:
"""Return a source provider from the package resolver hook."""
"""Return a source provider from the resolver hook or the index.

Without a ``get_resolver_provider`` hook, find versions on
``index_url`` in sdists and wheels for any platform, like
``pypi-git``. The ``download_source`` hook then gets the source.
"""
hook = self._find_resolver_hook(req)
if hook is None:
return resolver.PyPIProvider(
include_sdists=True,
include_wheels=True,
sdist_server_url=str(self.index_url),
constraints=ctx.constraints,
req_type=req_type,
ignore_platform=True,
)
return self._resolver_provider_from_hook(
hook=hook,
ctx=ctx,
req=req,
req_type=req_type,
Expand All @@ -964,10 +997,11 @@ class HookPrebuiltResolver(AbstractHookResolver):
"""Call resolver_provider and download_source hook, use pre-built wheel

The ``hook-prebuilt`` provider delegates resolution and download to
plugin hooks. The downloaded artifact must be a pre-built wheel. Download
hooks can return ``DownloadedSource`` with kind ``prebuilt_wheel`` or a
legacy wheel ``Path``. Without a download hook, the resolved URL is downloaded
into ``ctx.wheels_prebuilt``.
plugin hooks. Either hook is optional, but not both. Without a resolver
hook, versions come from ``index_url``. The downloaded artifact must be a
pre-built wheel. Download hooks can return ``DownloadedSource`` with kind
``prebuilt_wheel`` or a legacy wheel ``Path``. Without a download hook,
the resolved URL is downloaded into ``ctx.wheels_prebuilt``.

Example::

Expand All @@ -986,15 +1020,32 @@ class HookPrebuiltResolver(AbstractHookResolver):
{DownloadKind.prebuilt_wheel}
)
resolves_prebuilt_wheel: typing.ClassVar[bool] = True
hookless_provider: typing.ClassVar[str] = "pypi-prebuilt"

def resolver_provider(
self,
ctx: context.WorkContext,
req: Requirement,
req_type: requirements_file.RequirementType | None,
) -> resolver.BaseProvider:
"""Return a pre-built wheel provider from the package resolver hook."""
"""Return a pre-built wheel provider from the resolver hook or the index.

Without a ``get_resolver_provider`` hook, find versions on
``index_url`` in wheels for this platform, like ``pypi-prebuilt``.
The ``download_source`` hook then gets the wheel.
"""
hook = self._find_resolver_hook(req)
if hook is None:
return resolver.PyPIProvider(
include_sdists=False,
include_wheels=True,
sdist_server_url=str(self.index_url),
constraints=ctx.constraints,
req_type=req_type,
ignore_platform=False,
)
return self._resolver_provider_from_hook(
hook=hook,
ctx=ctx,
req=req,
req_type=req_type,
Expand Down
Loading
Loading