This is an automated email from the ASF dual-hosted git repository.

potiuk pushed a commit to branch v3-3-test
in repository https://gitbox.apache.org/repos/asf/airflow.git


The following commit(s) were added to refs/heads/v3-3-test by this push:
     new 3d475f2b20a [v3-3-test] Make the constraints check follow the cooldown 
rules the constraints use (#73316) (#73385)
3d475f2b20a is described below

commit 3d475f2b20afb36ddd9c0f4306c642748af462a2
Author: github-actions[bot] 
<41898282+github-actions[bot]@users.noreply.github.com>
AuthorDate: Sat Sep 19 20:30:00 2026 +0200

    [v3-3-test] Make the constraints check follow the cooldown rules the 
constraints use (#73316) (#73385)
    
    After every provider release wave the `Deps *:constraints` jobs doubled for
    three or four days and then recovered on their own. The constraints are
    resolved under uv's `exclude-newer` cooldown with the per-package overrides
    in the root pyproject.toml, where Airflow's own distributions are exempt, so
    a fresh provider wave lands in the constraints the same day. The check had
    its own 4-day cooldown applied to every package, so for those days the pin
    was newer than "latest" and a plain equality check counted it as outdated.
    With `--explain-why` every such package then cost a full `uv sync` that
    tried to pin it to the *older* version and reported that the pin did not
    take effect. On 2026-09-15 that was 37 providers and about 12 extra minutes
    per job, with nothing to act on.
    
    The check now reads `[tool.uv.exclude-newer-package]` and applies the same
    rules: no cooldown for exempt distributions, a moved cutoff where one is
    configured. A pin that is still ahead of "latest" counts as up to date, so
    no explanation runs for it either.
    (cherry picked from commit 7da73c89cb0133164f65d6189df97e23dc9292dd)
    
    
    Generated-by: Claude Opus 5
    
    Co-authored-by: Jarek Potiuk <[email protected]>
---
 .../utils/constraints_version_check.py             | 75 ++++++++++++++++++++--
 dev/breeze/tests/test_constraints_version_check.py | 73 ++++++++++++++++++++-
 2 files changed, 139 insertions(+), 9 deletions(-)

diff --git a/dev/breeze/src/airflow_breeze/utils/constraints_version_check.py 
b/dev/breeze/src/airflow_breeze/utils/constraints_version_check.py
index d5ee2f6cee8..10c485fdd61 100755
--- a/dev/breeze/src/airflow_breeze/utils/constraints_version_check.py
+++ b/dev/breeze/src/airflow_breeze/utils/constraints_version_check.py
@@ -38,6 +38,7 @@ from airflow_breeze.params.shell_params import ShellParams
 from airflow_breeze.utils.console import Output, console_print
 from airflow_breeze.utils.docker_command_utils import execute_command_in_shell
 from airflow_breeze.utils.github import download_constraints_file
+from airflow_breeze.utils.packages import load_pyproject_toml
 from airflow_breeze.utils.parallel import get_temp_file_name
 from airflow_breeze.utils.path_utils import AIRFLOW_ROOT_PATH
 from airflow_breeze.utils.shared_options import get_verbose
@@ -86,6 +87,15 @@ def is_valid_version(version_str: str, latest_version: 
Version) -> bool:
         return False
 
 
+def is_newer_version(candidate: str, reference: str) -> bool:
+    from packaging import version
+
+    try:
+        return version.parse(candidate) > version.parse(reference)
+    except version.InvalidVersion:
+        return False
+
+
 def count_versions_between(releases: dict[str, Any], current_version: str, 
latest_version: str):
     from packaging import version
 
@@ -185,14 +195,13 @@ def should_show_package(releases, latest_version, 
constraints_date, mode, is_lat
     return True
 
 
-def get_latest_version_with_cooldown(releases: dict[str, Any], cooldown_days: 
int) -> str | None:
-    """Find the latest non-prerelease version whose release date is outside 
the cooldown period.
+def get_latest_version_with_cooldown(releases: dict[str, Any], cutoff: 
datetime | None) -> str | None:
+    """Find the latest non-prerelease version uploaded no later than 
``cutoff`` (any time if None).
 
     Returns the version string, or None if no version qualifies.
     """
     from packaging import version
 
-    cutoff = datetime.now() - timedelta(days=cooldown_days)
     candidates: list[tuple[version.Version, str]] = []
     for v, release_files in releases.items():
         if not release_files:
@@ -211,7 +220,7 @@ def get_latest_version_with_cooldown(releases: dict[str, 
Any], cooldown_days: in
             ).replace(tzinfo=None)
         except (KeyError, IndexError, ValueError):
             continue
-        if upload_time <= cutoff:
+        if cutoff is None or upload_time <= cutoff:
             candidates.append((parsed_v, v))
     if not candidates:
         return None
@@ -219,6 +228,46 @@ def get_latest_version_with_cooldown(releases: dict[str, 
Any], cooldown_days: in
     return candidates[0][1]
 
 
+def load_cooldown_overrides() -> dict[str, bool | str]:
+    """Per-package ``exclude-newer-package`` entries of the root 
pyproject.toml, by canonical name.
+
+    The constraints are resolved under these rules (Airflow's own 
distributions are exempt from
+    the cooldown, a few packages have a moved cutoff), so "latest" has to be 
read under the same
+    rules or the check flags pins that resolved exactly as configured.
+    """
+    from packaging.utils import canonicalize_name
+
+    tool_uv = load_pyproject_toml(AIRFLOW_ROOT_PATH / 
"pyproject.toml").get("tool", {}).get("uv", {})
+    return {
+        canonicalize_name(name): value for name, value in 
tool_uv.get("exclude-newer-package", {}).items()
+    }
+
+
+_DURATION_RE = re.compile(r"^\s*(\d+(?:\.\d+)?)\s*(minute|hour|day)s?\s*$")
+
+
+def get_release_cutoff(pkg: str, cooldown_days: int, overrides: dict[str, bool 
| str]) -> datetime | None:
+    """Return the newest upload time still eligible as ``latest`` for ``pkg``, 
None for no limit.
+
+    Mirrors the ``exclude-newer-package`` values uv accepts: ``false`` lifts 
the cooldown, a
+    timestamp fixes the cutoff, a duration such as ``"12 hours"`` replaces the 
window.
+    """
+    from packaging.utils import canonicalize_name
+
+    override = overrides.get(canonicalize_name(pkg))
+    if override is False:
+        return None
+    if isinstance(override, str):
+        if match := _DURATION_RE.match(override):
+            amount, unit = float(match.group(1)), match.group(2)
+            return datetime.now() - timedelta(**{f"{unit}s": amount})
+        try:
+            return datetime.fromisoformat(override.replace("Z", 
"+00:00")).replace(tzinfo=None)
+        except ValueError:
+            console_print(f"[yellow]Ignoring unparsable exclude-newer-package 
value for {pkg}: {override}[/]")
+    return datetime.now() - timedelta(days=cooldown_days)
+
+
 def get_first_newer_release_date_str(releases, current_version):
     from packaging import version
 
@@ -265,7 +314,12 @@ def constraints_version_check(
 ):
     console_print(f"[bold cyan]Python version:[/] [white]{python}[/]")
     console_print(f"[bold cyan]Constraints mode:[/] 
[white]{airflow_constraints_mode}[/]")
-    console_print(f"[bold cyan]Cooldown period:[/] [white]{cooldown_days} 
days[/]\n")
+    cooldown_overrides = load_cooldown_overrides()
+    exempt_count = sum(1 for value in cooldown_overrides.values() if value is 
False)
+    console_print(
+        f"[bold cyan]Cooldown period:[/] [white]{cooldown_days} days[/] "
+        f"[white]({exempt_count} distributions exempt via 
exclude-newer-package)[/]\n"
+    )
     with tempfile.TemporaryDirectory() as temp_dir:
         constraints_file = Path(temp_dir) / "constraints.txt"
         download_constraints_file(
@@ -302,6 +356,7 @@ def constraints_version_check(
         airflow_constraints_mode=airflow_constraints_mode,
         github_repository=github_repository,
         cooldown_days=cooldown_days,
+        cooldown_overrides=cooldown_overrides,
     )
 
     print_table_footer(
@@ -445,6 +500,7 @@ def process_packages(
     airflow_constraints_mode: str,
     github_repository: str | None,
     cooldown_days: int = 4,
+    cooldown_overrides: dict[str, bool | str] | None = None,
 ) -> tuple[int, int, list[str], dict[str, int]]:
     def fetch_pypi_data(pkg: str) -> dict:
         pypi_url = f"https://pypi.org/pypi/{pkg}/json";
@@ -472,8 +528,13 @@ def process_packages(
         try:
             data = fetch_pypi_data(pkg)
             releases = data["releases"]
-            latest_version_with_cooldown = 
get_latest_version_with_cooldown(releases, cooldown_days)
+            cutoff = get_release_cutoff(pkg, cooldown_days, cooldown_overrides 
or {})
+            latest_version_with_cooldown = 
get_latest_version_with_cooldown(releases, cutoff)
             latest_version = latest_version_with_cooldown or 
data["info"]["version"]
+            if is_newer_version(pinned_version, latest_version):
+                # The pin can still be ahead of `latest` (an override dropped 
after the constraints
+                # resolved with it): nothing to upgrade to, so nothing to 
explain.
+                latest_version = pinned_version
             latest_release_date = get_release_dates(releases, latest_version)
             constraint_release_date = get_release_dates(releases, 
pinned_version)
             is_latest_version = pinned_version == latest_version
@@ -491,7 +552,7 @@ def process_packages(
                     format_str=format_str,
                     is_latest_version=is_latest_version,
                     versions_behind_str=versions_behind_str,
-                    cooldown_days=cooldown_days,
+                    cooldown_days=0 if cutoff is None else cooldown_days,
                 )
                 status_counts[status_category] += 1
                 if not is_latest_version:
diff --git a/dev/breeze/tests/test_constraints_version_check.py 
b/dev/breeze/tests/test_constraints_version_check.py
index c61f0d3f1ce..85856eb01fa 100644
--- a/dev/breeze/tests/test_constraints_version_check.py
+++ b/dev/breeze/tests/test_constraints_version_check.py
@@ -18,6 +18,7 @@ from __future__ import annotations
 
 import contextlib
 import json
+from datetime import datetime
 from pathlib import Path
 from unittest import mock
 
@@ -25,6 +26,7 @@ import pytest
 
 from airflow_breeze.utils.constraints_version_check import (
     explain_package_upgrade,
+    get_release_cutoff,
     get_table_format,
     process_packages,
 )
@@ -33,10 +35,15 @@ MODULE = "airflow_breeze.utils.constraints_version_check"
 
 # Old enough that the default 4-day cooldown never filters these releases out.
 OLD_UPLOAD_TIME = "2020-01-01T00:00:00.000000Z"
+# Far enough ahead to sit inside any cooldown window, whenever the test runs.
+FRESH_UPLOAD_TIME = "2099-01-01T00:00:00.000000Z"
 
 
-def _pypi_payload(latest: str, *versions: str) -> bytes:
-    releases = {v: [{"upload_time_iso_8601": OLD_UPLOAD_TIME, "yanked": 
False}] for v in versions}
+def _pypi_payload(latest: str, *versions: str, fresh: str | None = None) -> 
bytes:
+    releases = {
+        v: [{"upload_time_iso_8601": FRESH_UPLOAD_TIME if v == fresh else 
OLD_UPLOAD_TIME, "yanked": False}]
+        for v in versions
+    }
     return json.dumps({"info": {"version": latest}, "releases": 
releases}).encode()
 
 
@@ -105,6 +112,68 @@ def 
test_baseline_is_not_resolved_without_explain_why(mock_baseline, mock_explai
     mock_baseline.assert_not_called()
 
 
[email protected](f"{MODULE}.explain_package_upgrade")
[email protected](f"{MODULE}.resolve_baseline_versions")
[email protected](f"{MODULE}.get_latest_version_with_cooldown", return_value="1.0.0")
+def test_pin_newer_than_cooldown_latest_is_up_to_date(mock_cooldown, 
mock_baseline, mock_explain, pypi):
+    # Constraints already moved to 2.0.0 while the cooldown still reports 
1.0.0 as latest.
+    outdated_count, _, explanations, status_counts = 
_run_process_packages([("pkg-a", "2.0.0")])
+
+    assert outdated_count == 0
+    assert explanations == []
+    assert status_counts["ok"] == 1
+    mock_explain.assert_not_called()
+    mock_baseline.assert_not_called()
+
+
[email protected](
+    ("overrides", "expected_outdated"),
+    [
+        pytest.param({}, 0, id="third-party release inside the cooldown is not 
latest yet"),
+        pytest.param({"pkg-a": False}, 1, id="exempt distribution counts its 
fresh release right away"),
+    ],
+)
[email protected](f"{MODULE}.explain_package_upgrade", return_value="explanation")
[email protected](f"{MODULE}.resolve_baseline_versions", return_value=("baseline 
log", {}))
+def test_cooldown_follows_exclude_newer_package_overrides(
+    mock_baseline, mock_explain, monkeypatch, overrides, expected_outdated
+):
+    def fake_urlopen(_url):
+        response = mock.MagicMock()
+        response.read.return_value = _pypi_payload("2.0.0", "1.0.0", "2.0.0", 
fresh="2.0.0")
+        return contextlib.nullcontext(response)
+
+    monkeypatch.setattr(f"{MODULE}.urllib.request.urlopen", fake_urlopen)
+    col_widths, format_str, _, _ = get_table_format([("pkg-a", "1.0.0")])
+
+    outdated_count, _, _, _ = process_packages(
+        packages=[("pkg-a", "1.0.0")],
+        constraints_date=None,
+        mode="full",
+        explain_why=True,
+        col_widths=col_widths,
+        format_str=format_str,
+        python_version="3.11",
+        airflow_constraints_mode="constraints",
+        github_repository="apache/airflow",
+        cooldown_overrides=overrides,
+    )
+
+    assert outdated_count == expected_outdated
+    assert mock_explain.call_count == expected_outdated
+
+
[email protected](
+    ("override", "expected"),
+    [
+        pytest.param(False, None, id="false lifts the cooldown"),
+        pytest.param("2026-08-13T00:00:00Z", datetime(2026, 8, 13), 
id="timestamp fixes the cutoff"),
+    ],
+)
+def test_release_cutoff_honours_override_shapes(override, expected):
+    assert get_release_cutoff("Pkg_A", 4, {"pkg-a": override}) == expected
+
+
 @mock.patch(f"{MODULE}.update_pyproject_dependency")
 @mock.patch(f"{MODULE}.preserve_files")
 @mock.patch(f"{MODULE}.sync_and_freeze")

Reply via email to