diff --git a/scripts/refresh_nightly_stacks.py b/scripts/refresh_nightly_stacks.py index 35d8cae..0b49962 100644 --- a/scripts/refresh_nightly_stacks.py +++ b/scripts/refresh_nightly_stacks.py @@ -35,6 +35,7 @@ r"^(?P[A-Za-z0-9_]+)-(?P[A-Za-z0-9.!+]+)" r"-(?P[a-z0-9]+)-(?P[a-z0-9]+)-(?P[a-z0-9_.]+)\.whl$" ) +ANY_ABI = "any" DEV_DATE_RE = re.compile(r"\.dev(\d{8})\+") ANCHOR_TEXT_RE = re.compile(r">([^<>]+\.whl)") @@ -64,12 +65,22 @@ def abi_for_wheel(pytag, abitag): """`major.minor` for a standard CPython wheel; None for anything else (freethreaded cp31Xt, abi3, pypy) - the desktop app's venvs are standard CPython.""" + if pytag == "py3" and abitag == "none": + return ANY_ABI m = re.fullmatch(r"cp3(\d+)", pytag) if not m or abitag != pytag: return None return f"3.{m.group(1)}" +def shared_abis(abi_sets): + """ABIs common to every set; a set holding ANY_ABI fits all of them.""" + concrete = [s for s in abi_sets if ANY_ABI not in s] + if not concrete: + return set() + return set.intersection(*concrete) + + def fetch_index(tag, package): """(version, platform, abi) triples for one package's dated nightly wheels on desktop-relevant platforms.""" @@ -142,7 +153,7 @@ def resolve_tag(tag, today): for p in ("win32", "linux"): if not all(p in s["plat_abis"] for s in slots.values()): continue - shared = set.intersection(*(s["plat_abis"][p] for s in slots.values())) + shared = shared_abis([s["plat_abis"][p] for s in slots.values()]) if shared: abis_by_platform[p] = shared if not abis_by_platform: diff --git a/scripts/test_torch_index_scripts.py b/scripts/test_torch_index_scripts.py index 03509fc..ad25db4 100644 --- a/scripts/test_torch_index_scripts.py +++ b/scripts/test_torch_index_scripts.py @@ -317,6 +317,23 @@ def test_parse_index_filters_wheels(self): ], ) + def test_parse_index_accepts_py3_none_wheels(self): + html = index_html( + [ + "torchvision-0.30.0.dev20261008%2Bcu132-py3-none-win_amd64.whl", + "torchvision-0.30.0.dev20261008%2Bcu132-py3-none-manylinux_2_28_x86_64.whl", + "torchvision-0.30.0.dev20261008%2Bcu132-py3-none-manylinux_2_28_aarch64.whl", + ] + ) + wheels = refresher.parse_index("torchvision", "cu132", html) + self.assertEqual( + sorted(wheels), + [ + ("0.30.0.dev20261008+cu132", "linux", "any"), + ("0.30.0.dev20261008+cu132", "win32", "any"), + ], + ) + def fake_indexes(data): """fetch_index replacement: data[package] = list of wheel triples.""" @@ -359,6 +376,33 @@ def test_full_coverage_keeps_both_platforms(self): self.assertEqual(entry["pythonAbis"], ["3.12", "3.13"]) self.assertEqual(entry["packages"]["torch"], f"2.14.0.dev{day}+cu132") + def test_py3_none_companions_take_torch_abis(self): + today = utc_today() + day = (today - timedelta(days=1)).strftime("%Y%m%d") + both = {"win32": ["3.12", "3.13"], "linux": ["3.12"]} + universal = {"win32": ["any"], "linux": ["any"]} + data = { + "torch": triples("2.16.0", day, both), + "torchvision": triples("0.30.0", day, universal), + "torchaudio": triples("2.11.0", day, universal), + } + entry, err, _ = self.resolve(data) + self.assertIsNone(err) + self.assertEqual(sorted(entry["platforms"]), ["linux", "win32"]) + self.assertEqual(entry["pythonAbis"], ["3.12"]) + + def test_all_py3_none_has_no_abi_to_advertise(self): + today = utc_today() + day = (today - timedelta(days=1)).strftime("%Y%m%d") + universal = {"win32": ["any"]} + data = { + pkg: triples(ver, day, universal) + for pkg, ver in (("torch", "2.16.0"), ("torchvision", "0.30.0"), ("torchaudio", "2.11.0")) + } + entry, err, _ = self.resolve(data) + self.assertIsNone(entry) + self.assertIsNotNone(err) + def test_one_platform_abi_gap_shrinks_global_abis(self): # torchaudio missing linux 3.12: 3.12 must NOT be advertised # globally, but both platforms stay listed via the shared 3.13.