Skip to content

Add empty InlineScriptEnvManager skeleton behind internal flag (PEP 723 PR 4/16) - #1610

Merged
Stella Huang (StellaHuang95) merged 1 commit into
microsoft:mainfrom
StellaHuang95:pep723-pr4-manager-skeleton
Jun 24, 2026
Merged

Add empty InlineScriptEnvManager skeleton behind internal flag (PEP 723 PR 4/16)#1610
Stella Huang (StellaHuang95) merged 1 commit into
microsoft:mainfrom
StellaHuang95:pep723-pr4-manager-skeleton

Conversation

@StellaHuang95

@StellaHuang95 Stella Huang (StellaHuang95) commented Jun 24, 2026

Copy link
Copy Markdown
Contributor

Part of #1602 (PEP 723 inline script env support). Design doc: #1601.

Roadmap context — where this PR sits

This is PR 4 of 16 in the PEP 723 inline-script roadmap. The full plan lives in #1602; here's a one-line summary of where each PR sits relative to this one:

Phase PR Status
Phase 1 — Foundation PR 1: cache key hash utility in progress
PR 2: cache layout + meta.json sidecar in progress
PR 3: requires-python → interpreter selection in progress
Phase 2 — Manager PR 4: InlineScriptEnvManager skeleton this PR
PR 5: create() happy path not started (needs 1, 2, 3, 4)
PR 6: create() uv-install fallback not started (needs 3, 5)
PR 7: persistence — get / set + 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 inline manager not started (needs 4, 7)
PR 10: per-script project registration not started (needs 9)
Phase 3.5 — Cross-repo PR 17 (pyrx), PR 18 (pyrx), PR 19 (vscode-python) not started
Phase 4 — UX PR 11: picker item, PR 12: bulk command not started
Phase 5 — Lifecycle & polish PR 13: clear cache, PR 14: TTL, PR 15: telemetry, PR 16: status bar not started

After PRs 1–4 merge, PR 7 unlocks the largest downstream wave (PR 8, 9, 13, 17, 19) — see the ordering comment on #1602 for the full timeline.

Why a skeleton

The roadmap rolls out across 16 PRs. Landing each one behind a feature gate keeps main shippable at every step and lets reviewers see one concern at a time. This PR is the smallest possible mount point for that gate: an EnvironmentManager implementation that satisfies the interface contract and registers cleanly, but otherwise does nothing.

Every subsequent PR (5–8) replaces one of the no-ops in this skeleton with the real thing.

What this PR does

  1. Adds InlineScriptEnvManager (src/managers/builtin/inlineScriptEnvManager.ts) — implements EnvironmentManager:

    • Metadata: name = "inline-script", displayName = "Inline script environments", iconPath = file-code, preferredPackageManagerId = "ms-python.python:pip".
    • Methods: getEnvironments returns [], get / resolve return undefined, set / refresh are no-ops.
    • Optional methods omitted: create, remove, quickCreateConfig are deliberately not declared so the picker UI hides their entry points until PR 5 lands them.
    • Events: both onDidChangeEnvironments and onDidChangeEnvironment exposed and disposed correctly; never fired by this skeleton.
  2. Adds registerInlineScriptFeatures (src/managers/builtin/inlineScriptMain.ts) — a gated registration helper that reads the internal flag and registers the manager only when on. Wired into the existing Promise.all of manager-registration tasks in extension.ts (alongside system, conda, pyenv, pipenv, poetry, shellStartupVars).

  3. Adds isInlineScriptsFeatureEnabled (src/helpers.ts) — reads python-envs.inlineScripts.enabled. The setting is intentionally NOT declared in package.json, so it does not appear in Settings UI, JSON autocomplete, or settings search. End users never discover it. Devs / CI can opt in by manually adding it to settings.json. Default value false. Gate goes away in PR 16.

User impact

Zero. The feature flag is undeclared in package.json, so:

  • No setting visible in Settings UI search.
  • No autocomplete entry when typing python-envs. in settings.json.
  • No "Preview" badge or any indication the feature exists.
  • No new picker section, no commands, no status-bar changes.
  • No log output on the default level (gate-off path uses traceVerbose).

PR 5–15 will all land behind the same gate; PR 16 removes the gate and declares the public setting for real.

… 4/16)

First PR in Phase 2 of the inline-script roadmap. Lands the empty
manager class and a registration helper gated behind an INTERNAL
flag, so the plumbing for PRs 5-16 can land incrementally without
any user-visible surface.

What is in:

- src/managers/builtin/inlineScriptEnvManager.ts (new)
  Class implementing EnvironmentManager: name="inline-script",
  displayName="Inline script environments", iconPath=file-code,
  preferredPackageManagerId="ms-python.python:pip". Every method is
  a sentinel: getEnvironments returns [], get returns undefined,
  set / refresh are no-ops, resolve returns undefined. Optional
  methods (create, remove, quickCreateConfig) are deliberately
  omitted so the UI hides their entry points until PR 5 lands them.

- src/managers/builtin/inlineScriptMain.ts (new)
  registerInlineScriptFeatures(disposables, log) -- gated
  registration helper. Reads isInlineScriptsFeatureEnabled() and
  returns early (with a traceVerbose, not traceInfo, so no per-
  activation log noise for default users) when the flag is false.
  When true, constructs the manager and registers it.

- src/helpers.ts
  isInlineScriptsFeatureEnabled(): boolean -- reads
  python-envs.inlineScripts.enabled via getConfiguration().get(...).
  The setting is INTENTIONALLY NOT DECLARED in package.json, so:
    - it does not appear in Settings UI
    - it does not appear in JSON autocomplete
    - it does not appear in settings search
    - end users never discover it
  Devs / CI can still opt in by manually adding it to settings.json
  (VS Code will mark it as an unknown setting with a yellow squiggle
  -- that is a feature, not a bug; signals you are on an internal
  flag). The whole gate goes away in PR 16 when the feature ships
  for real.

- src/extension.ts
  Wires registerInlineScriptFeatures into the existing
  Promise.all of manager-registration tasks, alongside system,
  conda, pyenv, pipenv, poetry, and shellStartupVars.

- src/test/managers/builtin/inlineScriptEnvManager.unit.test.ts (new)
  19 tests pinning the skeleton's no-op contract: static metadata,
  all five EnvironmentManager methods, optional methods correctly
  omitted, events exposed but never fired, dispose() is idempotent.

- src/test/managers/builtin/inlineScriptMain.unit.test.ts (new)
  2 tests pinning the gate itself: flag false -> no register call,
  no getPythonApi call, no disposable pushed; flag true -> exactly
  one register call, two disposables pushed (mgr + registration
  handle). These guard the entire "zero user impact" promise
  against future refactors.

- src/test/helpers.inlineScriptsFeature.unit.test.ts (new)
  3 tests covering the feature-flag helper: defaults to false,
  returns true on user-set, reads from the python-envs section.

User impact

Zero. No setting in package.json, so:
- No setting visible in Settings UI search
- No autocomplete entry when typing "python-envs..." in settings.json
- No "Preview" badge or any indication the feature exists
- No empty picker section, no commands, no status-bar changes
- No log output on the default log level

PR 5-15 will land on top of this gate; PR 16 removes the gate and
declares the public setting for real.

Design context

Implements PR 4 in Phase 2 of pep723_design_questions.md. The
internal-flag approach (undeclared setting) is the strictest
interpretation of "zero user impact during incremental rollout" --
discussed in the planning conversation as the alternative to a
declared-but-default-false setting (which would still show in
autocomplete and confuse early adopters).

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we maybe have it under src/managers/builtin/inlineScriptEnvManager/inlineScriptEnvManager.ts and src/managers/builtin/inlineScriptEnvManager/main.ts ?

l10n.t('Environments built from PEP 723 inline script metadata.'),
true,
);
public readonly iconPath: IconPath = new ThemeIcon('file-code');

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nice touch :)

@StellaHuang95
Stella Huang (StellaHuang95) merged commit 707f2a8 into microsoft:main Jun 24, 2026
45 of 46 checks passed
Stella Huang (StellaHuang95) added a commit that referenced this pull request Jul 21, 2026
> Part of #1602 (PEP 723 inline script env support). Design doc: #1601.

### Roadmap context

This is **PR 1 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** | **this
PR** |
| | PR 2: cache layout + `meta.json` sidecar | _in progress_ |
| | PR 3: `requires-python` to interpreter selection | _in progress_ |
| **Phase 2: Manager** | PR 4: `InlineScriptEnvManager` skeleton |
merged (#1610) |
| | PR 5: `create()` happy path | not started (needs 1, 2, 3, 4) |
| | PR 6: `create()` uv-install fallback | not started (needs 3, 5) |
| | PR 7: persistence with `get`, `set`, and Memento | _in progress_
(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 3.5: Cross-repo** | PRs 17-19: Pylance and vscode-python
integration | not started |
| **Phase 4: UX** | PR 11: picker item; PR 12: bulk command | not
started |
| **Phase 5: Lifecycle and polish** | PR 13: clear cache; PR 14: TTL; PR
15: telemetry; PR 16: status bar | not started |

PRs 1-3 are independent Phase 1 utilities that PR 5 will compose when it
implements the environment creation path.

### Why this PR

The cache key decides whether a PEP 723 script reuses an existing
environment or builds a fresh one. Under the dependency-keyed design
from Q4 of #1601, scripts using the same dependencies and interpreter
should share an environment.

Trivial metadata edits such as changing package-name casing, separator
style, whitespace, dependency order, or extras order should not fragment
the cache.

This PR isolates that logic as pure functions with no filesystem access,
global storage dependency, or extension activation behavior. The on-disk
cache layout remains separate in PR 2.

### What this PR does

Adds `src/common/inlineScriptCacheKey.ts` with four exports:

1. **`normalizeDependency(dep)`**

   Canonicalizes common variants of the same requirement:

- Applies PEP 503 normalization to project names and extras: lowercase
and collapse runs of `.`, `_`, and `-` to a single `-`.
   - Sorts and deduplicates extras after normalization.
   - Removes whitespace around version comparison operators.
   - Preserves meaningful version and marker content.
   - Drops empty dependency entries when keys are computed.

   Examples:

   - `Django` and `django` normalize identically.
- `Flask-Login`, `flask_login`, and `flask.login` normalize identically.
   - `requests[Socks,security]` becomes `requests[security,socks]`.
   - `requests < 3` becomes `requests<3`.

This is intentionally not a complete PEP 508 parser. Inputs it cannot
meaningfully parse remain deterministic without attempting semantic
normalization.

2. **`normalizeInterpreterPath(interpreterPath)`**

Uses the existing `normalizePath` helper so Windows path casing and
separator differences do not fragment the cache. POSIX path casing
remains unchanged.

3. **`computeCacheKey({ dependencies, interpreterPath })`**

   - Normalizes, deduplicates, and sorts dependencies.
   - Normalizes the interpreter path.
   - Builds a versioned, labelled payload.
   - Hashes the payload with SHA-256.
   - Returns the first 16 lowercase hexadecimal characters.

The resulting key is deterministic and safe to use as a directory name
under the cache root introduced by PR 2.

4. **`CACHE_KEY_HEX_LENGTH`**

Exposes the key length so callers and tests do not duplicate the value
`16`.

### Caller contract

`interpreterPath` must be absolute, trimmed, and already resolved
through symlinks, for example with `fs.realpath()`.

The utility deliberately performs no I/O. Two different path strings
that resolve to the same executable will therefore produce different
keys unless the caller canonicalizes them first. The creation flow in PR
5 will own that resolution.

### Cache behavior

The script path and raw `requires-python` value are intentionally not
included directly in the key.

- Two scripts with equivalent dependencies and the same selected
interpreter share an environment.
- Moving or renaming a script does not invalidate its environment.
- Adding, removing, or repinning a dependency produces a new key.
- Selecting a different interpreter produces a new key.
- A later PR re-verifies `requires-python` on cache hits in case the
constraint changes without changing the selected interpreter.

This supports the design's two outcomes: reuse an unchanged cache entry
or build a fresh environment. There is no in-place dependency
synchronization path.

### Tests

Adds `src/test/common/inlineScriptCacheKey.unit.test.ts` with 37 unit
tests covering:

- PEP 503 project-name normalization.
- Extras normalization, sorting, deduplication, and whitespace handling.
- Version comparison operators and multi-clause specifiers.
- Empty and duplicate dependency entries.
- Dependency ordering and deterministic output.
- Inputs that should and should not change the key.
- Windows and POSIX interpreter-path behavior.
- Fixed-length lowercase hexadecimal output.
- Filesystem-safe output.
- Pinned caller-contract behavior such as trailing whitespace in
interpreter paths.

Platform behavior is stubbed through `platformUtils`, keeping the suite
independent of the host operating system.

### User impact

**Zero.**

Nothing in the extension calls these helpers yet. This PR adds no
settings, commands, UI, logging, filesystem writes, or activation
behavior. The utility is wired into environment creation in PR 5.

### Design context

This implements the cache-key portion of Q4 in #1601: a pipx-style cache
keyed by normalized dependencies and the selected interpreter.

PEP 503 normalization for project names and extras was added in response
to review feedback on the design PR.

---------

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Stella Huang (StellaHuang95) added a commit that referenced this pull request Jul 21, 2026
…guard (PEP 723 PR 2/16) (#1635)

> Part of #1602 (PEP 723 inline script env support). Design doc: #1601.

### Roadmap context

This is **PR 2 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 | in review
(#1634) |
| | **PR 2: cache layout + `meta.json` sidecar** | **this PR** |
| | PR 3: `requires-python` to interpreter selection | _in progress_ |
| **Phase 2: Manager** | PR 4: `InlineScriptEnvManager` skeleton |
merged (#1610) |
| | PR 5: `create()` happy path | not started (needs 1, 2, 3, 4) |
| | PR 6: `create()` uv-install fallback | not started (needs 3, 5) |
| | PR 7: persistence with `get`, `set`, and Memento | _in progress_
(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 3.5: Cross-repo** | PRs 17-19: Pylance and vscode-python
integration | not started |
| **Phase 4: UX** | PR 11: picker item; PR 12: bulk command | not
started |
| **Phase 5: Lifecycle and polish** | PR 13: clear cache; PR 14: TTL; PR
15: telemetry; PR 16: status bar | not started |

PRs 1-3 are independent Phase 1 utilities that PR 5 will compose when it
implements the environment creation path.

### Why this PR

Q2 and Q3 of #1601 place inline-script environments under
`<globalStorageUri>/script-envs-v1/<cache-key>/` and give each
environment a `.meta.json` sidecar for lifecycle bookkeeping. Those
contracts need to exist before the manager can create, discover, reuse,
or clean up cached environments.

This PR also implements the cache-hit usability guard from Q4 step 4.
Hashing the selected interpreter path catches a switch to a different
Python path, but it does not catch an interpreter being uninstalled from
the same path. The guard detects that stale cache state before the
extension attempts to reuse a broken environment.

The filesystem concerns are kept separate from PR 1's pure hashing logic
and PR 3's interpreter-selection logic.

### What this PR does

Adds `src/common/inlineScriptCacheLayout.ts` with cache path helpers,
typed sidecar I/O, stale-entry selection, and an environment-usability
check.

#### Cache layout

- `getScriptEnvCacheRoot(globalStorageUri)` returns
`<globalStorageUri>/script-envs-v1/`.
- `getScriptEnvDir(globalStorageUri, cacheKey)` returns the directory
for one cached environment.
- `getMetaJsonPath(envDir)` returns `<envDir>/.meta.json`.
- `INLINE_SCRIPT_CACHE_DIR_NAME`, `META_JSON_FILENAME`, and
`META_SCHEMA_VERSION` centralize the on-disk contract.

The cache directory and metadata schema are both versioned. An
incompatible future format should bump the cache directory suffix and
schema version together rather than migrate environments in place.

#### Typed `.meta.json` sidecar

`InlineScriptEnvMeta` contains the fields with concrete downstream
consumers:

- `schemaVersion`: validates the sidecar format.
- `scriptFsPath`: identifies the owning script for lifecycle cleanup.
- `lastUsedAt`: drives TTL eviction.
- `requiresPython?`: supports compatibility revalidation on cache hits.

`readMetaJson(envDir)` never throws. It returns `undefined` with a
warning for missing or non-regular files, files larger than 1 MiB,
malformed JSON, unknown schema versions, invalid field types, and
non-canonical ISO timestamps. Unknown properties are dropped when the
validated object is constructed.

`writeMetaJson(envDir, meta)` ensures the directory exists, writes
formatted JSON to a randomized sibling temporary file, and atomically
renames it to `.meta.json`. If writing or renaming fails, it attempts to
remove the temporary file and rethrows the original error.

#### TTL selection

`selectStaleEntries(entries, now, ttlMs)` is a pure selector used by the
future opportunistic cleanup path.

- An entry is stale only when its age is strictly greater than the TTL.
- Entries exactly at the TTL boundary are retained.
- Future timestamps are retained.
- Entries without `lastUsedAt` are retained because the extension should
not delete data it cannot classify confidently.

The function returns paths to delete; it performs no filesystem walk or
deletion itself.

#### Cache-hit usability guard

`verifyEnvUsable(envDir)` checks whether the cached environment still
has a usable base interpreter. It never throws; failures produce a
warning and return `false`.

- **POSIX:** stats `<envDir>/bin/python`. Because `fs.stat` follows
symlinks, a dead launcher symlink is detected.
- **Windows:** reads `home = ...` from `pyvenv.cfg` and stats
`<home>/python.exe`. Checking `<envDir>/Scripts/python.exe` would be
insufficient because that launcher can remain after the base Python is
uninstalled.

This covers common removal paths such as `pyenv uninstall`, `uv python
uninstall`, Homebrew or apt removal, and Windows Add/Remove Programs.

### Failure policy

The helpers distinguish recoverable cache state from creation-time
failures:

- Metadata reads and usability checks return `undefined` or `false`
rather than throwing, allowing callers to treat invalid cache state as a
miss.
- Metadata writes rethrow because a caller must know that newly created
state was not recorded successfully.
- The TTL selector is pure and cannot delete anything by itself.

### Tests

Adds `src/test/common/inlineScriptCacheLayout.unit.test.ts` with 44
focused unit tests covering:

- Cross-platform cache and sidecar path construction.
- Metadata write/read round trips and concurrent writers.
- Atomic write behavior and temporary-file cleanup.
- Missing, malformed, oversized, and structurally invalid sidecars.
- Schema-version and canonical-timestamp validation.
- TTL boundaries, future timestamps, and missing `lastUsedAt` values.
- POSIX launchers, live symlinks, dead symlinks, and non-file paths.
- Windows `pyvenv.cfg` parsing and missing base interpreters.

Required checks pass on this branch:

- `npm run lint`
- `npm run compile-tests`
- `npm run unittest`: 1418 passing, 0 failing, 4 pending

### User impact

**Zero.**

Nothing in the extension calls these helpers yet. This PR creates no
environments, writes nothing to global storage during activation,
deletes no cache entries, and adds no settings, commands, UI, or logging
on the default execution path.

PR 5 will use the layout and sidecar helpers during creation. PRs 7 and
8 will use them during reuse and activation-time discovery. PRs 13 and
14 will use the cleanup helpers.

### Design context

This implements Q2 (disk location), Q3 (environment contents and sidecar
metadata), Q4 step 4 (base-interpreter existence guard), and the pure
selection portion of Q7 (TTL cleanup) from #1601.

The base-interpreter guard was added in response to review feedback on
the design PR.

---------

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Stella Huang (StellaHuang95) added a commit that referenced this pull request Jul 21, 2026
)

> Part of #1602 (PEP 723 inline script env support). Design doc: #1601.

### Roadmap context

This is **PR 3 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 | in review
(#1634) |
| | PR 2: cache layout + `meta.json` sidecar | in review (#1635) |
| | **PR 3: `requires-python` to interpreter selection** | **this PR** |
| **Phase 2: Manager** | PR 4: `InlineScriptEnvManager` skeleton |
merged (#1610) |
| | PR 5: `create()` happy path | not started (needs 1, 2, 3, 4) |
| | PR 6: `create()` uv-install fallback | not started (needs 3, 5) |
| | PR 7: persistence with `get`, `set`, and Memento | _in progress_
(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 3.5: Cross-repo** | PRs 17-19: Pylance and vscode-python
integration | not started |
| **Phase 4: UX** | PR 11: picker item; PR 12: bulk command | not
started |
| **Phase 5: Lifecycle and polish** | PR 13: clear cache; PR 14: TTL; PR
15: telemetry; PR 16: status bar | not started |

PRs 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:

1. Which installed base interpreter is the newest one compatible with
the script's `requires-python` declaration?
2. If none is compatible, what lower-bound version can the existing uv
installation flow request?

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.ts` with 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.error` is set.
- `env.version` is missing, empty, or does not begin with numeric
release segments.
- The interpreter is not Python 3.
- The version does not satisfy `requiresPython` according to the
existing `matchesPythonVersion` helper.

An undefined, empty, or whitespace-only constraint is treated as
unconstrained. If no candidate qualifies, the function returns
`undefined` so the creation flow can offer the uv fallback.

Candidates are ranked by numeric release segments in descending order. A
leading `v` is 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:

- No constraint across Python 3.10, 3.11, and 3.12 selects 3.12.
- `>=3.11,<3.13` selects the newest interpreter within that range.
- `==3.12.*` uses the existing wildcard-matching semantics.
- `~=3.12.4` honors the compatible-release upper bound.
- If only Python 2, broken environments, or incompatible versions are
available, the result is `undefined`.

#### Base-interpreter caller contract

`installed` must contain base interpreters only: system Python,
pyenv-installed Python, uv-installed Python, or conda `base`. 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.13` to `3.13`
- `~=3.12.4` to `3.12.4`
- `==3.12.7` to `3.12.7`
- `==3.12.*` to `3.12`
- `>=3.11,>=3.12,<3.14` to `3.12`

When 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 with `traceWarn`.

A skipped clause does not discard a valid floor from another clause. For
example, `>=3.11,<3.13` returns `3.11`, while an upper-bound-only spec
returns `undefined`.

The extracted value is an installation hint, not a full compatibility
result. PR 6 will re-run `matchesPythonVersion` after uv installs an
interpreter.

### Tests

Adds `src/test/common/inlineScriptInterpreter.unit.test.ts` with 34
focused unit tests covering:

- Empty input and no-compatible-interpreter results.
- Unconstrained and constrained newest-version selection.
- Multi-clause, wildcard, and compatible-release constraints.
- Errored, versionless, unparseable, and Python 2 environments.
- Empty-string constraints and leading `v` prefixes.
- Stable ties and non-mutating input behavior.
- Ranking versions with prerelease, development, and local suffixes.
- Lower-bound extraction for `>=`, `==`, wildcard `==`, and `~=`.
- Tightest-floor selection across multiple clauses.
- Upper-bound-only and unsupported operators.
- Malformed clauses, wildcard misuse, and warning behavior.

Required checks pass on this branch:

- `npm run lint`
- `npm run compile-tests`
- `npm run unittest`: 1418 passing, 0 failing, 4 pending

### User 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 `pickCompatibleInterpreter` for the normal creation path.
PR 6 will call `extractLowerBoundVersion` when 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 `matchesPythonVersion` implementation 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:

- The modal explains the script's `requires-python` constraint and the
exact Python version requested.
- If uv is missing, both the message and action disclose that uv and
Python will be installed.
- No prompt is offered when a compatible concrete install version cannot
be derived.
- Script-controlled prompt text is flattened, bounded, and stripped of
control characters; install versions must be numeric release segments.
- Dismissal performs no installation and remains retryable;
inline-script setup does not inherit the global "Don't ask again"
suppression state.
- Approval forwards the displayed version to `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.

---------

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Stella Huang (StellaHuang95) added a commit that referenced this pull request Jul 27, 2026
> 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
Stella Huang (StellaHuang95) added a commit that referenced this pull request Jul 29, 2026
…1655)

> 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 PRs grouped by dependency layer:
> - **5a — generic env-creation utilities — #1651.** Merged.
> - **5b — inline-script cache + interpreter utilities — this PR
(#1655).** Rebased on `main`.
> - **5c — `create()` happy path (manager + wiring) — #1656.** Stacked
on 5b.
>
> #1651 has merged and this branch has been rebased. The diff now
contains only this PR's seven files. **Remaining merge order: 5b → 5c.**

### Roadmap context

This is the second slice of **PR 5 of 16**. See #1651 for the full
roadmap table.

| Phase 2: Manager | PR | Status |
|---|---|---|
| | PR 4: `InlineScriptEnvManager` skeleton | merged (#1610) |
| | PR 5a: generic env-creation utilities | merged (#1651) |
| | **PR 5b: inline-script cache + interpreter utilities** | **this PR
(#1655)** |
| | PR 5c: `create()` happy path (manager + wiring) | #1656 |

### Why this PR

With the generic primitives from 5a in place, this PR lands the
**inline-script-specific utilities** that `create()` (5c) composes: a
normalized dependency cache key, cache-layout ownership/status checks,
and interpreter-constraint handling. These are pure functions with no
manager wiring yet, so they are reviewed on their own.

### What this PR adds

**Cache-key tail normalization** (`src/common/inlineScriptCacheKey.ts`):
adds `normalizeRequirementTail`, a quote-aware scanner that collapses
whitespace and tightens comparator spacing (`>= 1.0` → `>=1.0`) in a
requirement's version/marker tail while **preserving quoted PEP 508
marker literals verbatim** (e.g. `python_version >= "3.11"`).
Direct-reference requirements (`pkg @ https://…`) are kept verbatim
after the name and extras. The effect is that semantically identical
dependency strings normalize to the same cache key, so they reuse the
same cached environment.

**Cache-layout additions** (`src/common/inlineScriptCacheLayout.ts`):
`resolveCacheEntryPath` (containment under the cache root),
`inspectOwnedCacheEntry` (realpath ownership),
`getBaseInterpreterStatus` (`available | missing | unavailable`),
`inspectMetaJson` (typed sidecar read), and a stricter `validateMeta`.
The `.meta.json` sidecar schema is `{ schemaVersion,
baseInterpreterPath, baseInterpreterVersion, lastUsedAt }`. Uses
`getVenvPythonPath` from merged PR #1651.

**Interpreter-constraint trimming**
(`src/common/inlineScriptInterpreter.ts`): `pickCompatibleInterpreter`
now trims `requires-python`, so a whitespace-only constraint is treated
as no constraint.

**Manager ID constants** (`src/common/constants.ts`): centralizes the
conda and inline-script manager IDs used by interpreter filtering.

### Tests

- **`inlineScriptCacheKey.unit.test.ts`** — canonicalization cases
including marker literals and direct references.
- **`inlineScriptCacheLayout.unit.test.ts`** — the new containment,
ownership, base-interpreter-status, and typed sidecar-read helpers.
- **`inlineScriptInterpreter.unit.test.ts`** — constraint trimming /
selection.

On this rebased branch `npm run compile-tests` is clean and `npm run
unittest` reports **1467 passing, 0 failing, 5 pending**.

### User impact

**None.** These are pure utilities. Nothing calls the new code paths
until the manager lands in 5c (#1656).

### Merge order

#1651 has merged. Merge this PR next, then #1656.

---------

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 39dcc6a3-0fbd-4f36-9d0f-68677de49c27
Stella Huang (StellaHuang95) added a commit that referenced this pull request Jul 31, 2026
)

> 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 PRs grouped by dependency layer:
> - **5a — generic env-creation utilities — #1651.** Merged.
> - **5b — inline-script cache + interpreter utilities — #1655.**
Merged.
> - **5c — `create()` happy path (manager + wiring) — this PR (#1656).**
Rebased on `main`.
>
> #1651 and #1655 have merged and this branch has been rebased. The diff
now contains only this PR's five files.

### Roadmap context

This is the final slice of **PR 5 of 16** — the actual `create()` happy
path. See #1651 for the full roadmap table.

| Phase 2: Manager | PR | Status |
|---|---|---|
| | PR 4: `InlineScriptEnvManager` skeleton | merged (#1610) |
| | PR 5a: generic env-creation utilities | merged (#1651) |
| | PR 5b: inline-script cache + interpreter utilities | merged (#1655)
|
| | **PR 5c: `create()` happy path (manager + wiring)** | **this PR
(#1656)** |
| | PR 6: `create()` uv-install fallback | not started (needs 3, 5) |

### Why this PR

`InlineScriptEnvManager.create()` was a deliberately empty no-op after
PR 4. This PR implements its **happy path**: the case where the machine
already has a base interpreter that satisfies the script's
`requires-python`, so no uv Python install is required. Given a PEP 723
script, it builds — or reuses — a dependency-keyed virtual environment
under the extension's global storage, following the pipx-style cache
design from Q4 of #1601. The uv-install fallback (no compatible
interpreter present) is deferred to PR 6.

It composes the primitives from 5a (#1651) and the inline-script
utilities from 5b (#1655); this PR adds only the manager and its wiring.

### What this PR does

**Wires the manager's collaborators** (`extension.ts`,
`inlineScriptMain.ts`): `registerInlineScriptFeatures` and the
`InlineScriptEnvManager` constructor now receive the
`NativePythonFinder`, the `PythonEnvironmentApi`, the base (system)
environment manager, and `globalStorageUri`.

**Implements `create(scope)`** (`inlineScriptEnvManager.ts`):

- Accepts exactly one local `file:` URI (a bare `Uri` or single-element
array). Anything else — `'global'`, a folder, or multiple URIs — logs a
warning and returns `undefined`.
- Reads PEP 723 metadata from the script; missing or invalid metadata
returns `undefined`.
- Merges `metadata.dependencies` with `options.additionalPackages`,
trims each, and rejects empty entries.
- Selects a base interpreter, computes the dependency + interpreter
cache key, and de-duplicates concurrent in-process `create()` calls for
the same key via a `pendingCreations` map.

**Base-interpreter selection** (`selectBaseInterpreter`): starts from
`getEnvironments('global')`, keeps only true base managers (system,
pyenv, conda `base`), and excludes derived environments by rejecting a
non-absolute `sysPrefix` or the presence of `pyvenv.cfg`. It then picks
the newest compatible interpreter with `pickCompatibleInterpreter` and
resolves the executable through `fs.realpath` so the cache key is
canonical. If a candidate cannot be resolved it falls through to the
next.

**Create-or-reuse under a cross-process lock**
(`createOrReuseEnvironment`): acquires a directory lock (5a), inspects
the existing cache entry, and reuses / rebuilds / preserves accordingly,
always releasing the lock in `finally`.

**Fail-closed cache inspection** (`inspectCacheEntry`) returns `absent |
stale | uncertain | reusable`:

- Rejects symlinks and non-directories; verifies the entry is contained
under the cache root (`resolveCacheEntryPath`).
- Reads and validates the `.meta.json` sidecar and confirms the recorded
base-interpreter path and version still match the selected base.
- Confirms the base interpreter is still present on disk
(`getBaseInterpreterStatus`).
- Resolves the cached venv to a real `PythonEnvironment`, confirms it is
genuinely ours via realpath containment (`inspectOwnedCacheEntry`),
compares Python release segments, and re-checks `requires-python` with
the existing `matchesPythonVersion`.
- Only conclusive evidence marks an entry `stale` (rebuild); any doubt
yields `uncertain`, and an uncertain entry is **preserved, never
deleted**. A reused entry has its `lastUsedAt` refreshed.

**Environment build** (`buildCacheEntry`): delegates to the existing
`createWithProgress` venv flow with `trackUvEnvironment` set to `false`
so cached script environments are not registered as workspace venvs. On
success it writes the sidecar and re-validates that the built
environment matches the requested release and is owned by this entry. On
failure it removes the directory and returns empty. On **cancellation**
it retains the lock so a half-built environment is not silently reused
later.

### Tests

- **`inlineScriptEnvManager.unit.test.ts`** — 40 tests across
scope/metadata validation, base-interpreter selection, cache creation,
cache reuse, transaction rollback, and events/disposal.
- **`inlineScriptMain.unit.test.ts`** — updated for the new
`registerInlineScriptFeatures` signature.

On this rebased branch `npm run compile-tests` is clean and `npm run
unittest` reports **1491 passing, 0 failing, 5 pending**.

### User impact

**None on the default path.** The manager is still registered only when
the undeclared `python-envs.inlineScripts.enabled` flag is on, so
default users see nothing.

`create()` is now a declared method (PR 4 omitted it), so with the flag
on the inline manager can appear as a create target. But it acts only on
a single local script URI and no-ops on every other scope, and **nothing
in the extension routes a script URI to it yet**. Wiring the trigger is
later work: routing in PR 9, and the "Set up env for this script" picker
item and bulk command in PR 11/12.

### Merge order

#1651 and #1655 have merged. This PR is the remaining final slice.

---------

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 39dcc6a3-0fbd-4f36-9d0f-68677de49c27
Mohit Yadav (mohityadav8) pushed a commit to mohityadav8/vscode-python-environments that referenced this pull request Aug 6, 2026
…oft#1651)

> Part of microsoft#1602 (PEP 723 inline script env support). Design doc: microsoft#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 (microsoft#1651).** Based on
`main`; independent; merges first.
> - **5b — inline-script cache + interpreter utilities — microsoft#1655.**
Stacked on 5a.
> - **5c — `create()` happy path (manager + wiring) — microsoft#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 microsoft#1602.

| Phase | PR | Status |
|---|---|---|
| **Phase 1: Foundation** | PR 1: cache key hash utility | merged
(microsoft#1634) |
| | PR 2: cache layout + `meta.json` sidecar | merged (microsoft#1635) |
| | PR 3: `requires-python` to interpreter selection | merged (microsoft#1636) |
| **Phase 2: Manager** | PR 4: `InlineScriptEnvManager` skeleton |
merged (microsoft#1610) |
| | **PR 5a: generic env-creation utilities** | **this PR (microsoft#1651)** |
| | **PR 5b: inline-script cache + interpreter utilities** | **microsoft#1655** |
| | **PR 5c: `create()` happy path (manager + wiring)** | **microsoft#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
Stella Huang (StellaHuang95) added a commit that referenced this pull request Aug 13, 2026
> Part of #1602 (PEP 723 inline script env support). Design doc: #1601.
>
> This replaces the earlier closed draft #1652 with the finalized
implementation rebased on `main`.

### Roadmap context

This is **PR 6 of 16** in the PEP 723 inline-script roadmap. It extends
the PR 5 `create()` happy path with the missing-compatible-interpreter
fallback.

| Phase 2: Manager | PR | Status |
|---|---|---|
| | PR 4: `InlineScriptEnvManager` skeleton | merged (#1610) |
| | PR 5a: generic env-creation utilities | merged (#1651) |
| | PR 5b: inline-script cache + interpreter utilities | merged (#1655)
|
| | PR 5c: `create()` happy path | merged (#1656) |
| | **PR 6: `create()` uv-install fallback** | **this PR** |
| | PR 7: persistence (`get` / `set` + Memento) | separate follow-up |

### Why this PR

PR 5 can create or reuse an inline-script environment when an installed
base interpreter already satisfies the script's `requires-python`. It
deliberately stops when no compatible interpreter exists.

This PR adds the consent-gated fallback for that case:

1. Re-check installed interpreters after entering a narrow fallback
queue.
2. Select a safe Python version satisfying the complete PEP 440
requirement.
3. Ask the user before installing uv or Python.
4. Refresh environment discovery after installation.
5. If discovery is stale or unavailable, resolve and validate uv's
returned executable directly.
6. Continue through the existing PR 5 cache create/reuse path.

### What this PR does

**Adds the inline-script fallback to `InlineScriptEnvManager.create()`**

- Keeps the installed-interpreter path unchanged and only enters the
fallback when no compatible base exists.
- Skips installation prompts for quick-create/noninteractive calls.
- Serializes only fallback installations, then re-checks discovery
before prompting so concurrent compatible requests reuse one
installation.
- Retains successfully direct-resolved uv interpreters so queued
requests can reuse them even while discovery remains stale.

**Selects a safe uv target from `requires-python`**

- Uses simple safe selectors directly, such as `>=3.13` → `3.13` and
`==3.13.1` → `3.13.1`.
- Queries uv's advertised versions for bounded or exclusion-heavy
constraints.
- Restricts catalog candidates to default CPython 3 builds satisfying
the complete specifier.
- Handles exclusions such as `>=3.13.2,!=3.13.2` without installing the
excluded floor.
- Uses an advertised release for bounded ranges rather than fabricating
a potentially unavailable patch-zero release.
- Applies full PEP 440 prerelease semantics consistently across
discovery, catalog selection, direct resolution, and cache validation.
- Normalizes accepted prerelease aliases (for example, `c1` → `rc1`)
before passing a version to uv.

**Extends the uv installer's consent flow**

- Adds an inline-script-specific prompt that shows both the script
requirement and selected Python version.
- Sanitizes and caps script-controlled prompt details.
- Validates install selectors before forwarding them to uv.
- If catalog lookup is required and uv is missing, asks for consent to
install uv first.
- Re-checks whether a newly installed uv is usable by the current
extension host and surfaces the existing restart-required message when
needed.

**Handles stale discovery after installation**

- Refreshes environment discovery after uv installs Python.
- Treats refresh/discovery failures as recoverable.
- Resolves the executable returned by uv directly, verifies that it
satisfies `requires-python`, and canonicalizes its path before creating
the cached environment.

### Examples

| `requires-python` | Fallback behavior |
|---|---|
| `>=3.13` | Request uv's `3.13` selector |
| `==3.13.1` | Request exactly `3.13.1` without requiring a catalog
lookup |
| `>=3.11,<3.12` | Choose an advertised compatible `3.11.x` release |
| `>=3.13.2,!=3.13.2` | Skip `3.13.2` and choose a compatible advertised
release |
| `>=3.15.0a1,<3.16` | Permit an explicitly requested prerelease |
| `>=3.14,<3.16` | Do not select a prerelease implicitly |

### Safety and concurrency

- No uv or Python installation occurs without explicit user consent.
- Script-derived values cannot inject arbitrary uv arguments.
- Declined, cancelled, or failed installations do not mutate the
script-environment cache.
- The fallback queue does not globally serialize environment selection
or normal cache creation.
- Existing cross-process cache locking and rollback behavior from PR 5
remains unchanged.

### Tests

Coverage includes:

- simple, exact, bounded, exclusion, and prerelease requirements;
- uv catalog filtering and consented uv bootstrap;
- declined and failed installations;
- refresh and discovery failures with direct resolution;
- simultaneous same- and different-constraint requests;
- quick-create prompt suppression;
- strict post-install and cache compatibility checks;
- prompt sanitization and install-selector validation.

`npm run compile-tests`, `npm run lint`, the full unit suite, and the
focused inline-script/uv suites are clean.

### User impact

**No default-path user impact yet.** This completes an internal Phase 2
manager capability. Automatic routing and user-facing entry points
arrive in later roadmap PRs.

When those entry points are wired, users whose scripts require an
unavailable Python will be able to approve installing a compatible
interpreter rather than having environment creation stop.

---------

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 1a9f6ba1-9bd3-4664-bc25-a0d34d7a2e91
Stella Huang (StellaHuang95) added a commit that referenced this pull request Aug 14, 2026
> Part of #1602 (PEP 723 inline script env support). Design doc: #1601.
>
> This replaces the earlier closed draft #1653 with the finalized
implementation rebased on `main`.

### Roadmap context

This is **PR 7 of 16** in the PEP 723 inline-script roadmap. It adds
durable per-script environment associations to the internal manager.

| Phase 2: Manager | PR | Status |
|---|---|---|
| | PR 4: `InlineScriptEnvManager` skeleton | merged (#1610) |
| | PR 5a: generic env-creation utilities | merged (#1651) |
| | PR 5b: inline-script cache + interpreter utilities | merged (#1655)
|
| | PR 5c: `create()` happy path | merged (#1656) |
| | PR 6: `create()` uv-install fallback | open (#1696) |
| | **PR 7: persistence (`get` / `set` + Memento)** | **this PR** |
| | PR 8: activation-time discovery | follow-up |
| | PR 9: route PEP 723 scripts to the inline manager | follow-up |

### Why this PR

PR 5 can build or reuse an inline-script environment, but the manager
does not remember that the resulting environment belongs to a particular
script. After an extension-host restart, the in-memory association is
gone.

This PR implements the persistence portion of Q4 in the design:

- maintain an independent environment association for each script;
- persist script-to-environment executable paths in workspace Memento;
- lazily and safely rehydrate those associations;
- re-check current `requires-python` metadata before returning an
environment;
- report changes so the central environment API can update its
last-known state.

### What this PR does

**Implements per-script `set()`**

- Accepts one or more local `file:` URIs and rejects invalid or mixed
scopes atomically.
- Validates that selected environments are owned inline-script cache
entries.
- Persists a normalized script path → environment executable path
mapping under a dedicated Memento key.
- Supports assigning and unassigning individual scripts or batches.
- Updates in-memory state and emits `onDidChangeEnvironment` only for
effective changes.
- Leaves the existing `create()` behavior separate: creation alone does
not implicitly establish a persisted association.

**Implements per-script `get()`**

- Reads current PEP 723 metadata before returning an association.
- Keeps unreadable or temporarily invalid script metadata from
destructively clearing state.
- Returns an in-memory association when valid.
- Lazily reconstructs persisted environments after restart instead of
resolving every script during activation.
- Re-checks `requires-python` against the reconstructed Python version
before returning it.

**Safely rehydrates persisted associations**

- Requires an absolute executable path.
- Preserves associations while their cache entry is locked or being
created.
- Verifies that the executable exists and is a regular file.
- Resolves it into a `PythonEnvironment` and confirms that it belongs to
the expected extension-owned cache entry.
- Removes only definitively stale associations; transient filesystem or
resolver failures remain retryable.
- Emits a change event when a slow rehydration eventually succeeds,
including when the public API's initial one-second wait has already
elapsed.

**Validates warm in-memory associations**

- Periodically revalidates cached associations without performing full
resolution on every lookup.
- Detects executables deleted while VS Code remains open.
- Detects an environment rebuilt at the same cache path with a different
Python version.
- Preserves busy/locked entries instead of misclassifying them as stale.
- Coalesces simultaneous validations for the same script.
- Retains the existing environment object when resolution produces only
a new generated ID for the same Python, avoiding false changes and
duplicate ID-keyed resources.

**Protects persistence and selection from races**

- Serializes Memento read-modify-write operations so concurrent script
selections cannot lose one another.
- Uses per-script association revisions so an older rehydration cannot
overwrite a newer selection or unset.
- Removes stale persisted values conditionally, only if the inspected
path is still current.
- Keeps failed persistence writes from changing in-memory state or
emitting success-shaped events.
- Does not globally serialize unrelated environment operations.

**Updates central active-environment tracking**

- Keys inline-script selections by normalized script path rather than
containing project, so two scripts in one workspace can retain different
environments.
- Uses per-scope revisions and manager identity checks so slow refreshes
cannot overwrite newer selections.
- Ensures failed selections and failed refreshes do not discard a valid
in-flight refresh.
- Groups same-manager batch unsets and calls the manager once with the
complete URI array.
- Updates central cache entries and events only after the manager
operation succeeds.
- Attributes inline-script change events to the script URI rather than
the containing project URI.

### Example

Given two scripts in the same workspace:

```text
tools/report.py → Python 3.12 inline environment
tools/import.py → Python 3.13 inline environment
```

PR 7 stores and retrieves those associations independently. Selecting
the environment for `import.py` does not overwrite the last-known
environment for `report.py`.

After restart:

```text
get(report.py)
→ read persisted executable
→ verify cache ownership and current metadata
→ resolve environment
→ cache and return it
```

If `report.py` later changes from `requires-python = ">=3.11"` to
`">=3.13"`, its persisted Python 3.12 environment is no longer returned
as compatible.

### Persistence and failure semantics

| Condition | Behavior |
|---|---|
| Executable exists and cache ownership is valid | Rehydrate and return
|
| Cache entry is locked/in progress | Preserve association; retry later
|
| Resolver fails transiently | Preserve association; retry later |
| Executable is definitively missing and unlocked | Remove stale
association and notify |
| A newer selection wins during rehydration | Discard the stale result |
| Memento write fails | Keep previous in-memory/persisted selection and
propagate the error |

### Tests

Coverage includes:

- assign, retrieve, unset, and batch persistence;
- restart-time lazy rehydration and delayed success events;
- metadata compatibility changes;
- missing, malformed, unowned, busy, and transient cache states;
- warm deletion and same-path rebuild detection;
- concurrent persistence, rehydration, validation, selection, and unset
races;
- failed Memento writes;
- strict URI-scope validation;
- independent same-project script selections;
- stale and failed central refresh ordering;
- atomic same-manager batch unsets.

`npm run compile-tests`, `npm run lint`, the full unit suite, and the
focused persistence/central-manager suites are clean.

### Performance

- Rehydration is lazy rather than activation-blocking.
- Warm associations are cached and validation is throttled.
- Same-script rehydration and validation work is coalesced.
- Queues cover only shared persistence and mutation ordering; unrelated
script reads and environment-manager operations remain independent.

### User impact

**No default-path user impact yet.** This completes an internal Phase 2
manager capability. Automatic routing and user-facing entry points
arrive in later roadmap PRs.

Once routing is wired, script-specific selections will survive
extension-host restarts and remain independent even for multiple scripts
in the same workspace.

### Merge order

The core persistence behavior depends on the merged manager skeleton
(#1610). This branch is rebased on current `main`; PR 8 and PR 9 build
on this capability.

---------

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 1a9f6ba1-9bd3-4664-bc25-a0d34d7a2e91
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

feature-request Request for new features or functionality

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants