unsloth/tests/python/test_windows_python_313_8_screen.py
Daniel Han 976a1152ad
Ask uv for a Python that can import torch, and skip the one that cannot (#7830)
* Ask uv for a Python that can import torch, and skip the one that cannot

Fixes #7803.

CPython 3.13.8 carries python/cpython#139783: inspect.getsourcelines() drops a
function body when a decorator is followed by a comment. That is the shape of
the @_overload_method blocks in torch 2.11's nn/modules/rnn.py, which are parsed
at import time, so `import torch` dies with IndentationError. 3.13.9 was an
expedited release carrying only that fix.

install.sh asked uv for a bare "3.13" and let it choose the patch. Measured with
uv 0.9.2 and only 3.13.8 present:

  --python 3.13             -> 3.13.8    then import torch: IndentationError
  --python >=3.13.9,<3.14   -> 3.13.12   then import torch: 2.11.0+cpu OK

So the request is the fix. PYTHON_SKIP names the releases that cannot run the
stack and _python_request turns a bare 3.13 into the range; a venv left on a
skipped interpreter by an earlier run is recreated, on any platform, which the
previous check could not do because it was gated on macOS arm64.

UV_MIN_VERSION also moves to 0.9.3, the first uv whose bundled manifest carries
3.13.9. That is belt-and-braces rather than the fix, since the range resolves on
0.9.2 too. Raising it pulls every 0.8.16-0.9.2 host into the refresh block, so
an existing uv in that range is no longer fatal when the network is unreachable.

Windows reaches such an interpreter differently: uv is handed a resolved path,
never a version, so it cannot pick the patch, but Find-CompatiblePython matches
on the minor version and would return an already-installed 3.13.8.
Remove-SkippedPython turns that into "not found" so the caller installs
$PythonFallbackFullVersion (3.13.13). The uv floor is left alone there, since
the uv-managed Python path is not taken on Windows.

* [pre-commit.ci] auto fixes from pre-commit.com hooks

for more information, see https://pre-commit.ci

* Screen the skipped patch inside the resolver, and never delete the only venv

Windows: Find-CompatiblePython matched on the minor, so the screen sat on
its result and every other caller of the resolver -- the two install
helpers among them -- could still be handed the interpreter the first
call had just rejected. Nulling the result also ended the search, so a
host with 3.13.8 and a healthy 3.12 failed instead of using the 3.12. The
patch is already in the string the minor comes from, so screening during
enumeration costs no extra subprocess and keeps the fallback ladder.

install.sh: the legacy-layout migration moves the old environment into
$VENV_DIR without arming the rollback, so a plain rm -rf before a
recreate that then fails leaves the machine with nothing. Move it aside
through the existing rollback machinery instead.

Ask uv for the series minus the skipped patches rather than for a floor
above them: an offline host, or a uv whose manifest predates 3.13.9, can
still have a good cached 3.13.7 that a floor would refuse. Measured with
uv 0.10.7, only 3.13.7 and 3.13.8 installed, --offline: "3.13" gives
3.13.8, ">=3.13.9,<3.14" errors, ">=3.13,<3.14,!=3.13.8" gives 3.13.7.

Hoist the install.ps1 extractions out of the f-strings: a backslash in an
f-string expression is a syntax error before 3.12 and the repo is 3.9+.

* [pre-commit.ci] auto fixes from pre-commit.com hooks

for more information, see https://pre-commit.ci

* Run the resolver driver only where its fake launcher can execute

The launcher is a /bin/sh script, and Windows has neither a shebang nor a
PATHEXT entry for an extensionless file, so Get-Command never finds it and
Find-CompatiblePython reports "none" whatever versions the tree holds.
Two cases failed on a real windows-latest runner and the third passed for
that reason rather than on merit. Skip the three on Windows, where the
rest of the file still covers the screen, and pair the negative case with
a positive control so a harness that cannot run the launcher fails
instead of quietly agreeing.

* Do not screen the interpreter for an install that never imports torch

Every entry in the skip list is there for one reason: it cannot import
torch. A --no-torch/-NoTorch install never does, so refusing the machine's
only 3.13 would send a locked-down GGUF-only host into a download it may
not be able to complete, over a package it will not install.

Also turn away anything that is not a plain X.Y before the arithmetic:
a relative --python path like 3.13/bin/python survived the globs, and
dash aborts the whole install with "Illegal number" rather than reaching
uv. And keep the uv version probe alive on an image with no awk, which is
precisely the host the offline exception around it exists to protect --
the pipeline exits 127 there and set -e was killing the install before
the code could treat the version as unreadable.

---------

Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
2026-08-05 03:03:39 -07:00

333 lines
12 KiB
Python

# SPDX-License-Identifier: AGPL-3.0-only
# Copyright 2026-present the Unsloth AI Inc. team. All rights reserved.
"""Windows must not build the venv on a CPython that cannot import torch.
CPython 3.13.8 carries python/cpython#139783: inspect.getsourcelines() drops a
function body when a decorator is followed by a comment, which is the shape of
the @_overload_method blocks torch/nn/modules/rnn.py parses at import time, so
`import torch` raises IndentationError (#7803).
Windows reaches such an interpreter differently from install.sh: uv is handed a
resolved path rather than a version, so it never picks the patch itself, but
Find-CompatiblePython matches on the *minor* version and would happily return an
already-installed 3.13.8. Remove-SkippedPython is what turns that into "not
found", so the caller installs $PythonFallbackFullVersion instead.
The function is extracted from install.ps1 and executed under pwsh rather than
reimplemented, so the test cannot drift from the text the installer runs.
"""
from __future__ import annotations
import os
import re
import shutil
import stat
import subprocess
from pathlib import Path
import pytest
REPO_ROOT = Path(__file__).resolve().parents[2]
INSTALL_PS1 = REPO_ROOT / "install.ps1"
pytestmark = pytest.mark.skipif(
shutil.which("pwsh") is None, reason = "pwsh is required to execute install.ps1 blocks"
)
def _extract(pattern: str) -> str:
source = INSTALL_PS1.read_text(encoding = "utf-8")
match = re.search(pattern, source, flags = re.DOTALL)
assert match is not None, f"install.ps1 no longer contains {pattern!r}"
return match.group(0)
def _blocks() -> tuple:
"""The skip list and the screen, straight out of install.ps1.
Hoisted out of the f-strings below: a backslash inside an f-string expression
is a syntax error before 3.12, and this repo is 3.9+ (ruff targets py311).
"""
return (
_extract(
r" # Patch releases the stack cannot run.*?"
r"if \(\$SkipTorch\) \{ \$PythonSkip = @\(\) \}"
),
_extract(r" function Remove-SkippedPython \{.*?\n \}"),
)
def _fake_python(tmp_path: Path, version: str) -> Path:
"""An executable that reports ``version`` for the resolver's probe."""
if os.name == "nt":
exe = tmp_path / "python.cmd"
exe.write_text(f"@echo off\r\necho {version}\r\n", encoding = "utf-8")
return exe
exe = tmp_path / "python"
exe.write_text(f'#!/bin/sh\necho "{version}"\n', encoding = "utf-8")
exe.chmod(exe.stat().st_mode | stat.S_IEXEC | stat.S_IXGRP | stat.S_IXOTH)
return exe
def _run(tmp_path: Path, version: str | None) -> str:
candidate = (
"$null"
if version is None
else f'@{{ Version = "3.13"; Path = "{_fake_python(tmp_path, version)}" }}'
)
skip_block, screen_block = _blocks()
script = f"""
$ErrorActionPreference = "Stop"
$SkipTorch = $false
# Write-Host, like the real substep: Write-Output would put the message on
# the pipeline, so the function would return @(message, $null) and every
# `if ($DetectedPython)` downstream would read it as truthy.
function substep {{ param($m, $c) Write-Host "SUBSTEP: $m" }}
{skip_block}
{screen_block}
$result = Remove-SkippedPython ({candidate})
if ($null -eq $result) {{ Write-Output "RESULT: rejected" }}
else {{ Write-Output "RESULT: kept" }}
"""
completed = subprocess.run(
["pwsh", "-NoProfile", "-NonInteractive", "-Command", script],
capture_output = True,
text = True,
)
return completed.stdout + completed.stderr
def test_a_skipped_patch_is_rejected(tmp_path):
out = _run(tmp_path, "3.13.8")
assert "RESULT: rejected" in out, out
assert "cannot import torch" in out, "the user should be told why: " + out
def test_a_good_patch_of_the_same_minor_is_kept(tmp_path):
# The screen is per patch: 3.13 itself is fine and must not be refused.
out = _run(tmp_path, "3.13.13")
assert "RESULT: kept" in out, out
def test_nothing_found_stays_nothing(tmp_path):
out = _run(tmp_path, None)
assert "RESULT: rejected" in out, out
def test_an_unreadable_interpreter_is_not_treated_as_bad(tmp_path):
# A probe that cannot run is not evidence of a bad version, and refusing it
# would send a working machine down the install path for no reason.
missing = tmp_path / "does-not-exist"
skip_block, screen_block = _blocks()
script = f"""
$ErrorActionPreference = "Stop"
$SkipTorch = $false
# Write-Host, like the real substep: Write-Output would put the message on
# the pipeline, so the function would return @(message, $null) and every
# `if ($DetectedPython)` downstream would read it as truthy.
function substep {{ param($m, $c) Write-Host "SUBSTEP: $m" }}
{skip_block}
{screen_block}
$result = Remove-SkippedPython (@{{ Version = "3.13"; Path = "{missing}" }})
if ($null -eq $result) {{ Write-Output "RESULT: rejected" }}
else {{ Write-Output "RESULT: kept" }}
"""
completed = subprocess.run(
["pwsh", "-NoProfile", "-NonInteractive", "-Command", script],
capture_output = True,
text = True,
)
assert "RESULT: kept" in completed.stdout + completed.stderr, (
completed.stdout + completed.stderr
)
def test_the_resolver_is_screened_at_every_entry_point():
"""A bare Find-CompatiblePython in the install flow would defeat the screen."""
source = INSTALL_PS1.read_text(encoding = "utf-8")
flow = source[source.index("# ── Install Python if no compatible version") :]
flow = flow[: flow.index("# ── Install uv ──")]
bare = [
line.strip()
for line in flow.splitlines()
if "= Find-CompatiblePython" in line and "Remove-SkippedPython" not in line
]
assert not bare, f"unscreened resolver calls in the install flow: {bare}"
# ── The screen inside the resolver ──
# The window above starts at the install step, so it never sees the recovery
# paths: Install-PythonFromPythonOrg and Install-X64Python both end in a bare
# `return (Find-CompatiblePython)`. Screening every candidate as it is
# enumerated is what makes those safe, and is also what lets the resolver carry
# on to its next minor instead of giving up on the machine.
def _every_version_match_screens_the_patch() -> list[str]:
body = _extract(r" function Find-CompatiblePython \{.*?\n \}")
lines = body.splitlines()
unscreened = []
for i, line in enumerate(lines):
if 'match "Python' not in line:
continue
window = "\n".join(lines[i : i + 9])
if "$PythonSkip -contains" not in window:
unscreened.append(line.strip())
return unscreened
def test_every_enumerated_candidate_is_screened():
assert (
len(
[
l
for l in _extract(r" function Find-CompatiblePython \{.*?\n \}").splitlines()
if 'match "Python' in l
]
)
== 3
), "the resolver's enumeration sites moved; re-check the screen"
assert not _every_version_match_screens_the_patch(), (
"Find-CompatiblePython enumerates a candidate without checking $PythonSkip: "
f"{_every_version_match_screens_the_patch()}"
)
# The launcher below is a /bin/sh script. Windows has no shebang and no PATHEXT
# entry for an extensionless file, so `Get-Command py` does not find it and the
# resolver reports "none" whatever the versions are -- which would make the
# negative case pass for the wrong reason. The PowerShell under test is the same
# text on every platform, and pwsh runs it here, so these three cases run on
# POSIX and the rest of the file still covers Windows.
_POSIX_LAUNCHER_ONLY = pytest.mark.skipif(
os.name == "nt", reason = "the fake py launcher is a /bin/sh script"
)
def _fake_launcher(root: Path, versions: dict[str, str]) -> Path:
"""A `py` launcher over fake interpreters, one per minor in ``versions``."""
root.mkdir(parents = True, exist_ok = True)
branches = []
for minor, full in versions.items():
exe = root / f"python{minor.replace('.', '')}"
# -S -c "import sys; print(sys.base_prefix)" for the conda screen.
exe.write_text('#!/bin/sh\necho "/usr"\n', encoding = "utf-8")
exe.chmod(0o755)
branches.append(f' {minor}) ver="{full}"; exe="{exe}" ;;')
launcher = root / "py"
launcher.write_text(
"#!/bin/sh\n"
'case "$1" in\n'
+ "\n".join(f" -{b.lstrip()}" for b in branches)
+ "\n *) exit 1 ;;\nesac\n"
"shift\n"
'case "$1" in\n'
' --version) echo "Python $ver" ;;\n'
' -S) echo "$exe" ;;\n'
" *) exit 1 ;;\n"
"esac\n",
encoding = "utf-8",
)
launcher.chmod(0o755)
return launcher
def _resolve(tmp_path: Path, versions: dict[str, str]) -> str:
"""Run the real Find-CompatiblePython over ``versions`` and report the hit."""
root = tmp_path / "bin"
_fake_launcher(root, versions)
skip_block, screen_block = _blocks()
# Hoisted for the same reason as _blocks: a backslash in an f-string
# expression does not parse before 3.12.
conda_block = _extract(r" function Test-IsCondaPython \{.*?\n \}")
tag_block = _extract(r" function Get-PythonPlatformTag \{.*?\n \}")
resolver_block = _extract(r" function Find-CompatiblePython \{.*?\n \}")
script = f"""
$ErrorActionPreference = "Stop"
$SkipTorch = $false
$env:PATH = "{root}"
$PythonVersion = "3.13"
function substep {{ param($m, $c) Write-Host "SUBSTEP: $m" }}
function Get-HostMachineArch {{ return "x86_64" }}
{skip_block}
$script:CondaSkipPattern = '(?i)(conda|miniconda|anaconda|miniforge|mambaforge)'
{conda_block}
{tag_block}
{resolver_block}
$found = Find-CompatiblePython
if ($null -eq $found) {{ Write-Output "RESULT: none" }}
else {{ Write-Output "RESULT: $($found.Version)" }}
"""
completed = subprocess.run(
["pwsh", "-NoProfile", "-NonInteractive", "-Command", script],
capture_output = True,
text = True,
)
out = completed.stdout + completed.stderr
match = re.search(r"RESULT: (\S+)", out)
assert match is not None, out
return match.group(1)
@_POSIX_LAUNCHER_ONLY
def test_the_resolver_falls_through_to_the_next_minor(tmp_path):
# The offline/locked-down case: 3.13.8 and a healthy 3.12 both installed and
# nothing installable. Ending the search on the 3.13 would leave the caller
# with a Python that cannot import torch; refusing it outright would fail a
# machine that has a perfectly good interpreter one entry down the list.
assert _resolve(tmp_path, {"3.13": "3.13.8", "3.12": "3.12.11"}) == "3.12"
@_POSIX_LAUNCHER_ONLY
def test_a_good_preferred_minor_still_wins(tmp_path):
assert _resolve(tmp_path, {"3.13": "3.13.13", "3.12": "3.12.11"}) == "3.13"
@_POSIX_LAUNCHER_ONLY
def test_nothing_usable_is_still_nothing(tmp_path):
# Paired with a positive control over the same tree, because "none" is also
# what a harness that cannot run the launcher at all reports: without the
# control this case would pass on a machine where it proves nothing.
assert _resolve(tmp_path / "good", {"3.13": "3.13.13"}) == "3.13"
assert _resolve(tmp_path / "bad", {"3.13": "3.13.8"}) == "none"
@_POSIX_LAUNCHER_ONLY
def test_no_torch_mode_keeps_the_skipped_patch(tmp_path):
"""The list is about `import torch`; -NoTorch never imports it.
A locked-down GGUF-only machine whose only Python is 3.13.8 would otherwise
be pushed into winget/python.org recovery it may not be able to complete.
"""
root = tmp_path / "bin"
_fake_launcher(root, {"3.13": "3.13.8"})
skip_block, screen_block = _blocks()
conda_block = _extract(r" function Test-IsCondaPython \{.*?\n \}")
tag_block = _extract(r" function Get-PythonPlatformTag \{.*?\n \}")
resolver_block = _extract(r" function Find-CompatiblePython \{.*?\n \}")
script = f"""
$ErrorActionPreference = "Stop"
$SkipTorch = $true
$env:PATH = "{root}"
$PythonVersion = "3.13"
function substep {{ param($m, $c) Write-Host "SUBSTEP: $m" }}
function Get-HostMachineArch {{ return "x86_64" }}
{skip_block}
$script:CondaSkipPattern = '(?i)(conda|miniconda|anaconda|miniforge|mambaforge)'
{conda_block}
{tag_block}
{resolver_block}
$found = Find-CompatiblePython
if ($null -eq $found) {{ Write-Output "RESULT: none" }}
else {{ Write-Output "RESULT: $($found.Version)" }}
"""
completed = subprocess.run(
["pwsh", "-NoProfile", "-NonInteractive", "-Command", script],
capture_output = True,
text = True,
)
out = completed.stdout + completed.stderr
assert "RESULT: 3.13" in out, out