Skip to content

Add PEP 723 inline script env support #1602

Description

@StellaHuang95

See #1601 for design doc.

PR Phases at a glance

Phase 1 — Foundation (parallelizable, no behavior change)

  • PR 1 — Cache key hash utility — Pure functions: dep-list normalization (sort, lowercase, strip whitespace), 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 → list of paths to delete). Depends on: —
  • PR 3 — requires-python → interpreter selection — Filter api.getEnvironments('global') via matchesPythonVersion; extract 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 compatible installed Python (PR 3), compute hash (PR 1), build via existing createWithProgress, install deps, write meta.json (PR 2). No persistence. No uv-install fallback. Depends on: 1, 2, 3, 4
  • PR 6 — create() uv-install fallback — Extend promptInstallPythonViaUv trigger union with 'inlineScript'; thread the requires-python lower bound to installPythonWithUv(version); wire 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-verify of requires-python (Q4 step 3). Mirrors VenvManager pattern. Depends on: 4
  • PR 8 — Activation-time discovery — Walk cache dir, load sidecars, resolve via nativeFinder.resolve(), register items into the manager. Deferred via setImmediate so it doesn't block activation. 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 managerenvManagers.getEnvironmentManager(uri): if uri is a known PEP 723 script and a cached env exists, return the inline manager; else fall through. Lazy parse with per-URI memoization (or extend InlineScriptLazyDetector with a public isInlineScript(uri) query). Depends on: 4, 7
  • PR 10 — Per-script project registration — When the inline env is created/set, register the script as a pythonProjects[] entry so addPythonProjectSetting (called from setEnvironment) routes correctly. Cleanup on uninstall. Depends on: 9

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

These PRs live in other repos (microsoft/pyrx for Pylance, microsoft/vscode-python for the debug fix). They are required for the feature to be user-visible end-to-end (see Q9 and Q10 in the design doc). They can be developed in parallel with Phases 1–3 but need to land before Phase 4 ships in this repo, so that the user-facing entry points light up correctly.

  • PR 17 — [microsoft/pyrx] Per-file pythonPath lookup for .py files in Pylance — Extend documentWorkspaceResolver.getWorkspaceForFile to query per-file pythonPath via the existing workspace/configuration request (mirroring the notebook-cell path already in that file). Inject _getConfiguration into the resolver. When the per-file pythonPath differs from the workspace's, _getOrCreateBestWorkspaceFileSync creates an immutable sub-workspace pinned to that interpreter via the existing _createImmutableCopy machinery; when it equals the workspace's, the equality check short-circuits and behavior is unchanged. Depends on: 7 (env API returns per-file env for known scripts)
  • PR 18 — [microsoft/pyrx] Per-file env change notification + handler — Add custom LSP notification python/didChangeFilePythonPath (declared in pylance-internal/src/customLSP.ts). Add a server-side handler in asyncServer.ts that mirrors _changeNotebookKernel: re-resolve per-file pythonPath, call moveFiles([fileUri], oldWorkspace, newWorkspace), invalidateAndForceReanalysis, and tryAutoDispose the old workspace if empty. Extend notifyChanges in vscode-pylance/src/common/pythonEnvironmentApi.ts to send the new notification for .py URIs (alongside the existing .ipynb branch). Depends on: 17, 10 (event fires with e.uri = scriptUri)
  • PR 19 — [microsoft/vscode-python] Debug resolver per-file env lookup fix — In resolveAndUpdatePythonPath (src/client/debugger/extension/configuration/resolvers/base.ts), prefer the program URI for getActiveInterpreter when present, falling back to the workspace folder. Same shape applied to both the pythonPath and python branches. ~10 LOC. Fixes a pre-existing per-file scope gap that affects any "Select Interpreter per file" user with the env extension enabled, not just PEP 723 — F5 / Debug-in-Terminal currently launch with the workspace env even when a per-file env was selected. Depends on: 7 (env API returns per-file env)

The "Run Python File" (green triangle) path already routes per-file correctly via codeExecutionManager → runInTerminal → getEnvironment(fileUri), so it needs no cross-repo work — it lights up automatically once we ship PR 7.

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

  • PR 11 — Top-level "Set up env for this script" picker item — Extend EnvironmentPickOptions with inlineScriptContext?: { uri, metadata }; conditional row at the top of pickEnvironment. Depends on: 5, 9
  • PR 12 — Bulk command: Set Up Environments for Inline Script Filesworkspace.findFiles('**/*.py', exclude) → parse filter → multi-select quick-pick → loop create. Caps result count; excludes .venv, node_modules. Depends on: 5, 9

Phase 5 — Lifecycle, telemetry & polish

  • PR 13 — Command: Clear Script Environment Cache — Modal confirm; delete bucket; clear Memento; remove pythonProjects[] entries; fire onDidChangeEnvironment. Reuses validateVenvRemovalPath guards. Depends on: 2, 7
  • PR 14 — Opportunistic TTL eviction — Debounced once-per-session walk on env-creation path; delete envs where lastUsedAt > 14d; reuses PR 13's cleanup helpers. Depends on: 2, 7, 13
  • PR 15 — Remaining inlineScript.* telemetryenvCreated, envReuseHit, envError (with category enum incl. 'compatible-python-declined'). Schema entries + call sites. Depends on: 5, 6, 7
  • PR 16 — Status-bar treatment — Implementation of whichever Q8 option (A/B/C) is chosen. Depends on: 9 + design decision

Dependency / parallelism diagram

[1] [2] [3]                ← Phase 1: all parallel
   ╲ │ ╱
    [4]                    ← skeleton
   ╱  │  ╲
 [5]  [7]  [8]             ← parallelizable after [4] (+ deps for 5/8)
  │
 [6]                       ← uv fallback, builds on [5]
  │
  └──► [9] ─► [10]         ← go-live (this repo)
              ╲ ╲
               ╲ [17] ─► [18]   ← cross-repo go-live (pyrx)
                ╲
                 [19]            ← cross-repo go-live (vscode-python, independent)
                 │
            [11] [12]      ← UX in parallel (needs Phase 3.5 for full effect)
                  │
            [13] ─► [14]   [15]   [16]

Why these specific seams

PR Cohesion principle
1 vs 2 Hash is pure crypto; meta.json is fs + globalStorageUri integration. Different review eyes.
5 vs 6 Happy-path stays in the inline manager; fallback touches uvPythonInstaller.ts and changes a public type (the trigger union).
7 separate from 5 Persistence is the easiest place to introduce a regression; isolating it makes bisecting trivial.
9 vs 10 Routing is read-only; project registration writes to settings.json. Different reversal cost.
11 vs 12 Single-script is on the hot path (every picker open); bulk is one-shot. Different perf/UX concerns.
13 vs 14 Clear-cache is user-triggered & destructive; TTL is silent & opportunistic. Different telemetry & user-trust profiles.
17 vs 18 Open-time per-file lookup (read-only) vs change-time re-routing (writes via moveFiles + invalidateAndForceReanalysis). Different test surfaces and reversal cost.
19 vs 17 / 18 Different repo (vscode-python vs pyrx), different team, different release cycle. The debug fix is self-contained and benefits non-PEP-723 users too, so it can ship first.

Behavioral cut-over

PR 9 is the only one in this repo that changes implicit behavior for users who have never invoked the feature. Cross-repo PRs 17–19 only have effect once a user has registered a per-file env (which happens via the Phase 4 entry points, gated on PRs 9–10); for users without any per-file env registration they are silent no-ops. If extra caution is warranted, gate PR 9 behind python-envs.inlineScripts.enabled.

Metadata

Metadata

Assignees

Labels

feature-requestRequest for new features or functionality

Type

No type
No fields configured for issues without a type.

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions