Add inline-script interpreter selection helpers (PEP 723 PR 3/16)#1636
Add inline-script interpreter selection helpers (PEP 723 PR 3/16)#1636StellaHuang95 wants to merge 3 commits into
Conversation
Pure helpers that pick the right installed Python interpreter for a
PEP 723 script (given its requires-python specifier) and extract the
lower-bound version to hand to `uv python install` when no compatible
interpreter is installed. Third foundation PR -- pure utility, no
behavior change on its own.
What is in:
- src/common/inlineScriptInterpreter.ts
- pickCompatibleInterpreter(installed, requiresPython) -- filters
out errored envs, version-unparseable envs, Python 2 (and Python
4+ until the code is revisited), and when constrained, anything
that does not satisfy requires-python. Sorts by version
descending and returns the head. Stable sort preserves input
order for ties so callers can express a preference (e.g. "system
Pythons first, then uv-managed"). Empty-string requiresPython is
normalized to "no constraint" rather than "matches nothing".
Docstring spells out the caller contract: only base interpreters
(system, pyenv, uv, conda base), not derived envs.
- extractLowerBoundVersion(requiresPython) -- extracts the floor
version string suitable for `uv python install <version>`:
">=3.13" -> "3.13"
">=3.11,<3.13" -> "3.11" (tightest lower bound)
"==3.12.*" -> "3.12"
"~=3.12.4" -> "3.12.4"
Returns undefined (and the caller falls back to uv defaults) for
upper-bound-only specs, the `>` operator (no clean integer
floor), `===` (opaque shape), illegal `~=X` without a minor
segment, illegal wildcards on `>=` / `~=`, and unparseable
input. Every rejection mirrors matchesPythonVersion so a value
we hand to uv is one the picker will then accept post-install.
- src/test/common/inlineScriptInterpreter.unit.test.ts -- 34 unit
tests covering: empty input, multi-clause specs, errored envs,
Python-2 filtering, ties (stable sort), wildcard specs (==X.* in
picker), implicit upper bound of `~=X.Y.Z`, empty-string
constraint, pre/dev/local-suffix version ranking, lower-bound
extraction across all operators, mixed-clause cases, illegal
shape rejection (`~=3`, `>=3.*`, `~=3.12.*`).
Re-uses matchesPythonVersion and the lessons of PEP 440 shape
validation from inlineScriptMetadata.ts rather than re-implementing
the matcher.
Design context
Implements the "pick a compatible interpreter" half of Q4 step 1
from pep723_design_questions.md, plus the lower-bound extraction
needed to wire up step 1`s fallback to promptInstallPythonViaUv in a
later PR. The actual call sites (creation flow, fallback flow,
re-verify after install) all live in a later PR.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
91ea3fc to
af51bb4
Compare
|
One feedback from Brett is to ask for user's permission to install Python using UV if none is compatible. |
|
Addressed in |
brettcannon
left a comment
There was a problem hiding this comment.
I don't understand where a "v" prefix would show up?
| } | ||
|
|
||
| function parseReleaseSegments(version: string): number[] | undefined { | ||
| const m = version.match(/^v?(\d+(?:\.\d+)*)/i); |
There was a problem hiding this comment.
Same reason I mentioned below, a valid version specifier can have a leading v
| } | ||
|
|
||
| function parseLeadingMajor(version: string): number | undefined { | ||
| const m = version.match(/^\s*v?(\d+)/i); |
There was a problem hiding this comment.
@brettcannon So my understanding is that the pep723 specs says "requires-python: A string that specifies the Python version(s) with which the script is compatible. The value of this field MUST be a valid version specifier." (See here).
And if I look for what is a valid version specifier, it says it can have preceding v character. See here
There was a problem hiding this comment.
but I guess you're right, since this function is parsing existing python version not a declaration, actual CPython version can't have a leading v. Let me update it, but I think we should keep the other one.
…ajor-version parse
Roadmap context
This is PR 3 of 16 in the PEP 723 inline-script roadmap. The full plan lives in #1602.
meta.jsonsidecarrequires-pythonto interpreter selectionInlineScriptEnvManagerskeletoncreate()happy pathcreate()uv-install fallbackget,set, and MementoPRs 1-3 are independent Phase 1 utilities that PR 5 will compose when it implements the environment creation path.
Why this PR
Before the inline-script manager can create an environment, it needs to answer two questions from Q4 of #1601:
requires-pythondeclaration?Both decisions are pure functions over an environment list and a PEP 440 specifier. Keeping them outside the manager makes the filtering, ranking, and fallback rules independently testable without environment discovery, UI, filesystem access, or process execution.
What this PR does
Adds
src/common/inlineScriptInterpreter.tswith two exported helpers.pickCompatibleInterpreter(installed, requiresPython)Filters the supplied environments and returns the newest usable Python 3 interpreter satisfying the script's constraint.
A candidate is rejected when:
env.erroris set.env.versionis missing, empty, or does not begin with numeric release segments.requiresPythonaccording to the existingmatchesPythonVersionhelper.An undefined, empty, or whitespace-only constraint is treated as unconstrained. If no candidate qualifies, the function returns
undefinedso the creation flow can offer the uv fallback.Candidates are ranked by numeric release segments in descending order. A leading
vis tolerated, and prerelease, development, and local suffixes are ignored for ranking. Ties preserve input order. The input array is copied before sorting and is never mutated.Examples:
>=3.11,<3.13selects the newest interpreter within that range.==3.12.*uses the existing wildcard-matching semantics.~=3.12.4honors the compatible-release upper bound.undefined.Base-interpreter caller contract
installedmust contain base interpreters only: system Python, pyenv-installed Python, uv-installed Python, or condabase. Derived environments such as venvs, named conda environments, Poetry environments, and Pipenv environments must not be used as venv bases.api.getEnvironments('global')is the intended source. The helper still defensively rejects errored, versionless, unparseable, and non-Python-3 entries.extractLowerBoundVersion(requiresPython)Extracts the tightest usable lower bound for a future
uv python install <version>request.Supported floor-producing forms include:
>=3.13to3.13~=3.12.4to3.12.4==3.12.7to3.12.7==3.12.*to3.12>=3.11,>=3.12,<3.14to3.12When multiple lower bounds are present, the numerically greatest one is returned.
Recognized operators that do not provide a safe integer floor (
>,<,<=,!=, and===) are skipped without warning. Malformed clauses, invalid wildcard placement, and~=values without at least major and minor segments are skipped withtraceWarn.A skipped clause does not discard a valid floor from another clause. For example,
>=3.11,<3.13returns3.11, while an upper-bound-only spec returnsundefined.The extracted value is an installation hint, not a full compatibility result. PR 6 will re-run
matchesPythonVersionafter uv installs an interpreter.Tests
Adds
src/test/common/inlineScriptInterpreter.unit.test.tswith 34 focused unit tests covering:vprefixes.>=,==, wildcard==, and~=.Required checks pass on this branch:
npm run lintnpm run compile-testsnpm run unittest: 1418 passing, 0 failing, 4 pendingUser impact
Zero.
Nothing in the extension calls these helpers yet. This PR discovers no interpreters, installs no Python versions, creates no environments, and adds no settings, commands, UI, filesystem access, or activation behavior.
PR 5 will call
pickCompatibleInterpreterfor the normal creation path. PR 6 will callextractLowerBoundVersionwhen no compatible installed interpreter exists and the user is offered a uv installation.Design context
This implements Q4 step 1 from #1601: select the newest installed Python satisfying
requires-python, and derive a lower-bound version for the uv fallback when no installed interpreter qualifies.It reuses the existing
matchesPythonVersionimplementation rather than introducing a second PEP 440 matcher.Explicit installation consent
Following review feedback, the uv fallback now has an explicit, informed-consent contract for inline scripts:
requires-pythonconstraint and the exact Python version requested.uv python install, and installed-version lookup matches release-segment boundaries.The inline-script manager still has no creation call site in this PR, so this establishes and tests the consent API without changing current user behavior. PR 6 will invoke it from the uv fallback.