Skip to content

[Do not merge] PEP 723 inline script env support design doc#1601

Draft
StellaHuang95 wants to merge 7 commits into
microsoft:mainfrom
StellaHuang95:pep723-design
Draft

[Do not merge] PEP 723 inline script env support design doc#1601
StellaHuang95 wants to merge 7 commits into
microsoft:mainfrom
StellaHuang95:pep723-design

Conversation

@StellaHuang95

Copy link
Copy Markdown
Contributor

⚠️ Not for merge. This PR exists to gather feedback on the design before any implementation is going too far.

Context

PEP 723 defines inline script metadata — a # /// script# /// block at the top of a single-file Python script that declares requires-python and dependencies inline, so the script is self-contained without a pyproject.toml or requirements.txt. Tools like uv and pipx already 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:

  • Q2 — Disk location. Proposal: <globalStorageUri>/script-envs-v1/<hash>/. Never inside the workspace.
  • Q4 — Script-to-directory mapping. Proposal: pipx-style, deps-keyed hash (adapted to honor requires-python). The "no in-place sync" argument is the load-bearing one here.
  • Q5 — Reuse vs build fresh. Only two outcomes, no "sync" path. Table of every interesting transition.
  • Q6 — Persistence. Proposal: reuse the existing setEnvironment(scriptUri, env, /*persist*/ true) pipeline rather than inventing a new layer.
  • Q7 — Cleanup. Proposal: explicit command + opportunistic 14-day TTL (matches pipx).
  • Q9 / Q10 — Pylance & Run/F5. Essentially solved by Q6's setEnvironment call, with source pointers into Pylance and the Python extension to back the claim.

Any feedback is welcome, thanks!

@StellaHuang95

Copy link
Copy Markdown
Contributor Author

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

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Comment thread pep723_design_questions.md Outdated

## 8. UX flows

**Question:** How does the user create envs — for a single script, and

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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"]
 # ///

Comment thread pep723_design_questions.md Outdated
- **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

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

StellaHuang95 and others added 7 commits June 24, 2026 14:37
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>
StellaHuang95 added a commit that referenced this pull request Jun 24, 2026
…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>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants