Skip to content
Open
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
14 changes: 14 additions & 0 deletions docs/how-tos/release-age-cooldown.rst
Original file line number Diff line number Diff line change
Expand Up @@ -130,3 +130,17 @@ Valid values:
This is useful when a specific package is trusted enough to allow recent
versions, or when a package's release cadence makes the global cooldown
impractical.

Top-Level ``==`` Pin Exemption
------------------------------

Top-level requirements that are a single exact ``==`` pin
(e.g. ``torch==2.5.1``) bypass the cooldown automatically -- the operator
has explicitly chosen that version.

Wildcard or compound specifiers (for example ``==1.*`` or ``==1.0,>0.9``)
are **not** exempt.

``==`` specifiers in transitive dependencies are also **not** exempt;
without this distinction a malicious package could pin its own dependencies
to bypass cooldown.
18 changes: 7 additions & 11 deletions docs/proposals/release-cooldown.md
Original file line number Diff line number Diff line change
Expand Up @@ -55,17 +55,13 @@ References:
global default plus per-package overrides.
- **SSH transport** for git timestamp retrieval.

### Future consideration: `==` pin exemptions

Whether `==` pins in top-level requirements or constraints files
should automatically bypass cooldown is deferred. The per-package
`resolver_dist.min_release_age: 0` override already provides an
explicit, auditable escape hatch for packages that need to use
recently-published versions. Adding automatic `==` exemptions
would introduce a special case that weakens the security model
and requires users to understand the distinction. This can be
revisited if the per-package override proves too cumbersome in
practice.
### `==` pin exemptions (implemented)

Top-level single exact `==` pins (e.g. `torch==2.5.1`) bypass cooldown
automatically. Wildcard (`==1.*`) and compound specifiers are not exempt.
Transitive `==` pins remain subject to cooldown for security. See
[the how-to guide](../how-tos/release-age-cooldown.rst) for details.
Tracked in [#1123](https://github.com/python-wheel-build/fromager/issues/1123).
Copy link
Copy Markdown
Contributor

@smoparth smoparth May 8, 2026

Choose a reason for hiding this comment

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

Not sure if we need to link this in proposal


## How

Expand Down
19 changes: 18 additions & 1 deletion src/fromager/resolver.py
Original file line number Diff line number Diff line change
Expand Up @@ -103,7 +103,7 @@ def resolve(
req_type=req_type,
ignore_platform=ignore_platform,
)
provider.cooldown = resolve_package_cooldown(ctx, req)
provider.cooldown = resolve_package_cooldown(ctx, req, req_type=req_type)
max_age_cutoff = _compute_max_age_cutoff(ctx)
results = find_all_matching_from_provider(
provider, req, max_age_cutoff=max_age_cutoff
Expand Down Expand Up @@ -137,19 +137,36 @@ def default_resolver_provider(
)


def _has_equality_pin(req: Requirement) -> bool:
"""Return True if the requirement has a single exact ``==`` pin.

Rejects wildcard pins (``==1.*``) and compound specifiers (``==1,>2``)
which are not true exact version pins.
"""
specs = list(req.specifier)
return len(specs) == 1 and specs[0].operator == "==" and "*" not in specs[0].version


def resolve_package_cooldown(
ctx: context.WorkContext,
req: Requirement,
req_type: RequirementType | None = None,
) -> Cooldown | None:
"""Compute the effective cooldown for a single package.

Args:
ctx: The current work context (provides the global cooldown).
req: The package requirement being resolved.
req_type: The requirement type (top-level, install, etc.).

Returns:
The cooldown to pass to the provider, or ``None`` if disabled.
"""
if req_type == RequirementType.TOP_LEVEL and _has_equality_pin(req):
if ctx.cooldown is not None:
logger.info("cooldown bypassed as the top-level requirement uses == pin")
return None

per_package_days = ctx.package_build_info(req).resolver_min_release_age
global_cooldown = ctx.cooldown
if per_package_days is None:
Expand Down
2 changes: 1 addition & 1 deletion src/fromager/sources.py
Original file line number Diff line number Diff line change
Expand Up @@ -153,7 +153,7 @@ def get_source_provider(
ignore_platform=pbi.resolver_ignore_platform,
),
)
provider.cooldown = resolver.resolve_package_cooldown(ctx, req)
provider.cooldown = resolver.resolve_package_cooldown(ctx, req, req_type=req_type)
return provider


Expand Down
4 changes: 3 additions & 1 deletion src/fromager/wheels.py
Original file line number Diff line number Diff line change
Expand Up @@ -549,7 +549,9 @@ def resolve_all_prebuilt_wheels(
provider = get_prebuilt_wheel_provider(
ctx=ctx, req=req, wheel_server_url=url, req_type=req_type
)
provider.cooldown = resolver.resolve_package_cooldown(ctx, req)
provider.cooldown = resolver.resolve_package_cooldown(
ctx, req, req_type=req_type
)
# The local fromager wheel server is PEP 503-only and serves
# packages that were already resolved and vetted earlier in the
# same run. Don't fail-closed on missing upload_time there.
Expand Down
104 changes: 104 additions & 0 deletions tests/test_cooldown.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
from packaging.version import Version

from fromager import candidate, context, packagesettings, resolver, sources, wheels
from fromager.requirements_file import RequirementType

_BOOTSTRAP_TIME = datetime.datetime(2026, 3, 26, 0, 0, 0, tzinfo=datetime.UTC)
_COOLDOWN_7_DAYS = datetime.timedelta(days=7)
Expand Down Expand Up @@ -247,6 +248,43 @@ def test_cooldown_applied_automatically_via_ctx(tmp_path: pathlib.Path) -> None:
assert str(version) == "1.3.2"


def test_toplevel_equality_pin_bypasses_cooldown_via_resolve(
tmp_path: pathlib.Path,
) -> None:
"""Top-level == pin threads through resolve() and bypasses cooldown end-to-end.

Verifies the req_type plumbing in resolver.resolve() actually causes
resolve_package_cooldown() to disable cooldown, allowing a recent version
that would normally be filtered.
"""
ctx = context.WorkContext(
active_settings=None,
constraints_file=None,
patches_dir=tmp_path / "patches",
sdists_repo=tmp_path / "sdists-repo",
wheels_repo=tmp_path / "wheels-repo",
work_dir=tmp_path / "work-dir",
cooldown=_COOLDOWN,
)

with requests_mock.Mocker() as r:
r.get(
"https://pypi.org/simple/test-pkg/",
json=_cooldown_json_response,
headers={"Content-Type": _PYPI_SIMPLE_JSON_CONTENT_TYPE},
)

_, version = resolver.resolve(
ctx=ctx,
req=Requirement("test-pkg==2.0.0"),
sdist_server_url="https://pypi.org/simple/",
include_sdists=True,
include_wheels=True,
req_type=RequirementType.TOP_LEVEL,
)
assert str(version) == "2.0.0"


def test_cooldown_applied_via_get_source_provider(tmp_path: pathlib.Path) -> None:
"""ctx.cooldown propagates through sources.get_source_provider() to any provider.

Expand Down Expand Up @@ -860,3 +898,69 @@ def test_compute_max_age_cutoff_disabled(
"""_compute_max_age_cutoff returns None when max_release_age is not set."""
cutoff = resolver._compute_max_age_cutoff(tmp_context)
assert cutoff is None


def test_resolve_package_cooldown_exempt_toplevel_equality_pin(
tmp_path: pathlib.Path,
) -> None:
"""Top-level == pin bypasses cooldown."""
ctx = _make_ctx(tmp_path, cooldown=_COOLDOWN)
result = resolver.resolve_package_cooldown(
ctx, Requirement("test-pkg==1.3.2"), req_type=RequirementType.TOP_LEVEL
)
assert result is None


def test_resolve_package_cooldown_enforced_transitive_equality_pin(
tmp_path: pathlib.Path,
) -> None:
"""Transitive == pin does NOT bypass cooldown."""
ctx = _make_ctx(tmp_path, cooldown=_COOLDOWN)
result = resolver.resolve_package_cooldown(
ctx, Requirement("test-pkg==1.3.2"), req_type=RequirementType.INSTALL
)
assert result is _COOLDOWN


def test_resolve_package_cooldown_enforced_toplevel_no_pin(
tmp_path: pathlib.Path,
) -> None:
"""Top-level requirement without == still gets cooldown."""
ctx = _make_ctx(tmp_path, cooldown=_COOLDOWN)
result = resolver.resolve_package_cooldown(
ctx, Requirement("test-pkg>=1.0"), req_type=RequirementType.TOP_LEVEL
)
assert result is _COOLDOWN


def test_resolve_package_cooldown_none_req_type_not_exempt(
tmp_path: pathlib.Path,
) -> None:
"""Unknown req_type (None) with == does NOT bypass cooldown."""
ctx = _make_ctx(tmp_path, cooldown=_COOLDOWN)
result = resolver.resolve_package_cooldown(
ctx, Requirement("test-pkg==1.3.2"), req_type=None
)
assert result is _COOLDOWN
Comment thread
coderabbitai[bot] marked this conversation as resolved.


def test_resolve_package_cooldown_toplevel_wildcard_equality_not_exempt(
tmp_path: pathlib.Path,
) -> None:
"""Top-level wildcard equality (==1.*) is not an exact pin — cooldown applies."""
ctx = _make_ctx(tmp_path, cooldown=_COOLDOWN)
result = resolver.resolve_package_cooldown(
ctx, Requirement("test-pkg==1.*"), req_type=RequirementType.TOP_LEVEL
)
assert result is _COOLDOWN


def test_resolve_package_cooldown_toplevel_compound_specifier_not_exempt(
tmp_path: pathlib.Path,
) -> None:
"""Top-level compound specifier (==1.0,>0.9) is not a single exact pin."""
ctx = _make_ctx(tmp_path, cooldown=_COOLDOWN)
result = resolver.resolve_package_cooldown(
ctx, Requirement("test-pkg==1.0,>0.9"), req_type=RequirementType.TOP_LEVEL
)
assert result is _COOLDOWN
Loading