Skip to content

Add PEP 723 inline script env support #1602

Description

See #1601 for the design document.

PR phases at a glance

Phase 1 — Foundation (parallelizable, no behavior change)

  • 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: —
  • PR 2 — Cache layout + meta.json sidecar helpers — Resolve <globalStorage>/script-envs-v1/<hash>; typed MetaJson interface; atomic read/write; pure TTL-eviction helper (given a list of entries, return the paths to delete). Depends on: —
  • 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)

  • 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: —
  • 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
  • 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
  • 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
  • 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)

  • 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
  • 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.

  • 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 (_revalidateOpenRegularFilesrevalidateWorkspaceForFile), so no new notification is required. Depends on: 7
  • 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
  • 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 Filesworkspace.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

  • 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
  • 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
  • PR 15 — Remaining inlineScript.* telemetryenvCreated, envReuseHit, and envError, including the 'compatible-python-declined' category. Add typed GDPR schema entries and call sites. Depends on: 5, 6, 7
  • 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

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

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.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Labels

feature-requestRequest for new features or functionality

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions