You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
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 manager — envManagers.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
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 Files — workspace.findFiles('**/*.py', exclude) → parse filter → multi-select quick-pick → loop create. Caps result count; excludes .venv, node_modules. Depends on: 5, 9
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.
See #1601 for design doc.
PR Phases at a glance
Phase 1 — Foundation (parallelizable, no behavior change)
meta.jsonsidecar helpers — Resolve<globalStorage>/script-envs-v1/<hash>; typedMetaJsoninterface; atomic read/write; pure TTL-eviction helper (given a list of entries → list of paths to delete). Depends on: —requires-python→ interpreter selection — Filterapi.getEnvironments('global')viamatchesPythonVersion; extract lower-bound version (">=3.13"→"3.13") for the uv-install fallback. Depends on: —Phase 2 — Manager (internal only)
InlineScriptEnvManagerskeleton — Class implementingEnvironmentManager;displayName = "Inline script environments"; registered inextension.ts.getEnvironmentsreturns[]. Nocreate/setyet. Smoke check: appears as an empty section in the picker. Depends on: —create()happy path — Given(scriptUri, metadata): pick compatible installed Python (PR 3), compute hash (PR 1), build via existingcreateWithProgress, install deps, writemeta.json(PR 2). No persistence. No uv-install fallback. Depends on: 1, 2, 3, 4create()uv-install fallback — ExtendpromptInstallPythonViaUvtrigger union with'inlineScript'; thread therequires-pythonlower bound toinstallPythonWithUv(version); wire intocreate()for the no-compatible-interpreter case. Depends on: 3, 5get/set+ Memento — NewINLINE_SCRIPT_ENVS_KEY; per-URIfsPathToEnvmap; cache-hit re-verify ofrequires-python(Q4 step 3). MirrorsVenvManagerpattern. Depends on: 4nativeFinder.resolve(), register items into the manager. Deferred viasetImmediateso it doesn't block activation. Depends on: 2, 4, 7Phase 3 — Routing (the "go-live" PRs)
envManagers.getEnvironmentManager(uri): ifuriis a known PEP 723 script and a cached env exists, return the inline manager; else fall through. Lazy parse with per-URI memoization (or extendInlineScriptLazyDetectorwith a publicisInlineScript(uri)query). Depends on: 4, 7pythonProjects[]entry soaddPythonProjectSetting(called fromsetEnvironment) routes correctly. Cleanup on uninstall. Depends on: 9Phase 3.5 — Cross-repo integration (Pylance + Python extension)
These PRs live in other repos (
microsoft/pyrxfor Pylance,microsoft/vscode-pythonfor 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.microsoft/pyrx] Per-file pythonPath lookup for.pyfiles in Pylance — ExtenddocumentWorkspaceResolver.getWorkspaceForFileto query per-file pythonPath via the existingworkspace/configurationrequest (mirroring the notebook-cell path already in that file). Inject_getConfigurationinto the resolver. When the per-file pythonPath differs from the workspace's,_getOrCreateBestWorkspaceFileSynccreates an immutable sub-workspace pinned to that interpreter via the existing_createImmutableCopymachinery; 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)microsoft/pyrx] Per-file env change notification + handler — Add custom LSP notificationpython/didChangeFilePythonPath(declared inpylance-internal/src/customLSP.ts). Add a server-side handler inasyncServer.tsthat mirrors_changeNotebookKernel: re-resolve per-file pythonPath, callmoveFiles([fileUri], oldWorkspace, newWorkspace),invalidateAndForceReanalysis, andtryAutoDisposethe old workspace if empty. ExtendnotifyChangesinvscode-pylance/src/common/pythonEnvironmentApi.tsto send the new notification for.pyURIs (alongside the existing.ipynbbranch). Depends on: 17, 10 (event fires withe.uri = scriptUri)microsoft/vscode-python] Debug resolver per-file env lookup fix — InresolveAndUpdatePythonPath(src/client/debugger/extension/configuration/resolvers/base.ts), prefer the program URI forgetActiveInterpreterwhen present, falling back to the workspace folder. Same shape applied to both thepythonPathandpythonbranches. ~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)Phase 4 — UX (the user-facing entry points)
EnvironmentPickOptionswithinlineScriptContext?: { uri, metadata }; conditional row at the top ofpickEnvironment. Depends on: 5, 9workspace.findFiles('**/*.py', exclude)→ parse filter → multi-select quick-pick → loopcreate. Caps result count; excludes.venv,node_modules. Depends on: 5, 9Phase 5 — Lifecycle, telemetry & polish
pythonProjects[]entries; fireonDidChangeEnvironment. ReusesvalidateVenvRemovalPathguards. Depends on: 2, 7lastUsedAt > 14d; reuses PR 13's cleanup helpers. Depends on: 2, 7, 13inlineScript.*telemetry —envCreated,envReuseHit,envError(withcategoryenum incl.'compatible-python-declined'). Schema entries + call sites. Depends on: 5, 6, 7Dependency / parallelism diagram
Why these specific seams
globalStorageUriintegration. Different review eyes.uvPythonInstaller.tsand changes a public type (the trigger union).settings.json. Different reversal cost.moveFiles+invalidateAndForceReanalysis). Different test surfaces and reversal cost.vscode-pythonvspyrx), 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.