microsoft / microsoft/vscode-python-environments

Add PEP 723 inline script env support

Open
#1,602 0 comments 2 reactions 1 assignee View on GitHub

@StellaHuang95 is already working on this.

Since Jun 22, 2026.

feature-request
Dominant language
TypeScript
Stars
138
Forks
62
Avg merge
1d 4h
Merged PRs (30d)
35

Description

See https://github.com/microsoft/vscode-python-environments/pull/1601 for the design document.

## PR phases at a glance

### Phase 1 — Foundation (parallelizable, no behavior change)

- [x] **PR 1 — Cache key hash utility** — Pure functions for dependency-list normalization (sort, lowercase, strip whitespace) and SHA-256 truncation. Unit tests only. _Depends on: —_
- [x] **PR 2 — Cache layout + `meta.json` sidecar helpers** — Resolve `/script-envs-v1/`; typed `MetaJson` interface; atomic read/write; pure TTL-eviction helper (given a list of entries, return the paths to delete). _Depends on: —_
- [x] **PR 3 — `requires-python` → interpreter selection** — Filter `api.getEnvironments('global')` via `matchesPythonVersion`; extract a lower-bound version (`">=3.13"` → `"3.13"`) for the uv-install fallback. _Depends on: —_

> All three can be developed in parallel.

### Phase 2 — Manager (internal only)

- [x] **PR 4 — `InlineScriptEnvManager` skeleton** — Class implementing `EnvironmentManager`; `displayName = "Inline script environments"`; registered in `extension.ts`. `getEnvironments` returns `[]`. No `create` / `set` yet. Smoke check: appears as an empty section in the picker. _Depends on: —_
- [x] **PR 5 — `create()` happy path** — Given `(scriptUri, metadata)`: pick a compatible installed Python (PR 3), compute the hash (PR 1), build via existing `createWithProgress`, install dependencies, and write `meta.json` (PR 2). No persistence. No uv-install fallback. _Depends on: 1, 2, 3, 4_
- [x] **PR 6 — `create()` uv-install fallback** — Extend the `promptInstallPythonViaUv` trigger union with `'inlineScript'`; thread the `requires-python` lower bound to `installPythonWithUv(version)`; wire it into `create()` for the no-compatible-interpreter case. _Depends on: 3, 5_
- [x] **PR 7 — Persistence: `get` / `set` + Memento** — New `INLINE_SCRIPT_ENVS_KEY`; per-URI `fsPathToEnv` map; cache-hit re-verification of `requires-python` (Q4 step 3). Mirrors the `VenvManager` pattern. _Depends on: 4_
- [x] **PR 8 — Activation-time discovery** — Walk the cache directory, load sidecars, resolve through `nativeFinder.resolve()`, and register items in the manager. Defer via `setImmediate` so activation is not blocked. _Depends on: 2, 4, 7_

> After Phase 2, the manager is fully functional but nothing automatically uses it.

### Phase 3 — Routing (the "go-live" PRs)

- [x] **PR 9 — Route PEP 723 scripts to the inline manager** — In `envManagers.getEnvironmentManager(uri)`, return the inline manager when `uri` is a known PEP 723 script **and** a validated cached environment exists; otherwise fall through to normal routing. Use lazy parsing with per-URI memoization. _Depends on: 4, 7_
- [x] **PR 10 — Per-script project registration** — When an inline environment is created or set, register the script as an exact `pythonProjects[]` entry for **user-visible project surfacing**. Routing and persistence across restart are handled by the dedicated `InlineScriptAssociationStore` (+ metadata-identity binding), **not** by this entry — exact-manager resolution deliberately ignores the managed entry (`getExactProjectEnvironmentManager` returns `undefined` for it), and the per-file change event fires from the inline manager's own selection. Cleanup removes only the generated inline-script entry. _Depends on: 9_

### Phase 3.5 — Cross-repository integration (Pylance + Python extension)

These PRs live in other repositories (`microsoft/pyrx` for Pylance and `microsoft/vscode-python` for the debugger fix). **PR 17 (#9265) and PR 19 (#26129) are required** for the feature to work end-to-end; **PR 18 is optional** (a later performance optimization — see below). They can be developed in parallel with Phases 1–3, but PR 17 and 19 must land before Phase 4 ships so the user-facing setup action activates the correct language-service and debugging behavior.

- [x] **PR 17 — [`microsoft/pyrx`] Per-file `pythonPath` lookup for `.py` files** — Extend `documentWorkspaceResolver.getWorkspaceForFile` to query the per-file Python path through the existing `workspace/configuration` request, mirroring the notebook-cell path. When the per-file path differs from the workspace path, mimic the file into an immutable workspace pinned to that interpreter (`_mimicOpenFiles` / `_getOrCreateImmutableCopy`, with `_filterResultsToCurrentWorkspace` keeping diagnostics attributed correctly); when it matches, retain the existing workspace with no additional cost. **The same PR (#9265) also handles change-time re-routing** on the existing `didChangeConfiguration` signal (`_revalidateOpenRegularFiles` → `revalidateWorkspaceForFile`), so no new notification is required. _Depends on: 7_
- [x] **PR 18 — [`microsoft/pyrx`] (OPTIONAL — performance only) Dedicated per-file change notification** — Superseded for correctness by PR 17 (#9265), which already re-routes open `.py` files on the existing `workspace/didChangeConfiguration` signal. A dedicated `python/didChangeFilePythonPath` notification would only be a later optimization to re-route a single file instead of re-scanning all open files. **Not required to ship the feature.** _Depends on: 10, 17_
- [x] **PR 19 — [`microsoft/vscode-python`] Debug resolver per-file environment lookup fix** (shipped as #26129) — In `getInterpreterForDebugConfiguration` (`base.ts`), resolve `${file}` / `${workspaceFolder}` to an absolute program path and look up the interpreter scoped to that file via `getActiveInterpreter(programUri, { exactResource: true })`, falling back to the workspace folder. When the program env differs, set `__pythonIsProgramInterpreter` so `launch.ts` activates it. A matching `{ exactResource: true }` lookup is added to the shared middleware base (`languageClientMiddlewareBase.ts`), since vscode-python hosts Pylance. **Larger than the original ~10-line estimate**; `exactResource` is a no-op unless `python.useEnvironmentsExtension` is enabled. _Depends on: 7_

> **Run Python File** (the green triangle) already routes per-file through `codeExecutionManager → runInTerminal → getEnvironment(fileUri)`, so it needs no cross-repository work — **when the Environments extension owns resolution** (`python.useEnvironmentsExtension` / `useEnvExtension()`). Behavior with that setting off is out of scope.

### Phase 4 — UX (the user-facing entry points)

- [ ] **PR 11 — CodeLens: _Set up environment for this script_** — Show a CodeLens above the `# /// script` block only when saved PEP 723 metadata has no matching inline environment. Clicking it creates or reuses the environment, registers the exact script project (PR 10), persists the association, and publishes the per-file environment change. Hide it while the association is current; show it again after metadata invalidates the association. _Depends on: 5, 9, 10_
- [ ] **PR 12 — Bulk command: _Set Up Environments for Inline Script Files_** — `workspace.findFiles('**/*.py', exclude)` → parse/filter → multi-select quick pick → run the same create/register/persist flow as PR 11 for each selected script. Cap results; exclude `.venv` and `node_modules`. _Depends on: 5, 9, 10_

### Phase 5 — Lifecycle, telemetry, and polish

- [x] **PR 13 — Command: _Clear Script Environment Cache_** — Modal confirmation; delete the cache bucket; clear Memento; remove generated `pythonProjects[]` entries; fire `onDidChangeEnvironment`. Reuse the existing venv-removal safety guards. _Depends on: 2, 7_
- [x] **PR 14 — Opportunistic TTL eviction** — Once per session, walk the cache during environment creation/reuse and delete environments whose `lastUsedAt` is older than 14 days. Reuse PR 13 cleanup helpers. _Depends on: 2, 7, 13_
- [x] **PR 15 — Remaining `inlineScript.*` telemetry** — `envCreated`, `envReuseHit`, and `envError`, including the `'compatible-python-declined'` category. Add typed GDPR schema entries and call sites. _Depends on: 5, 6, 7_
- [x] **PR 16 — Status-bar decision: no special treatment (Option A)** — Before setup, keep showing the current workspace/default environment. PR 11's CodeLens provides discoverability. After setup, the existing active-document environment lookup naturally shows `Python X.Y (inline)`. When metadata becomes stale, routing falls back and the CodeLens returns. **No separate implementation PR is required.**

## Dependency summary

```text
[1] [2] [3] [4] ──► [5] and [7]
[5] ──► [6]
[2] [4] [7] ──► [8]
[4] [7] ──► [9] ──► [10] ──► [11] and [12]
[2] [7] ──► [13] ──► [14]
[5] [6] [7] ──► [15]
[7] ──► [17]
[10] [17] ──► [18] (optional; perf only — not required to ship)
[7] ──► [19]
[16] ──► resolved design decision; no code
```

## Why these seams

| PRs | Cohesion principle |
|---|---|
| 1 vs 2 | Hashing is pure crypto; `meta.json` is filesystem + `globalStorageUri` integration. Different review areas. |
| 5 vs 6 | The happy path stays in the inline manager; fallback touches `uvPythonInstaller.ts` and changes the trigger union. |
| 7 separate from 5 | Persistence is the easiest place to introduce regressions; isolating it makes bisecting easier. |
| 9 vs 10 | Routing is read-only; project registration writes settings. Different reversal costs. |
| 11 vs 12 | Single-script CodeLens work runs on editor/document changes; bulk setup is a one-shot workspace scan. Different performance and UX concerns. |
| 13 vs 14 | Clear-cache is user-triggered and destructive; TTL is silent and opportunistic. Different telemetry and user-trust profiles. |
| 17 vs 18 | Open-time per-file lookup is read-only; change-time rerouting moves files and forces reanalysis. |
| 19 vs 17/18 | Different repositories, teams, and release cycles. The debugger fix is self-contained and helps all per-file interpreter users. |

## First-rollout UX decision

Use a **CodeLens as the only single-script setup surface**:

```text
Set up environment for this script
# /// script
# dependencies = ["requests"]
# ///
```

- Before setup, the status bar remains unchanged and shows the current workspace/default environment.
- Clicking the CodeLens creates or reuses the inline environment, registers the exact script project, persists the association, and publishes the active-environment change.
- After setup, the existing status-bar lookup naturally shows the inline environment.
- If setup fails or is cancelled, the current environment stays active and the CodeLens remains.
- If saved metadata later invalidates the association, routing falls back and the CodeLens returns.
- Do not add a special Select Interpreter setup item or a separate status-bar implementation for the first rollout.

## Behavioral cut-over

PR 9 is the only PR in this repository that changes implicit routing behavior. Keep it behind `python-envs.inlineScripts.enabled` until the user-facing rollout is intentional. Cross-repository PRs 17–19 are silent no-ops for users without a per-file environment registration, and their per-file interpreter routing is active only when `python.useEnvironmentsExtension` is enabled.

Contributor guide

Open the contributing guide

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. Open a pull request that references the issue number.

Assessment

This issue has not been assessed yet.

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.