Repository navigation
Commit bde7cf8
Add inline script environment persistence (PEP 723 PR 7/16) (#1697)
> Part of #1602 (PEP 723 inline script env support). Design doc: #1601.
>
> This replaces the earlier closed draft #1653 with the finalized
implementation rebased on `main`.
### Roadmap context
This is **PR 7 of 16** in the PEP 723 inline-script roadmap. It adds
durable per-script environment associations to the internal manager.
| Phase 2: Manager | PR | Status |
|---|---|---|
| | PR 4: `InlineScriptEnvManager` skeleton | merged (#1610) |
| | PR 5a: generic env-creation utilities | merged (#1651) |
| | PR 5b: inline-script cache + interpreter utilities | merged (#1655)
|
| | PR 5c: `create()` happy path | merged (#1656) |
| | PR 6: `create()` uv-install fallback | open (#1696) |
| | **PR 7: persistence (`get` / `set` + Memento)** | **this PR** |
| | PR 8: activation-time discovery | follow-up |
| | PR 9: route PEP 723 scripts to the inline manager | follow-up |
### Why this PR
PR 5 can build or reuse an inline-script environment, but the manager
does not remember that the resulting environment belongs to a particular
script. After an extension-host restart, the in-memory association is
gone.
This PR implements the persistence portion of Q4 in the design:
- maintain an independent environment association for each script;
- persist script-to-environment executable paths in workspace Memento;
- lazily and safely rehydrate those associations;
- re-check current `requires-python` metadata before returning an
environment;
- report changes so the central environment API can update its
last-known state.
### What this PR does
**Implements per-script `set()`**
- Accepts one or more local `file:` URIs and rejects invalid or mixed
scopes atomically.
- Validates that selected environments are owned inline-script cache
entries.
- Persists a normalized script path → environment executable path
mapping under a dedicated Memento key.
- Supports assigning and unassigning individual scripts or batches.
- Updates in-memory state and emits `onDidChangeEnvironment` only for
effective changes.
- Leaves the existing `create()` behavior separate: creation alone does
not implicitly establish a persisted association.
**Implements per-script `get()`**
- Reads current PEP 723 metadata before returning an association.
- Keeps unreadable or temporarily invalid script metadata from
destructively clearing state.
- Returns an in-memory association when valid.
- Lazily reconstructs persisted environments after restart instead of
resolving every script during activation.
- Re-checks `requires-python` against the reconstructed Python version
before returning it.
**Safely rehydrates persisted associations**
- Requires an absolute executable path.
- Preserves associations while their cache entry is locked or being
created.
- Verifies that the executable exists and is a regular file.
- Resolves it into a `PythonEnvironment` and confirms that it belongs to
the expected extension-owned cache entry.
- Removes only definitively stale associations; transient filesystem or
resolver failures remain retryable.
- Emits a change event when a slow rehydration eventually succeeds,
including when the public API's initial one-second wait has already
elapsed.
**Validates warm in-memory associations**
- Periodically revalidates cached associations without performing full
resolution on every lookup.
- Detects executables deleted while VS Code remains open.
- Detects an environment rebuilt at the same cache path with a different
Python version.
- Preserves busy/locked entries instead of misclassifying them as stale.
- Coalesces simultaneous validations for the same script.
- Retains the existing environment object when resolution produces only
a new generated ID for the same Python, avoiding false changes and
duplicate ID-keyed resources.
**Protects persistence and selection from races**
- Serializes Memento read-modify-write operations so concurrent script
selections cannot lose one another.
- Uses per-script association revisions so an older rehydration cannot
overwrite a newer selection or unset.
- Removes stale persisted values conditionally, only if the inspected
path is still current.
- Keeps failed persistence writes from changing in-memory state or
emitting success-shaped events.
- Does not globally serialize unrelated environment operations.
**Updates central active-environment tracking**
- Keys inline-script selections by normalized script path rather than
containing project, so two scripts in one workspace can retain different
environments.
- Uses per-scope revisions and manager identity checks so slow refreshes
cannot overwrite newer selections.
- Ensures failed selections and failed refreshes do not discard a valid
in-flight refresh.
- Groups same-manager batch unsets and calls the manager once with the
complete URI array.
- Updates central cache entries and events only after the manager
operation succeeds.
- Attributes inline-script change events to the script URI rather than
the containing project URI.
### Example
Given two scripts in the same workspace:
```text
tools/report.py → Python 3.12 inline environment
tools/import.py → Python 3.13 inline environment
```
PR 7 stores and retrieves those associations independently. Selecting
the environment for `import.py` does not overwrite the last-known
environment for `report.py`.
After restart:
```text
get(report.py)
→ read persisted executable
→ verify cache ownership and current metadata
→ resolve environment
→ cache and return it
```
If `report.py` later changes from `requires-python = ">=3.11"` to
`">=3.13"`, its persisted Python 3.12 environment is no longer returned
as compatible.
### Persistence and failure semantics
| Condition | Behavior |
|---|---|
| Executable exists and cache ownership is valid | Rehydrate and return
|
| Cache entry is locked/in progress | Preserve association; retry later
|
| Resolver fails transiently | Preserve association; retry later |
| Executable is definitively missing and unlocked | Remove stale
association and notify |
| A newer selection wins during rehydration | Discard the stale result |
| Memento write fails | Keep previous in-memory/persisted selection and
propagate the error |
### Tests
Coverage includes:
- assign, retrieve, unset, and batch persistence;
- restart-time lazy rehydration and delayed success events;
- metadata compatibility changes;
- missing, malformed, unowned, busy, and transient cache states;
- warm deletion and same-path rebuild detection;
- concurrent persistence, rehydration, validation, selection, and unset
races;
- failed Memento writes;
- strict URI-scope validation;
- independent same-project script selections;
- stale and failed central refresh ordering;
- atomic same-manager batch unsets.
`npm run compile-tests`, `npm run lint`, the full unit suite, and the
focused persistence/central-manager suites are clean.
### Performance
- Rehydration is lazy rather than activation-blocking.
- Warm associations are cached and validation is throttled.
- Same-script rehydration and validation work is coalesced.
- Queues cover only shared persistence and mutation ordering; unrelated
script reads and environment-manager operations remain independent.
### User impact
**No default-path user impact yet.** This completes an internal Phase 2
manager capability. Automatic routing and user-facing entry points
arrive in later roadmap PRs.
Once routing is wired, script-specific selections will survive
extension-host restarts and remain independent even for multiple scripts
in the same workspace.
### Merge order
The core persistence behavior depends on the merged manager skeleton
(#1610). This branch is rebased on current `main`; PR 8 and PR 9 build
on this capability.
---------
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 1a9f6ba1-9bd3-4664-bc25-a0d34d7a2e911 parent 89cce49 commit bde7cf8
5 files changed
Lines changed: 2032 additions & 104 deletions
File tree
- src
- features
- settings
- managers/builtin/inlineScript
- test
- features
- managers/builtin/inlineScript
0 commit comments