Add inline-script interpreter selection helpers (PEP 723 PR 3/16) - #1636
Conversation
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.
8c4f057 to
53a1c5a
Compare
42c1db0 to
b162761
Compare
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>
…ajor-version parse
b162761 to
c428853
Compare
| const MAX_PROMPT_DETAIL_LENGTH = 120; | ||
| const INSTALLABLE_PYTHON_VERSION = /^\d+(?:\.\d+)*$/; | ||
| const PROMPT_CONTROL_CHARACTERS = | ||
| /[\u0000-\u001f\u007f-\u009f\u061c\u200b-\u200f\u202a-\u202e\u2060-\u206f\ufeff]/g; |
There was a problem hiding this comment.
Maybe we can move the constants to the top to have them all together?
> Part of #1602 (PEP 723 inline script env support). Design doc: #1601. > **Split for review (3 PRs).** Reviewers flagged the original PR 5 as too large, so it is split into three stacked PRs grouped by dependency layer: > - **5a — generic env-creation utilities — this PR (#1651).** Based on `main`; independent; merges first. > - **5b — inline-script cache + interpreter utilities — #1655.** Stacked on 5a. > - **5c — `create()` happy path (manager + wiring) — #1656.** Stacked on 5b. > > Applied together the three PRs are byte-for-byte identical to the original single change. **Merge order: 5a → 5b → 5c.** ### Roadmap context This is the first slice of **PR 5 of 16** in the PEP 723 inline-script roadmap. The full plan lives in #1602. | Phase | PR | Status | |---|---|---| | **Phase 1: Foundation** | PR 1: cache key hash utility | merged (#1634) | | | PR 2: cache layout + `meta.json` sidecar | merged (#1635) | | | PR 3: `requires-python` to interpreter selection | merged (#1636) | | **Phase 2: Manager** | PR 4: `InlineScriptEnvManager` skeleton | merged (#1610) | | | **PR 5a: generic env-creation utilities** | **this PR (#1651)** | | | **PR 5b: inline-script cache + interpreter utilities** | **#1655** | | | **PR 5c: `create()` happy path (manager + wiring)** | **#1656** | | | PR 6: `create()` uv-install fallback | not started (needs 3, 5) | | | PR 7: persistence with `get`, `set`, and Memento | not started (needs 4) | | | PR 8: activation-time discovery | not started (needs 2, 4, 7) | | **Phase 3: Routing** | PR 9: route PEP 723 scripts to the inline manager | not started (needs 4, 7) | | | PR 10: per-script project registration | not started (needs 9) | | **Phase 4+: UX / lifecycle** | PRs 11-16 | not started | ### Why this PR PR 5c implements `InlineScriptEnvManager.create()`. Before touching the manager, this PR lands the **generic, reusable primitives** it relies on — a cross-process file lock, a venv Python-path helper, a cancellation-hardened process runner, and two small `createWithProgress` options. None of this code is inline-script-specific, so it is reviewed on its own. ### What this PR adds **Cross-process file lock** (`src/common/lockfile.apis.ts`, new): `acquireFileLock` uses an atomic `mkdir` of a `<path>.lock` directory plus a per-owner marker file, returning `AcquiredFileLock { release, retain }`. `retain()` writes a `retained` marker so a later acquirer **fails fast with `ELOCKRETAINED`** instead of waiting out the 5-minute timeout — used when a build is cancelled mid-flight. Distinct error codes (`ELOCKED`, `ELOCKRETAINED`, `ELOCKORPHANED`, `ECOMPROMISED`, `ERETAINFAILED`) separate contention from corruption. **Shared `getVenvPythonPath`** (`src/common/utils/virtualEnvironment.ts`, new): returns `Scripts\python.exe` on Windows, else `bin/python`. Replaces an inline copy in `venvUtils` and is reused by 5b/5c. **Hardened process helper** (`src/managers/builtin/helpers.ts`): `runUV` and `runPython` now share one `runProcess` implementation whose cancellation guards `kill()` in `try/catch` and still emits a clean `CancellationError` if the process errors after a cancel. Per-caller options preserve existing behavior (`collectStderr`, `logPrefix`). **`venvUtils.ts`:** `createWithProgress` gains `CreateWithProgressOptions { trackUvEnvironment }`, and `CreateEnvironmentResult` gains `pkgInstallationCancelled` so a caller can tell cancellation apart from a real install failure. Existing callers are unaffected (both are optional / additive). ### Tests - **`lockfile.apis.unit.test.ts`** — 9 tests: contention, retain/fail-fast, orphaned and compromised locks, and timeout. - **`virtualEnvironment.unit.test.ts`** — 2 tests for `getVenvPythonPath` on Windows and POSIX. - **`helpers.cancellation.unit.test.ts`** — 4 tests for `runProcess` cancellation safety. - **`venvUtils.createWithProgress.unit.test.ts`** — 3 tests for `trackUvEnvironment` and `pkgInstallationCancelled`. On this branch alone `npm run compile-tests` is clean and `npm run unittest` reports **1447 passing, 0 failing, 4 pending**. ### User impact **None.** These are internal primitives with no new user-visible behavior. The refactors to `helpers.ts` and `venvUtils.ts` are behavior-preserving for existing callers. --------- Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 39dcc6a3-0fbd-4f36-9d0f-68677de49c27
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.