Skip to content

Inline script: reusing a cached PEP 723 environment doesn't publish the interpreter until the file is saved #30

Description

@StellaHuang95

Filed from a source investigation. Everything below is cited to file:line; the one unverified link is called out explicitly at the end.

Environment data

  • Python Environments extension version: source, branch stellahuang-microsoft-inline-script-code-action @ 528346d7
  • Python extension (ms-python.python) version: N/A (observed manually)
  • VS Code version (Help → About): N/A
  • OS and version: Windows
  • Python version (& distribution if applicable, e.g. Anaconda): N/A
  • Environment manager in use: inline-script (PEP 723), behind python-envs.inlineScripts.enabled
  • Shell (bash / zsh / fish / pwsh / cmd / other): pwsh
  • Remote / container scenario (none / WSL / SSH Remote / Dev Container / Codespaces): none
  • Workspace type (single folder / multi-root / mono-repo): single folder
  • Is this a regression? If yes, last known working extension version: unknown

Repro Steps

Requires python-envs.inlineScripts.enabled: true.

  1. Create q01.py with a # /// script block declaring requires-python = ">=3.11" and dependencies = ["rich"], plus import rich.
  2. Run inline-script environment setup on it. A new cached environment is built.
  3. Create q02.py with a byte-identical metadata block (same requires-python, same dependencies) and import rich.
  4. Run inline-script environment setup on q02.py. The cached environment from step 2 is reused.

Expected behavior

After step 4, import rich resolves in q02.py without further action — as it did for q01.py in step 2.

Actual behavior

The environment is correctly selected and reused (visible in the UI), but import rich stays unresolved in q02.py until the file is saved. q01.py needed no save.

Analysis

There are four paths that can publish onDidChangeActiveEnvironment

Path Depends on manager.get()?
A manager list change → onDidChangeEnvironments No
B setEnvironmentCore fires directly (src/features/envManagers.ts:523) No
C routeability flip → handleInlineScriptRouteabilityChange (src/features/envManagers.ts:126-132, :974, :999) Yes
D manager's own onDidChangeEnvironmentrefreshEnvironment (src/features/pythonApi.ts:120-137src/features/envManagers.ts:807) Yes

onDidChangeActiveEnvironment is the only signal ms-python.python / Pylance consumes, bridged at src/features/pythonApi.ts:91-92.

Path B is designed to lose to path C: commitSelectionOperation (src/features/envManagers.ts:1211-1217) rejects B's now-stale revision because C already committed a newer one during manager.set.

Why the q01 / q02 asymmetry

Path A fires only for genuinely new environments — src/managers/builtin/inlineScript/envManager.ts:893-901 pushes a change only when !previous || !isSameDiscoveredEnvironment(...), then fires only if (changes.length > 0).

  • q01 built a new environment → path A fired, and path A does not depend on manager.get().
  • q02 reused the cache entry (routing identity is content-only — sorted/normalized/deduped deps + requires-python, src/common/inlineScript/routingRegistry.ts:243-252) → changes.length === 0no path A, leaving only C and D.

Why C and D can silently publish nothing

Both call manager.get(uri)getInternal (envManager.ts:1076-1089) → getAssociationForMetadata (:1113).

set calls invalidateCachedAssociationValidation (:1049:2024-2027), which deletes cachedAssociationValidatedAt. That makes the fast cached return at :1129-1136 unreachable immediately after setup, so the full validateCachedAssociation (:1183) always runs.

validateCachedAssociation has roughly ten return undefined exits. When it returns undefined, both C and D compute newEnv = undefined, isSameEnvironment(undefined, undefined) is true, and nothing is published — with no log line. A later save re-enters via handleSavedMetadataChange (:1513) and publishes then, which matches the observed "save fixes it".

The root design problem: a transient "cannot validate right now" is indistinguishable from "there is no environment". Notably :1196, :1204, and :1216 already handle the analogous case by returning this.fsPathToEnv.get(scriptPath) instead of undefined, so there is in-file precedent for the fix shape.

Candidates ruled out

  • Publish gate sampled before manager.set (envManagers.ts:432 vs :445). Disproven experimentally: a unit test asserting the first-time inline publish passes identically with and without a fix to that gate, because path C publishes anyway.
  • isCacheEntryBusy (:1198-1200, :2601-2611). Disconfirmed: setupEnvironment.ts:68 awaits manager.create(...) to completion before :80 calls setEnvironment, and pendingCreations is cleared in a finally at envManager.ts:424-426, so neither busy signal is set by then.
  • inspectAssociationOwnership (:1497-1511). Env-dir/cache-root scoped, not script scoped, so it returns the same result for both scripts sharing one environment.

Still unidentified

Which specific return undefined branch fires for the reused-cache case. Remaining candidates are inspectAssociationMetadata'mismatched' (:1238-1243), sidecar 'unavailable' (:1245-1247), cacheEntryMatchesRuntimeAndMetadata (:1251-1253), and hasInstalledPackagesChanged (:1254-1259). None is obviously script-specific, since the metadata identity is content-based and identical across q01/q02.

This cannot be narrowed further from source alone.

Suggested next step

Add a traceVerbose at each return undefined in validateCachedAssociation identifying the branch, reproduce, and read the Python Environments output channel. That converts this from inference to a single log line. Once the branch is known, the fix is likely to follow the :1196 precedent — return the cached environment for transient validation failures rather than undefined.

Additional context

Not related to the unresolved-import quick fix on the same branch; this reproduces through the CodeLens path too. This is an envs-extension issue, not a Pylance one: the interpreter change is never published, so there is nothing for Pylance to react to.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions