[Do not merge] PEP 723 inline script env support design doc#1601
[Do not merge] PEP 723 inline script env support design doc#1601StellaHuang95 wants to merge 7 commits into
Conversation
|
@brettcannon Still can't assign you as a reviewer so tagging you here, any feedback is welcome, thank you! |
| - **Lazy on open + save** — parse only when the file enters an editor or | ||
| is saved. | ||
| - **Opt-in bulk command** — user runs `Python Envs: Set Up Environments for Inline Script Files` from the command palette to discover all of them, list every detected inline-script files in a multi-select quick-pick. | ||
|
|
There was a problem hiding this comment.
What about a third option (requires pylance's help), whenever the script header is detected by the Pylance parser.
This might happen if somebody opens a script that is referenced by another script with a header (imported by would be set in Pylance).
Example:
main_script.py (has the PEP 723 script)
tools.py (is referenced by PEP 723 script)
User edits tools.py
There was a problem hiding this comment.
I was mostly thinking to actually run or edit main_script.py, the user almost always opens it at some point. And once they open it once, the current mechanism captures it, so the inline env stays set up across sessions.
Are you thinking about the case where the user opens tools.py first, and is seeing squiggles on the imports? I agree that could be annoying. The tricky part for us is that the inline env is intentionally tied to the file with the PEP 723 header. For a file like tools.py to also pick up that env as its active one, we'd need to either register it as its own "project" pointing at someone else's env, or follow the import graph and pick a "best" inline env.
So my lean is probably not do this for v1 unless we can think of a good way to solve the problem above. Detect-on-open + persistence covers most real workflows, and the bulk command addresses the same underlying frustration in a simpler way:
A user exploring a new repo can set up every inline env in one shot without needing Pylance to drive discovery. Are you seeing lots of Pylance users complaining about that? If we hear that this still isn't enough I am happy to make some improvements.
There was a problem hiding this comment.
No sounds reasonable. I was just trying to think of other situations that could cause the env to be created. My idea was more about what background things could cause the env to automatically be created, and not necessarily about Pylance behaving correctly with respect to imports in the tools.py.
|
|
||
| **Decision.** | ||
|
|
||
| Reuse the existing venv persistence mechanism.Do not invent a new |
There was a problem hiding this comment.
This is going to require changes to Pylance outside of the work to support Python Environments sub environments because the environment in this case is tied to a file and not an entire folder.
Not sure if that will work correctly or not.
There was a problem hiding this comment.
yeah that's a good point, I think it requires changes on Pylance side to make it to work. I wonder how pylance work for notebook files, maybe we can do the same thing to look up env for files with inline script.
|
|
||
| ## 8. UX flows | ||
|
|
||
| **Question:** How does the user create envs — for a single script, and |
There was a problem hiding this comment.
Personally, I think the user should never have to create environments for the script. It should just happen automatically. What do they gain by having this command?
There was a problem hiding this comment.
Right now the envs extension runs on one shared extension host process, unlike pylance which has its own process and can do the heave-lifting in its own background thread without any architecture changes.
So for the bulk command I would rather keep it user triggered. A full workspace scan + per-file parse on a large repo is too much work and may block other. For single file case, I can probably add a codeLens that looks like this so that one click setup is obvious to user.
📦 Set up environment for this script
# /// script
# dependencies = ["requests"]
# ///
| - **Sorted, whitespace-normalized dependency list.** Sort | ||
| alphabetically and strip internal whitespace so `["rich", | ||
| "requests"]` and `["requests", "rich"]` and `"requests <3"` vs | ||
| `"requests<3"` all produce the same hash. Without this the cache |
There was a problem hiding this comment.
Also make sure to normalize project names and extras so, e.g. "Django" and "django" are the same.
| changed" and "metadata change switched us to a different | ||
| installed Python". | ||
|
|
||
| 3. **On cache hit, re-verify `requires-python` is still satisfied.** |
There was a problem hiding this comment.
Also check that the underlying Python interpreter is still installed by making sure the symlink that will be run (e.g. bin/python) is still pointing to a file that exists.
Q4 - Hash inputs: - Add PEP 503 name canonicalization (lowercase, collapse [._-] to -) so "Django"/"django" and "django_extensions"/"Django-Extensions" hash to the same cache key. Same rule applied to extras. - Restructure into three explicit normalization passes (canonicalize, strip whitespace, sort) and note that the version specifier itself stays case-sensitive (PEP 440 local versions / pre-release markers). Q4 - Cache-hit validation: - Add step 4: stat the env's launcher (POSIX bin/python, Windows pyvenv.cfg home -> python.exe) to detect when the base interpreter was uninstalled (pyenv/uv/brew/apt/Add-Remove Programs). Step 2's path hash does not catch this because the path is unchanged; only the file is gone. - Rename section to "Honoring requires-python and validating cache hits" and label steps 1-2 as build-time, 3-4 as cache-hit guards. Q8 - UX flows: - Add CodeLens above the inline metadata block as the primary single-script entry point, with the existing Select Interpreter quick-pick item kept as the fallback / rebuild path. - Add rationale for keeping creation user-triggered rather than fully automatic (implicit network/install side effects, shared extension host). - Note that with the CodeLens carrying discoverability, status-bar option (A) "don't change" becomes the natural choice. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
3071f21 to
7075572
Compare
…23 PR 4/16) (#1610) > 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](#1602 (comment)) 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. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Context
PEP 723 defines inline script metadata — a
# /// script…# ///block at the top of a single-file Python script that declaresrequires-pythonanddependenciesinline, so the script is self-contained without apyproject.tomlorrequirements.txt. Tools likeuvandpipxalready honor it; users opening these scripts in VS Code today get no automatic environment support.This doc talks about how the extension should build, manage, and dispose of environments for those scripts.
What's in this doc
Eleven design questions, each with a proposed decision and the reasoning. The interesting / contentious ones:
<globalStorageUri>/script-envs-v1/<hash>/. Never inside the workspace.requires-python). The "no in-place sync" argument is the load-bearing one here.setEnvironment(scriptUri, env, /*persist*/ true)pipeline rather than inventing a new layer.setEnvironmentcall, with source pointers into Pylance and the Python extension to back the claim.Any feedback is welcome, thanks!