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

potiuk pushed a commit to branch pin-constraints-providers-to-pypi-versions
in repository https://gitbox.apache.org/repos/asf/airflow.git

commit 2dceda458fcbfcae2ad52e71493920b485c33b60
Author: Jarek Potiuk <[email protected]>
AuthorDate: Sat Aug 8 16:00:46 2026 +0800

    Pin providers in constraints to the versions published in PyPI
    
    Constraints cut for a release candidate had to land on the wave being voted
    on, whose providers exist in PyPI only as rc versions. Asking uv for a
    pre-release strategy left it free to answer with any version satisfying the
    lower bounds, so the pins were neither the candidate nor the last release, 
and
    what a candidate shipped depended on how the resolution happened to go.
    
    Retrieving the versions from PyPI and naming them leaves nothing to resolve:
    the constraints pin what is actually published. A pre-release is only ever
    considered when the run allows it, and even then it has to sort above every
    final release, so a provider without a candidate in the wave keeps its 
release
    and a released constraints file can never carry an rc pin.
    
    A candidate is exempt from the downgrade check - it sorts below the releases
    the constraints branch already carries, so the comparison says nothing 
there.
    
    Re-running the workflow for the same candidate now replaces that candidate's
    branch and tag instead of pushing onto them, so the two always describe the 
run
    that produced them and a wave can be re-cut.
---
 .github/workflows/release-constraints.yml          |  30 +++-
 .../airflow_breeze/utils/release_constraints.py    |  11 +-
 scripts/in_container/run_generate_constraints.py   | 171 ++++++++++++++-------
 .../in_container/test_run_generate_constraints.py  | 164 +++++++++++++++++---
 4 files changed, 295 insertions(+), 81 deletions(-)

diff --git a/.github/workflows/release-constraints.yml 
b/.github/workflows/release-constraints.yml
index 94506af6f01..be898f91fce 100644
--- a/.github/workflows/release-constraints.yml
+++ b/.github/workflows/release-constraints.yml
@@ -20,10 +20,11 @@
 #
 # The stage is derived from the version, so the two cannot be mismatched by 
hand:
 #
-#   * a candidate (`3.1.3rc1`) resolves with pre-releases allowed - the 
providers of the wave
-#     being voted on exist on PyPI only as rc versions - and lands on a branch 
of its own, so a
-#     candidate never moves the branch every other build reads;
-#   * a final (`3.1.3`) resolves without them, against the providers now 
published as finals, and
+#   * a candidate (`3.1.3rc1`) pins the providers at the versions PyPI holds - 
the wave being
+#     voted on exists there only as rc versions - and lands on a branch of its 
own, so a candidate
+#     never moves the branch every other build reads. Re-running it for the 
same candidate
+#     replaces that branch and its tag, so the two always describe the run 
that produced them;
+#   * a final (`3.1.3`) pins the providers at their released versions, 
ignoring any candidate, and
 #     commits onto `constraints-X-Y` itself, which is what makes the released 
constraints the
 #     baseline everything downstream reads.
 #
@@ -203,6 +204,7 @@ jobs:
       VERSION: ${{ inputs.version }}
       CONSTRAINTS_BRANCH: ${{ needs.build-info.outputs.constraints-branch }}
       TARGET_BRANCH: ${{ needs.build-info.outputs.target-branch }}
+      ALLOW_PRE_RELEASES: ${{ needs.build-info.outputs.allow-pre-releases }}
     steps:
       - name: "Cleanup repo"
         shell: bash
@@ -226,6 +228,26 @@ jobs:
         with:
           pattern: constraints-*
           path: ./files
+      # A candidate's branch and tag belong to that candidate alone, so 
re-running for the same
+      # rc replaces them rather than adding to them: the branch would 
otherwise already hold the
+      # previous run's constraints (making the push a non-fast-forward) and 
the tag already exist.
+      # A final never gets this - it commits onto the shared constraints-X-Y 
branch, whose history
+      # every other build reads.
+      - name: "Delete the previous ${{ needs.build-info.outputs.target-branch 
}} branch and tag"
+        if: needs.build-info.outputs.allow-pre-releases == 'true'
+        working-directory: "constraints"
+        shell: bash
+        run: |
+          if git ls-remote --exit-code origin "refs/heads/${TARGET_BRANCH}" > 
/dev/null; then
+            echo "Deleting the existing '${TARGET_BRANCH}' branch."
+            git push origin --delete "refs/heads/${TARGET_BRANCH}"
+          fi
+          if git ls-remote --exit-code origin 
"refs/tags/constraints-${VERSION}" > /dev/null; then
+            echo "Deleting the existing 'constraints-${VERSION}' tag."
+            git push origin --delete "refs/tags/constraints-${VERSION}"
+          fi
+          git tag --delete "constraints-${VERSION}" > /dev/null 2>&1 || true
+          git branch --delete --force "${TARGET_BRANCH}" > /dev/null 2>&1 || 
true
       - name: "Switch to ${{ needs.build-info.outputs.target-branch }}"
         working-directory: "constraints"
         shell: bash
diff --git a/dev/breeze/src/airflow_breeze/utils/release_constraints.py 
b/dev/breeze/src/airflow_breeze/utils/release_constraints.py
index 466ab689120..13ad8a79d9e 100644
--- a/dev/breeze/src/airflow_breeze/utils/release_constraints.py
+++ b/dev/breeze/src/airflow_breeze/utils/release_constraints.py
@@ -39,11 +39,12 @@ APACHE_AIRFLOW_REPO = "apache/airflow"
 def publish_constraints(*, version: str, ref: str, workflow_branch: str = 
"main") -> None:
     """Resolve, publish and tag the constraints belonging to ``version``.
 
-    ``version`` alone decides what happens: a candidate (``3.1.3rc1``) 
resolves with pre-releases
-    allowed - the providers of the wave being voted on are on PyPI only as rc 
versions - and lands
-    on a branch of its own, leaving the branch every other build reads where 
it was. A final
-    (``3.1.3``) resolves without them and commits onto ``constraints-X-Y``, 
which is what makes the
-    released constraints the baseline everything downstream reads.
+    ``version`` alone decides what happens: a candidate (``3.1.3rc1``) pins 
the providers at the
+    versions PyPI holds - the wave being voted on is there only as rc versions 
- and lands on a
+    branch of its own, leaving the branch every other build reads where it 
was. Re-running it for
+    the same candidate replaces that branch and its tag, so a wave can be 
re-cut. A final
+    (``3.1.3``) resolves against the published releases and commits onto 
``constraints-X-Y``, which
+    is what makes the released constraints the baseline everything downstream 
reads.
     """
     stage = "candidate" if "rc" in version else "final"
     if not confirm_action(
diff --git a/scripts/in_container/run_generate_constraints.py 
b/scripts/in_container/run_generate_constraints.py
index c1cca95e2c2..6bd75777253 100755
--- a/scripts/in_container/run_generate_constraints.py
+++ b/scripts/in_container/run_generate_constraints.py
@@ -21,15 +21,19 @@ import ast
 import json
 import os
 import sys
+from concurrent.futures import ThreadPoolExecutor
 from dataclasses import dataclass
 from datetime import datetime
 from functools import cached_property
+from itertools import repeat
 from pathlib import Path
 from typing import TextIO
 
 import requests
 from click import Choice
 from in_container_utils import AIRFLOW_DIST_PATH, AIRFLOW_ROOT_PATH, click, 
console, run_command
+from packaging.specifiers import InvalidSpecifier, SpecifierSet
+from packaging.version import InvalidVersion, Version
 
 try:
     import tomllib
@@ -39,6 +43,8 @@ except ImportError:
 DEFAULT_BRANCH = os.environ.get("DEFAULT_BRANCH", "main")
 PYTHON_VERSION = os.environ.get("PYTHON_MAJOR_MINOR_VERSION", "3.10")
 GENERATED_PROVIDER_DEPENDENCIES_FILE = AIRFLOW_ROOT_PATH / "generated" / 
"provider_dependencies.json"
+PYPI_JSON_API_URL = "https://pypi.org/pypi/{distribution}/json";
+PYPI_LOOKUP_PARALLELISM = 8
 
 
 def _read_version_from_pyproject(pyproject_path: Path) -> str:
@@ -380,11 +386,19 @@ def check_providers_not_downgraded(config_params: 
ConfigParams) -> None:
     Fail generation if any released provider is downgraded compared to the 
latest constraints.
 
     Released provider versions only ever move forward on PyPI, so a lower 
version in the freshly
-    generated constraints signals a resolution problem (a broken dependency 
forcing an old provider
-    back in) rather than an intended change. We stop here so it is caught 
instead of being published.
-    """
-    from packaging.version import InvalidVersion, Version
+    generated constraints means the version we had pinned is no longer 
installable - yanked, or
+    left without a file for the Python being resolved. We stop here so it is 
caught instead of
+    being published.
 
+    A release candidate is exempt: it pins the wave being voted on, which 
sorts below the released
+    versions a constraints branch may already carry, so the comparison says 
nothing there.
+    """
+    if config_params.allow_pre_releases:
+        console.print(
+            "[yellow]Pre-releases are allowed - skipping the provider 
downgrade check, a candidate "
+            "sorts below the releases the constraints branch carries."
+        )
+        return
     if not config_params.latest_constraints_file.exists():
         console.print("[yellow]No previous constraints file downloaded - 
skipping provider downgrade check.")
         return
@@ -408,10 +422,10 @@ def check_providers_not_downgraded(config_params: 
ConfigParams) -> None:
         for provider, latest_version, current_version in sorted(downgraded):
             console.print(f"[red]  * {provider}: {latest_version} -> 
{current_version}")
         console.print(
-            "[yellow]Released providers should never be downgraded. This 
usually means a broken "
-            "dependency version forced an older provider back in during 
resolution. Investigate the "
-            "diff above and, if needed, add an exclusion in the "
-            f"`additional_constraints_for_highest_resolution` list in [/] 
{__file__}"
+            "[yellow]Released providers should never be downgraded. The 
providers are pinned at the "
+            "newest version PyPI serves for this Python, so a lower one means 
the version we had is "
+            "gone - yanked, or no longer offering a file this Python can 
install. Investigate the "
+            "diff above before publishing.[/]"
         )
         write_provider_downgrade_slack_message(config_params, downgraded)
         sys.exit(1)
@@ -459,16 +473,82 @@ def get_all_active_provider_distributions(python_version: 
str | None = None) ->
     ]
 
 
-def build_provider_pre_release_requirements(python_version: str) -> list[str]:
-    """Requirements that let only the providers resolve to a pre-release.
-
-    uv considers a pre-release for a package only when some requirement for it 
mentions one, so a
-    pre-release lower bound on each provider confines the allowance to them. 
`--pre` would apply to
-    the whole resolution and could put a pre-release of any third-party 
dependency into the
-    constraints a release ships.
+def is_file_installable(pypi_file: dict, target_python: Version) -> bool:
+    """Whether a file PyPI lists for a release can be installed on 
``target_python``."""
+    if pypi_file.get("yanked"):
+        return False
+    requires_python = pypi_file.get("requires_python")
+    if not requires_python:
+        return True
+    try:
+        return target_python in SpecifierSet(requires_python)
+    except InvalidSpecifier:
+        return True
+
+
+def find_newest_version_in_pypi(
+    distribution: str, python_version: str, allow_pre_releases: bool
+) -> str | None:
+    """Return the newest version of ``distribution`` PyPI can install for 
``python_version``.
+
+    A pre-release is only ever considered when ``allow_pre_releases`` is set, 
and even then it wins
+    only by sorting above every final release - so a provider with a candidate 
in the wave being
+    voted on resolves to that candidate while one without keeps its last 
release. Versions PyPI can
+    no longer serve for the Python being resolved - fully yanked, or excluded 
by ``requires_python``
+    - are passed over, because pinning one leaves the resolution nothing to 
install. ``None``
+    (nothing installable at all, e.g. a provider whose first release is still 
in this wave) leaves
+    the distribution unpinned rather than pinned to a version that cannot be 
had.
     """
+    response = 
requests.get(PYPI_JSON_API_URL.format(distribution=distribution), timeout=60)
+    if response.status_code == 404:
+        console.print(f"[yellow]{distribution} is not published in PyPI - 
leaving it unpinned.")
+        return None
+    response.raise_for_status()
+    target_python = Version(python_version)
+    newest_version: Version | None = None
+    for version, files in response.json().get("releases", {}).items():
+        try:
+            parsed_version = Version(version)
+        except InvalidVersion:
+            continue
+        if parsed_version.is_prerelease and not allow_pre_releases:
+            continue
+        if not any(is_file_installable(pypi_file, target_python) for pypi_file 
in files):
+            continue
+        if newest_version is None or parsed_version > newest_version:
+            newest_version = parsed_version
+    if newest_version is None:
+        console.print(f"[yellow]{distribution} has no installable version in 
PyPI - leaving it unpinned.")
+        return None
+    return str(newest_version)
+
+
+def build_pinned_provider_requirements(python_version: str, 
allow_pre_releases: bool) -> list[str]:
+    """Exact pins for every active provider, at the newest version PyPI holds 
for it.
+
+    Naming the versions retrieved from PyPI leaves the providers nothing to 
resolve: the constraints
+    pin what is actually published rather than whatever the resolver settles 
on. It is what makes a
+    release candidate land on the wave being voted on, whose providers are 
published only as rc
+    versions - handing uv a pre-release strategy instead left it free to 
answer with any version
+    satisfying the lower bounds. It also keeps pre-releases confined to the 
providers: no
+    third-party dependency can answer with one, because no requirement here 
mentions a pre-release
+    of anything else.
+    """
+    distributions = get_all_active_provider_distributions(python_version)
+    console.print(f"[bright_blue]Retrieving the newest PyPI version of 
{len(distributions)} providers.")
+    with ThreadPoolExecutor(max_workers=PYPI_LOOKUP_PARALLELISM) as executor:
+        newest_versions = list(
+            executor.map(
+                find_newest_version_in_pypi,
+                distributions,
+                repeat(python_version),
+                repeat(allow_pre_releases),
+            )
+        )
     return [
-        f"{distribution}>=0.0.0rc0" for distribution in 
get_all_active_provider_distributions(python_version)
+        f"{distribution}=={version}"
+        for distribution, version in zip(distributions, newest_versions)
+        if version is not None
     ]
 
 
@@ -532,17 +612,13 @@ def generate_constraints_pypi_providers(config_params: 
ConfigParams) -> None:
     #   that the resolver will not downgrade the provider.
     # * opentelemetry-exporter-prometheus>=0.47b0 — this package only ever 
publishes beta versions
     #   (airflow-core requires ``>=0.47b0`` and released constraints already 
pin a beta, e.g.
-    #   ``==0.65b0``). For a release candidate the resolution runs with 
``--prerelease explicit``,
-    #   which permits a pre-release only for a package some requirement marks 
as such and drops the
-    #   if-necessary fallback; without this entry the package cannot resolve 
and generation fails
-    #   with "No solution found". Keeping it here (rather than the provider 
pre-release list) marks
-    #   it as an always-allowed pre-release across every resolution, matching 
how it already ships.
+    #   ``==0.65b0``). Marking it as a pre-release here rather than relying on 
uv's if-necessary
+    #   fallback keeps the choice deliberate and identical across every 
resolution.
     # * opentelemetry-semantic-conventions>=0.48b0 — same story: a beta-only 
package, pulled in as a
-    #   hard dependency of opentelemetry-sdk (via opentelemetry-exporter-otlp 
and shared/observability),
-    #   so the ``--prerelease explicit`` resolution needs the same explicit 
mark. The floor only marks
-    #   it as a pre-release and must stay at the version paired with our 
``opentelemetry-*>=1.27.0``
-    #   floor — opentelemetry-sdk exact-pins the semantic conventions version 
it ships with, so a
-    #   higher floor here would conflict with any sdk older than that pairing.
+    #   hard dependency of opentelemetry-sdk (via opentelemetry-exporter-otlp 
and shared/observability).
+    #   The floor only marks it as a pre-release and must stay at the version 
paired with our
+    #   ``opentelemetry-*>=1.27.0`` floor — opentelemetry-sdk exact-pins the 
semantic conventions
+    #   version it ships with, so a higher floor here would conflict with any 
sdk older than that pairing.
     #
     # These two are the only pre-releases in the constraints we tag, and 
removing the need for the
     # exception is tracked at https://github.com/apache/airflow/issues/71176
@@ -554,27 +630,16 @@ def generate_constraints_pypi_providers(config_params: 
ConfigParams) -> None:
         "opentelemetry-semantic-conventions>=0.48b0",
     ]
 
-    # Constraints cut for a release candidate have to pin the candidates 
themselves - the providers
-    # for that wave exist on PyPI only as rcN versions, and uv will not 
resolve to a pre-release
-    # unless asked. The final release regenerates these without it, so a 
released constraints file
-    # can never carry an rc pin.
-    #
-    # Scoped to the providers rather than passing `--pre`, which applies to 
the whole resolution
-    # and would let any dependency answer with a pre-release - putting, say, a 
beta of a
-    # third-party library into the constraints a release ships. uv has no 
per-package pre-release
-    # flag, so the scoping is expressed the way it does support: `explicit` 
permits a pre-release
-    # only for a package some requirement marks as such, and the rc lower 
bound below is that mark.
-    # `explicit` rather than the default `if-necessary-or-explicit` also drops 
the fallback that
-    # would otherwise let an unmarked package resolve to a pre-release when no 
final satisfies.
-    pre_release_requirements: list[str] = []
-    pre_release_strategy: list[str] = []
-    if config_params.allow_pre_releases:
-        pre_release_requirements = 
build_provider_pre_release_requirements(config_params.python)
-        pre_release_strategy = ["--prerelease", "explicit"]
-        console.print(
-            f"[bright_blue]Allowing pre-releases for 
{len(pre_release_requirements)} provider "
-            "distributions - no other package can resolve to one."
-        )
+    # Every run pins the providers at the versions PyPI holds when it is 
generated. Only a run for
+    # a release candidate considers the rc versions of the wave being voted 
on; a final regenerates
+    # these against the published releases, so a released constraints file can 
never carry an rc pin.
+    pinned_provider_requirements = build_pinned_provider_requirements(
+        config_params.python, config_params.allow_pre_releases
+    )
+    console.print(
+        f"[bright_blue]Pinning {len(pinned_provider_requirements)} provider 
distributions to the "
+        f"{'newest' if config_params.allow_pre_releases else 'newest final'} 
versions in PyPI."
+    )
 
     result = run_command(
         cmd=[
@@ -589,8 +654,7 @@ def generate_constraints_pypi_providers(config_params: 
ConfigParams) -> None:
             f"apache-airflow-task-sdk=={AIRFLOW_TASK_SDK_VERSION}",
             "./airflow-ctl",
             *additional_constraints_for_highest_resolution,
-            *pre_release_requirements,
-            *pre_release_strategy,
+            *pinned_provider_requirements,
             "--reinstall",  # We need to pull the provider distributions from 
PyPI or dist, not the local ones
             "--resolution",
             "highest",
@@ -603,7 +667,9 @@ def generate_constraints_pypi_providers(config_params: 
ConfigParams) -> None:
     if result.returncode != 0:
         console.print(
             "[red]Failed to install airflow with PyPI providers with highest 
resolution.[/]\n"
-            "[yellow]Please check the output above for details. One of they 
ways how to resolve it, in "
+            "[yellow]Please check the output above for details. The providers 
are pinned at the newest "
+            "versions PyPI serves, so two of them requiring incompatible 
dependencies fails here rather "
+            "than quietly settling on an older provider. One of they ways how 
to resolve it, in "
             "case it is caused by a specific broken dependency version, is to 
exclude it above in the "
             f"`additional_constraints_for_highest_resolution` list in [/] 
{__file__}"
         )
@@ -693,8 +759,9 @@ ALLOWED_CONSTRAINTS_MODES = ["constraints", 
"constraints-source-providers", "con
     "--allow-pre-releases",
     is_flag=True,
     default=False,
-    help="Allow pre-release versions of Airflow and providers to be pinned. 
Used when constraints "
-    "are generated for a release candidate, whose providers are only on PyPI 
as rc versions.",
+    help="Let the provider pins use pre-release versions when those are newer 
than any final "
+    "release. Used when constraints are generated for a release candidate, 
whose providers are "
+    "only on PyPI as rc versions.",
     envvar="ALLOW_PRE_RELEASES",
 )
 def generate_constraints(
diff --git a/scripts/tests/in_container/test_run_generate_constraints.py 
b/scripts/tests/in_container/test_run_generate_constraints.py
index 88af9e84102..99c7f4c5ee9 100644
--- a/scripts/tests/in_container/test_run_generate_constraints.py
+++ b/scripts/tests/in_container/test_run_generate_constraints.py
@@ -24,15 +24,38 @@ import pytest
 import run_generate_constraints as m
 
 
-def _config(latest: Path, current: Path, constraints_dir: Path | None = None) 
-> SimpleNamespace:
+def _config(
+    latest: Path,
+    current: Path,
+    constraints_dir: Path | None = None,
+    allow_pre_releases: bool = False,
+) -> SimpleNamespace:
     return SimpleNamespace(
         latest_constraints_file=latest,
         current_constraints_file=current,
         constraints_dir=constraints_dir if constraints_dir is not None else 
latest.parent,
         python="3.10",
+        allow_pre_releases=allow_pre_releases,
     )
 
 
+def _file(*, yanked: bool = False, requires_python: str | None = ">=3.10") -> 
dict:
+    return {"yanked": yanked, "requires_python": requires_python}
+
+
+class _FakeResponse:
+    def __init__(self, payload: dict, status_code: int = 200):
+        self._payload = payload
+        self.status_code = status_code
+
+    def json(self) -> dict:
+        return self._payload
+
+    def raise_for_status(self) -> None:
+        if self.status_code >= 400:
+            raise AssertionError(f"Unexpected status {self.status_code}")
+
+
 class TestReadProviderVersionsFromConstraints:
     def test_extracts_only_providers_and_strips_markers(self, tmp_path):
         constraints = tmp_path / "constraints.txt"
@@ -82,6 +105,16 @@ class TestCheckProvidersNotDowngraded:
         payload = json.loads((tmp_path / 
"provider-downgrade-slack-message.json").read_text())
         assert payload["channel"] == "some-other-channel"
 
+    def test_skips_the_check_for_a_release_candidate(self, tmp_path):
+        latest = tmp_path / "latest.txt"
+        current = tmp_path / "current.txt"
+        latest.write_text("apache-airflow-providers-amazon==8.0.0\n")
+        current.write_text("apache-airflow-providers-amazon==7.5.0\n")
+
+        m.check_providers_not_downgraded(_config(latest, current, 
allow_pre_releases=True))
+
+        assert not (tmp_path / 
"provider-downgrade-slack-message.json").exists()
+
     def test_passes_when_no_provider_is_downgraded(self, tmp_path):
         latest = tmp_path / "latest.txt"
         current = tmp_path / "current.txt"
@@ -111,40 +144,131 @@ class TestCheckProvidersNotDowngraded:
         m.check_providers_not_downgraded(_config(latest, current))
 
 
-class TestBuildProviderPreReleaseRequirements:
-    """Pre-releases are allowed for the providers only, never for the whole 
resolution."""
+class TestFindNewestVersionInPypi:
+    @pytest.mark.parametrize(
+        "releases, allow_pre_releases, expected",
+        [
+            pytest.param(
+                {"1.0.0": [_file()], "1.1.0": [_file()]},
+                False,
+                "1.1.0",
+                id="newest-final-release",
+            ),
+            pytest.param(
+                {"1.1.0": [_file()], "1.2.0rc1": [_file()]},
+                True,
+                "1.2.0rc1",
+                id="candidate-newer-than-the-release-wins-when-allowed",
+            ),
+            pytest.param(
+                {"1.1.0": [_file()], "1.2.0rc1": [_file()]},
+                False,
+                "1.1.0",
+                id="candidate-is-ignored-when-not-allowed",
+            ),
+            pytest.param(
+                {"1.1.0rc1": [_file()], "1.1.0": [_file()]},
+                True,
+                "1.1.0",
+                id="superseded-candidate-loses-to-its-release",
+            ),
+            pytest.param(
+                {"1.0.0": [_file()], "1.1.0": [_file(yanked=True)]},
+                False,
+                "1.0.0",
+                id="yanked-version-is-passed-over",
+            ),
+            pytest.param(
+                {"1.0.0": [_file()], "1.1.0": []},
+                False,
+                "1.0.0",
+                id="version-with-no-files-is-passed-over",
+            ),
+            pytest.param(
+                {"1.0.0": [_file()], "1.1.0": 
[_file(requires_python=">=3.12")]},
+                False,
+                "1.0.0",
+                id="version-excluded-by-requires-python-is-passed-over",
+            ),
+            pytest.param(
+                {"1.0.0": [_file()], "not-a-version": [_file()]},
+                False,
+                "1.0.0",
+                id="unparseable-version-is-passed-over",
+            ),
+            pytest.param(
+                {"1.0.0": [_file(requires_python=None)]},
+                False,
+                "1.0.0",
+                id="file-without-requires-python-is-installable",
+            ),
+            pytest.param(
+                {"1.0.0": [_file(requires_python="not-a-specifier")]},
+                False,
+                "1.0.0",
+                id="file-with-broken-requires-python-is-not-excluded",
+            ),
+            pytest.param({"1.0.0": [_file(yanked=True)]}, False, None, 
id="nothing-installable-is-unpinned"),
+            pytest.param({"1.0.0rc1": [_file()]}, False, None, 
id="only-a-candidate-is-unpinned"),
+        ],
+    )
+    def test_picks_the_newest_installable_version(self, monkeypatch, releases, 
allow_pre_releases, expected):
+        monkeypatch.setattr(m.requests, "get", lambda url, timeout: 
_FakeResponse({"releases": releases}))
+
+        newest = 
m.find_newest_version_in_pypi("apache-airflow-providers-amazon", "3.10", 
allow_pre_releases)
+
+        assert newest == expected
+
+    def test_unpublished_distribution_is_unpinned(self, monkeypatch):
+        monkeypatch.setattr(m.requests, "get", lambda url, timeout: 
_FakeResponse({}, status_code=404))
+
+        assert 
m.find_newest_version_in_pypi("apache-airflow-providers-brand-new", "3.10", 
True) is None
+
 
-    def 
test_every_requirement_names_a_provider_and_permits_a_pre_release(self, 
monkeypatch):
+class TestBuildPinnedProviderRequirements:
+    """Providers are pinned at what PyPI holds rather than left for the 
resolution to pick."""
+
+    def test_every_provider_is_pinned_to_its_newest_pypi_version(self, 
monkeypatch):
         monkeypatch.setattr(
             m,
             "get_all_active_provider_distributions",
             lambda python_version=None: [
                 "apache-airflow-providers-amazon",
                 "apache-airflow-providers-cncf-kubernetes",
+                "apache-airflow-providers-brand-new",
             ],
         )
+        newest = {
+            "apache-airflow-providers-amazon": "9.0.0rc1",
+            "apache-airflow-providers-cncf-kubernetes": "10.20.0",
+            "apache-airflow-providers-brand-new": None,
+        }
+        monkeypatch.setattr(
+            m, "find_newest_version_in_pypi", lambda dist, python_version, 
allow_pre_releases: newest[dist]
+        )
 
-        requirements = m.build_provider_pre_release_requirements("3.10")
-
-        assert requirements == [
-            "apache-airflow-providers-amazon>=0.0.0rc0",
-            "apache-airflow-providers-cncf-kubernetes>=0.0.0rc0",
+        assert m.build_pinned_provider_requirements("3.10", True) == [
+            "apache-airflow-providers-amazon==9.0.0rc1",
+            "apache-airflow-providers-cncf-kubernetes==10.20.0",
         ]
-        # The rc lower bound is what marks the package as explicit to uv; 
without it the
-        # requirement would not permit a pre-release at all.
-        assert all(requirement.endswith(">=0.0.0rc0") for requirement in 
requirements)
-        assert all(requirement.startswith("apache-airflow-providers-") for 
requirement in requirements)
 
-    def test_the_python_version_is_passed_through(self, monkeypatch):
-        """Providers excluded on a Python version must not be named for it."""
-        seen: list[str | None] = []
+    def 
test_the_python_version_and_the_pre_release_choice_reach_the_lookup(self, 
monkeypatch):
+        """Providers excluded on a Python version must not be named, nor 
pinned to a version it cannot install."""
+        seen_for_distributions: list[str | None] = []
+        seen_for_lookup: list[tuple[str, bool]] = []
 
         def fake_distributions(python_version=None):
-            seen.append(python_version)
-            return []
+            seen_for_distributions.append(python_version)
+            return ["apache-airflow-providers-amazon"]
+
+        def fake_lookup(distribution, python_version, allow_pre_releases):
+            seen_for_lookup.append((python_version, allow_pre_releases))
+            return "9.0.0"
 
         monkeypatch.setattr(m, "get_all_active_provider_distributions", 
fake_distributions)
+        monkeypatch.setattr(m, "find_newest_version_in_pypi", fake_lookup)
 
-        m.build_provider_pre_release_requirements("3.14")
+        m.build_pinned_provider_requirements("3.14", False)
 
-        assert seen == ["3.14"]
+        assert seen_for_distributions == ["3.14"]
+        assert seen_for_lookup == [("3.14", False)]

Reply via email to