dep_freshness: Python's latest is python.org's newest release, not the interpreter
Some checks failed
dependency freshness / dependencies on newest stable (push) Has been cancelled
file length / file-length (push) Has been cancelled
md-naming / markdown naming convention (push) Has been cancelled
palette drift / palette (push) Has been cancelled
repo-contract / repo contract (push) Has been cancelled
shared gate tests / gate unit tests (push) Has been cancelled

toolchain:python's "latest" was platform.python_version(), so the verdict
depended on the machine running the gate: setup-python "3.x" gave kuhyx/spellcode
3.15.0 on one runner and 3.14.8 on another, making its transitive:pyodide
allowlist entry dead (exit 2) on one and required (exit 1) on the other.

Now registries/toolchain.python_latest() reads python.org's release API
(is_published, pre_release=false), keeps only exact "Python X.Y.Z" names (the
feed also lists "Python install manager 26.4"), and takes the newest stable.

The never_cached() carve-out is gone: it existed because a cached
*interpreter* version from an older runner image (3.13.15) was served to a
3.14.7 job in screen-locker's CI. A registry answer is safe to cache, so
python now goes through the normal TTL cache, prefetch, and offline fallback
(expired cache -> DEGRADED; no cache -> unavailable), never the local
interpreter.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01A4VrLZn9YjhPuoxTj54WKk
This commit is contained in:
Krzysztof Rudnicki 2026-10-20 22:39:25 +02:00
parent 9733a29a65
commit 08a4010a16
8 changed files with 155 additions and 76 deletions

View File

@ -37,11 +37,10 @@ jobs:
repository: kuhyx/utils
path: .utils
# "3.x", never a pinned minor: the Python target IS the interpreter the
# gate runs under (registries/toolchain.python_installed), so a runner
# held a release behind this machine reports a repo's `requires-python`
# as excluding "latest" while the same check passes locally. flax-docs-mcp
# hit exactly that -- `>=3.14` green here, red on a 3.13 runner.
# Any recent Python runs the gate. The Python TARGET is python.org's
# newest release (registries/toolchain.python_latest), not this
# interpreter: when it was, "3.x" resolved to 3.15.0 on one runner and
# 3.14.8 on the next, and spellcode's allowlist flipped dead/required.
- name: Set up Python
uses: actions/setup-python@v5
with:

View File

@ -104,9 +104,14 @@ MAX_WORKERS: Final = 8
# against LTS, so the gate targets LTS. One-line switch to "current".
NODE_CHANNEL: Final = "lts"
# The interpreter is pacman-managed here. A gate satisfiable only by fighting
# the distro gets disabled, so Python is compared to what is INSTALLED.
PYTHON_SOURCE: Final = "installed"
# Python is a registry answer like every other toolchain (kuhy, 2026-10-20).
# It used to be the interpreter running the gate, which made the verdict
# depend on the machine: `setup-python` "3.x" gave spellcode 3.15.0 on one
# runner and 3.14.8 on the next. See registries/toolchain.py for the source.
PYTHON_RELEASES: Final = (
"https://www.python.org/api/v2/downloads/release/"
"?is_published=true&pre_release=false"
)
# --- Manifest discovery -----------------------------------------------------

View File

@ -1,19 +1,24 @@
"""Latest stable Flutter/Dart, Node and Python, for the toolchain checks (Q3).
"""Latest stable Flutter/Dart, Node, Python and Gradle, for the toolchain checks (Q3).
Python is deliberately compared against the INSTALLED interpreter rather than
python.org: on Arch it is pacman-managed, and a gate that can only be satisfied
by fighting the distro gets switched off.
Python comes from python.org's release API, NOT the interpreter running the
gate. That used to be the target, and it made the verdict a property of the
machine: `setup-python` "3.x" resolved to 3.15.0 on one runner and 3.14.8 on
another, so spellcode's `transitive:` allowlist entry was dead on one and
required on the other (2026-10-20). python.org is first-party, filters
pre-releases server-side, and answers in one request; endoflife.date is a
third party, and cpython's git tags would mean walking every rc tag.
"""
from __future__ import annotations
import platform
import re
from dep_freshness._tables import (
FLUTTER_RELEASES,
GRADLE_RELEASES,
NODE_CHANNEL,
NODE_RELEASES,
PYTHON_RELEASES,
)
from dep_freshness.registries.http import get_json
from dep_freshness.versions import newest_stable
@ -47,9 +52,22 @@ def node_latest(channel: str = NODE_CHANNEL) -> str | None:
return newest_stable(candidates)
def python_installed() -> str:
"""The interpreter this gate is running under — the Python target."""
return platform.python_version()
# Exactly "Python X.Y.Z": the feed also lists "Python install manager 26.4"
# (the Windows launcher), which read as a version would be Python 26.
_CPYTHON_RELEASE = re.compile(r"Python (\d+\.\d+\.\d+)")
def python_latest() -> str | None:
"""Newest final CPython release on python.org."""
payload = get_json(PYTHON_RELEASES)
if not payload:
return None
candidates = []
for release in payload:
match = _CPYTHON_RELEASE.fullmatch(release.get("name") or "")
if match and not release.get("pre_release"):
candidates.append(match.group(1))
return newest_stable(candidates)
def gradle_latest() -> str | None:

View File

@ -66,24 +66,12 @@ def _toolchain_latest(name: str) -> str | None:
if name == "node":
return tc.node_latest()
if name == "python":
return tc.python_installed()
return tc.python_latest()
if name == "gradle":
return tc.gradle_latest()
return None
def never_cached(ecosystem: str, name: str) -> bool:
"""True for answers that describe THIS process, not a remote registry.
`toolchain:python` is `platform.python_version()`: free to compute, and
different on every machine. Caching it made screen-locker's CI red on
2026-08-28 -- the `actions/cache` restore-key handed a job running 3.14.7
a 3.13.15 answer written by an older runner image, and the gate declared
the repo's own `requires-python = ">=3.14"` to exclude "latest".
"""
return ecosystem == TOOLCHAIN and name == "python"
def _fetch(ecosystem: str, package: str) -> str | None:
if ecosystem == TOOLCHAIN:
return _toolchain_latest(package)
@ -105,11 +93,8 @@ class Resolver:
wanted = {
(d.ecosystem, d.name)
for d in deps
if not never_cached(d.ecosystem, d.name)
and (
not (entry := self.cache.get(d.ecosystem, d.name))
if not (entry := self.cache.get(d.ecosystem, d.name))
or not entry.fresh(d.ecosystem)
)
}
if not wanted:
return
@ -130,8 +115,6 @@ class Resolver:
self.cache.save()
def latest(self, dep: Dep) -> Answer:
if never_cached(dep.ecosystem, dep.name):
return Answer(_fetch(dep.ecosystem, dep.name))
entry = self.cache.get(dep.ecosystem, dep.name)
if entry and entry.fresh(dep.ecosystem):
return Answer(entry.version)

View File

@ -37,11 +37,10 @@ jobs:
repository: kuhyx/utils
path: .utils
# "3.x", never a pinned minor: the Python target IS the interpreter the
# gate runs under (registries/toolchain.python_installed), so a runner
# held a release behind this machine reports a repo's `requires-python`
# as excluding "latest" while the same check passes locally. flax-docs-mcp
# hit exactly that -- `>=3.14` green here, red on a 3.13 runner.
# Any recent Python runs the gate. The Python TARGET is python.org's
# newest release (registries/toolchain.python_latest), not this
# interpreter: when it was, "3.x" resolved to 3.15.0 on one runner and
# 3.14.8 on the next, and spellcode's allowlist flipped dead/required.
- name: Set up Python
uses: actions/setup-python@v5
with:

View File

@ -0,0 +1,110 @@
"""`toolchain:python` is a registry answer, never the interpreter running the gate.
kuhyx/spellcode went red on one runner and green on another: `setup-python`
with "3.x" handed one job 3.15.0 and the next 3.14.8, so a `transitive:`
allowlist entry was dead (exit 2) on one machine and required (exit 1) on the
other. The target must not depend on where the gate happens to run.
"""
from __future__ import annotations
import platform
from pathlib import Path
import pytest
from dep_freshness.models import Dep
from dep_freshness.registries import http
from dep_freshness.registries import toolchain as tc
from dep_freshness.resolve import Resolver, _toolchain_latest
# Shaped like python.org's /api/v2/downloads/release/ rows, trimmed.
RELEASES = [
{"name": "Python 2.7.18", "pre_release": False},
{"name": "Python 3.14.8", "pre_release": False},
{"name": "Python 3.15.0", "pre_release": False},
# The Windows install manager is listed as a "release" with version 100;
# read naively it is Python 26.4.
{"name": "Python install manager 26.4", "pre_release": False},
{"name": "Python 3.16.0a1", "pre_release": True},
{"name": "Python 3.16.0", "pre_release": True}, # flagged, so not final
]
@pytest.fixture
def feed(monkeypatch):
def install(payload):
monkeypatch.setattr(tc, "get_json", lambda *_a, **_k: payload)
return install
def python_dep() -> Dep:
return Dep(
ecosystem="toolchain", name="python", constraint="3.14.8",
path=Path(".python-version"), line=1,
)
@pytest.fixture
def no_interpreter(monkeypatch):
"""Any read of the running interpreter's version is a failure."""
monkeypatch.setattr(
platform,
"python_version",
lambda: pytest.fail("the target must not come from this interpreter"),
)
@pytest.mark.parametrize("interpreter", ["3.14.8", "3.15.0"])
def test_latest_is_the_same_whatever_interpreter_runs_the_gate(
feed, monkeypatch, interpreter
):
feed(RELEASES)
monkeypatch.setattr(platform, "python_version", lambda: interpreter)
assert _toolchain_latest("python") == "3.15.0"
def test_the_registry_answer_skips_pre_releases_and_the_install_manager(feed):
feed(RELEASES)
assert tc.python_latest() == "3.15.0"
def test_a_missing_feed_is_unknown(feed):
feed(None)
assert tc.python_latest() is None
def test_rows_without_a_name_are_ignored(feed):
feed([{"pre_release": False}, {"name": "Python 3.14.8"}])
assert tc.python_latest() == "3.14.8"
@pytest.fixture
def resolver(tmp_path):
from dep_freshness.cache import Cache
return Resolver(Cache(tmp_path / "registry.json"))
def test_the_answer_is_cached_like_any_registry(resolver, feed, no_interpreter):
feed(RELEASES)
resolver.prefetch([python_dep()])
entry = resolver.cache.get("toolchain", "python")
assert entry is not None and entry.version == "3.15.0"
def test_offline_serves_the_cached_answer_as_degraded(resolver, no_interpreter):
resolver.cache.put("toolchain", "python", "3.15.0")
resolver.cache._data["toolchain/python"]["fetched_at"] = 0.0 # long expired
http.force_offline()
answer = resolver.latest(python_dep())
assert answer.version == "3.15.0"
assert answer.stale_cache_days is not None
def test_offline_with_no_cache_is_unavailable(resolver, no_interpreter):
http.force_offline()
answer = resolver.latest(python_dep())
assert answer.version is None
assert answer.unavailable

View File

@ -85,30 +85,6 @@ def test_refresh_discards_the_existing_cache(cache_dir, monkeypatch):
assert resolver.latest(dep()).version == "1.6.0"
def test_the_python_target_ignores_the_cache_entirely(resolver, monkeypatch):
"""screen-locker went red on a cached interpreter from another machine.
`actions/cache` restored a `dep-freshness-` entry written by an older
runner image, so a job that `setup-python` had put on 3.14.7 was told the
"current toolchain" was 3.13.15 -- and the repo's own
`requires-python = ">=3.14"` was reported as excluding latest. The answer
describes this process and costs nothing, so it is never cached.
"""
resolver.cache.put("toolchain", "python", "3.13.15")
monkeypatch.setattr(tc, "python_installed", lambda: "3.14.7")
assert resolver.latest(dep("toolchain", "python")).version == "3.14.7"
def test_prefetch_does_not_warm_the_python_target(resolver, monkeypatch):
monkeypatch.setattr(
http, "get_json", lambda *a, **k: pytest.fail("no lookup is needed")
)
monkeypatch.setattr(
tc, "python_installed", lambda: pytest.fail("prefetch must skip python")
)
resolver.prefetch([dep("toolchain", "python")])
def test_git_tags_get_a_longer_ttl_than_registries():
assert ttl_for("gittag") > ttl_for("pub")
@ -153,12 +129,6 @@ def test_toolchain_routing(monkeypatch, name, expected):
assert _toolchain_latest(name) == expected
def test_python_targets_the_installed_interpreter():
"""Never python.org: on Arch the interpreter is pacman-managed."""
import platform
assert _toolchain_latest("python") == platform.python_version()
def test_an_unknown_toolchain_has_no_target():
assert _toolchain_latest("go") is None

View File

@ -60,11 +60,6 @@ def test_node_with_no_feed_is_unknown(feed):
assert tc.node_latest() is None
def test_python_reports_the_running_interpreter():
import platform
assert tc.python_installed() == platform.python_version()
LS_REMOTE = (
"aaa\trefs/tags/crdt_sync_dart-v0.10.0\n"
"bbb\trefs/tags/crdt_sync_dart-v0.11.0\n"