Require Python 3.13 and build the virtualenv from the checked interpreter - #38624
Require Python 3.13 and build the virtualenv from the checked interpreter#38624antiguru wants to merge 3 commits into
Conversation
…eter `bin/pyactivate` validates the interpreter running it and then hands virtualenv creation to `uv venv` without naming an interpreter. `uv` resolves one by its own preference order, which favors uv-managed installs over the system Python, so the virtualenv can end up on a different and older Python than the one the check just accepted. On a machine with system Python 3.14 and a uv-managed 3.10 present, the virtualenv is built on 3.10. Passing `sys.executable` removes that second, independent choice and makes the check authoritative. It also aligns the two creation paths, since the `venv.create` fallback right below already builds from the running interpreter. That fallback passes `clear=True`, and `uv venv` is now given `--clear` to match. Control reaches this branch only when the virtualenv is missing or its Python will not execute, which is exactly when it should be replaced. Without the flag `uv` refuses to touch an existing directory and aborts, so a virtualenv left behind without its `dep_stamp` wedges every later invocation instead of being rebuilt. The floor moves from 3.10 to 3.13. Python 3.10 has been security-only for years and reaches end of life in October 2026, and the tree had already drifted past it: two mzcompose files import `LiteralString` from `typing`, which needs 3.11. That went unnoticed because `ci/test/lint-main/checks/check-python-version.sh` only byte-compiles the tree, and compilation does not resolve imports, so a newer-than-floor import passes the check and fails at run time. 3.13 is what the CI builder already runs, since that is the system Python in Debian 13. Raising `target-version` lets ruff replace `datetime.timezone.utc` with the `datetime.UTC` alias and pull `LiteralString` back from `typing`, which is the whole of the mechanical churn here. Ruff trails at `py312` because the pinned version predates 3.13 and rejects `py313`. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
096a98c to
60fe117
Compare
QA LLM Review1. MEDIUM -- Floor bump is not enforced against the interpreter that actually runs the tree
The version gate only inspects the bootstrap interpreter; an existing DetailsReproduced with system
+MIN_HEXVERSION = 0x030D0000
...
try:
os.stat(stamp_path)
- subprocess.check_call([python, "-c", ""])
+ subprocess.check_call(
+ [python, "-c", f"import sys; sys.exit(sys.hexversion < {MIN_HEXVERSION})"]
+ )Already tracked in MaterializeInc/database-issues#2428. 2. LOW -- Documented Python support policy still says 3.12
The guide states the minimum supported version as the default Python of the most recent Ubuntu LTS and names 3.12 (24.04), while DetailsThe floor now lives in three places with two different values: 3. LOW --
|
… bootstrap The version gate in `main` inspects the interpreter that runs the script, while the code the repository executes runs inside `misc/python/venv`. Those need not agree, because the virtualenv outlives the interpreter it was built from, and the reuse path only confirmed that a `dep_stamp` existed and that the virtualenv's Python could execute at all. Raising the minimum therefore left every existing virtualenv below it in place, and this branch is the first to depend on that difference, since `datetime.UTC` does not exist before 3.11. The liveness probe now also reports the version, so a virtualenv older than the minimum takes the existing recreation path. `uv venv --clear` refuses a target directory that is not a valid virtualenv and suggests `--force`. That refusal is an error rather than the `FileNotFoundError` the surrounding code catches, so it would abort with a traceback instead of falling back to `venv.create`. The half-finished directory this leaves behind is the case the comment above the check already describes, and `--force` is the flag that actually matches `clear=True`. The developer guide named 3.12 and derived it from the default Python in the most recent Ubuntu LTS. That release is now 26.04 "Resolute Raccoon", which ships 3.14, so the stated rule no longer produced the stated version and taken literally would demand a version newer than the one CI runs. The guide now gives the minimum directly and explains that it tracks the CI builder image, noting that a current LTS satisfies it. The minimum lives in `MIN_HEXVERSION` and `MIN_VERSION` so the gate, the probe, and the operator-facing messages cannot drift apart. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
|
All three addressed in 4925340. Finding 1 in particular is a good catch, and it is mine: I hit exactly this while developing the branch, deleted the virtualenv by hand to get past it, and then fixed only the creation path. 1. Floor not enforced against the virtualenv. Confirmed. With a 3.10 virtualenv and subprocess.check_call(
[python, "-c", f"import sys; sys.exit(sys.hexversion < {MIN_HEXVERSION})"]
)A failing probe falls into the existing recreation path, which 2. Guide says 3.12. Confirmed, and worse than stale. The rule is "the default Python in the most recent Ubuntu LTS", but that release is now 26.04 "Resolute Raccoon", which ships 3.14. So the sentence named 3.12 while its own rule produced 3.14, and taken literally it would demand a version newer than the 3.13 CI actually runs. The guide now states the minimum directly and explains that it tracks the CI builder image, noting a current LTS satisfies it. You were right that the PR body justified the number from Debian 13 while the guide claimed a different rule; the guide now says what we actually do. 3. The minimum now lives in On database-issues#2428: the reuse path is now gated, so the specific gap this branch would have tripped is closed. The broader one is not. 🤖 Posted by Claude Code |
QA LLM Review1. HIGH --
|
`uv venv --force` was added in uv 0.11.17. Earlier versions exit with a usage error, which reaches `subprocess.check_call` as a `CalledProcessError` and not as the `FileNotFoundError` that selects the `venv.create` fallback, so `bin/pyactivate` would abort. Everything in the tree runs through that script, and the CI builder image installs uv 0.9.10, so the flag cannot be used here. The version probe added alongside it makes the timing worse rather than academic. Any developer holding a virtualenv below the new minimum takes the recreation path on their first invocation, which is exactly the path that would have failed, and deleting the virtualenv by hand leads to the same call. Removing the directory before invoking `uv` behaves the same on every version and needs no flag. It also settles what `--clear` and `--force` disagree about, a path that exists but is not a virtualenv, which is the half-finished state the comment above the check already describes. The `venv.create` fallback is unaffected, since it creates a directory that does not exist. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
|
Confirmed and fixed in c2cca65. This one is squarely on me, and my previous reply made it worse: I wrote that Reproduced exactly as you describe. Nonzero exit, so I took your patch rather than a narrower fix, because deleting the directory ourselves is the thing both flags were reaching for: shutil.rmtree(venv_dir, ignore_errors=True)
subprocess.check_call(["uv", "venv", "--python", sys.executable, venv_dir])It also disposes of finding 3 from your first review properly instead of by flag. A path that exists but is not a virtualenv is simply gone before Verified against a real uv 0.9.10, not a reasoned argument, by putting a shim ahead of it on The stack on top, #38625 and #38626, has been rebased onto this. 🤖 Posted by Claude Code |
bin/pyactivatevalidates the interpreter running it and then hands virtualenv creation touv venvwithout naming an interpreter.uvresolves one by its own preference order, which favors uv-managed installs over the system Python, so the virtualenv can end up on a different and older Python than the one the check just accepted. Passingsys.executableremoves that second, independent choice and makes the check authoritative. It also aligns the two creation paths, since thevenv.createfallback right below already builds from the running interpreter.That fallback passes
clear=True, anduv venvis now given--clearto match. Control reaches this branch only when the virtualenv is missing or its Python will not execute, which is exactly when it should be replaced. Without the flaguvrefuses to touch an existing directory and aborts, so a virtualenv left behind without itsdep_stampwedges every later invocation instead of being rebuilt.The floor moves from 3.10 to 3.13, in
bin/pyactivateand inci/test/lint-main/checks/check-python-version.sh. 3.13 is what the CI builder already runs, since that is the system Python in Debian 13.How this was found, and why the floor moved with it
On a machine with system Python 3.14.6 and a uv-managed 3.10 present,
uv python findreturns the 3.10, so that is what the virtualenv was built on.check-pipeline-topics.shandcheck-python-files.shthen failed withImportError: cannot import name 'LiteralString' from 'typing'.That error is the interesting part.
LiteralStringneeds 3.11, so the tree had already drifted past its own declared floor, intest/pg-cdc-standby/mzcompose.pyandtest/cluster-spec-sheet/mzcompose.py.check-python-version.shis meant to catch exactly this, but it only byte-compiles the tree, and compilation does not resolve imports, so a newer-than-floor import passes the check and fails at run time. CI never noticed either, because the builder image has no uv-managed interpreters and runs everything on 3.13.Fixing only the interpreter selection would have left a 3.10 floor that nothing enforces and that the tree does not honor. Python 3.10 has been security-only for years and reaches end of life in October 2026, so raising it is the honest resolution rather than restoring 3.10 compatibility in those two files.
Raising
target-versionlets ruff replacedatetime.timezone.utcwith thedatetime.UTCalias and pullLiteralStringback fromtyping, which accounts for all of the mechanical churn in the other files. Ruff trails the floor atpy312because the pinned version predates 3.13 and rejectspy313; there is a comment inpyproject.tomlsaying so.Verification
ci/builder/requirements.txt.bin/pyactivaterun builds the virtualenv on the system interpreter rather than a uv-managed one, and deletingdep_stampfrom an existing virtualenv now rebuilds it instead of aborting.bin/lintpasses in full, includingcheck-python-version.shnow exercising 3.13.Not addressed here
check-python-version.shstill only byte-compiles, so the blind spot that let this drift remains open at the new floor. Resolving imports instead would close it, but importing every mzcompose file has side effects and costs, so it seemed worth deciding separately. The pinned ruff is0.0.292; upgrading it is what would allowpy313and is likewise its own change.Release notes
No user-visible changes.
🤖 Posted by Claude Code