Skip to content

Add uv fallback for inline script environments (PEP 723 PR 6/16) - #1696

Open
Stella Huang (StellaHuang95) wants to merge 1 commit into
microsoft:mainfrom
StellaHuang95:pep723-pr6-uv-fallback
Open

Add uv fallback for inline script environments (PEP 723 PR 6/16)#1696
Stella Huang (StellaHuang95) wants to merge 1 commit into
microsoft:mainfrom
StellaHuang95:pep723-pr6-uv-fallback

Conversation

@StellaHuang95

@StellaHuang95 Stella Huang (StellaHuang95) commented Aug 6, 2026

Copy link
Copy Markdown
Contributor

Part of #1602 (PEP 723 inline script env support). Design doc: #1601.

This replaces the earlier closed draft #1652 with the finalized implementation rebased on main.

Roadmap context

This is PR 6 of 16 in the PEP 723 inline-script roadmap. It extends the PR 5 create() happy path with the missing-compatible-interpreter fallback.

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 this PR
PR 7: persistence (get / set + Memento) separate follow-up

Why this PR

PR 5 can create or reuse an inline-script environment when an installed base interpreter already satisfies the script's requires-python. It deliberately stops when no compatible interpreter exists.

This PR adds the consent-gated fallback for that case:

  1. Re-check installed interpreters after entering a narrow fallback queue.
  2. Select a safe Python version satisfying the complete PEP 440 requirement.
  3. Ask the user before installing uv or Python.
  4. Refresh environment discovery after installation.
  5. If discovery is stale or unavailable, resolve and validate uv's returned executable directly.
  6. Continue through the existing PR 5 cache create/reuse path.

What this PR does

Adds the inline-script fallback to InlineScriptEnvManager.create()

  • Keeps the installed-interpreter path unchanged and only enters the fallback when no compatible base exists.
  • Skips installation prompts for quick-create/noninteractive calls.
  • Serializes only fallback installations, then re-checks discovery before prompting so concurrent compatible requests reuse one installation.
  • Retains successfully direct-resolved uv interpreters so queued requests can reuse them even while discovery remains stale.

Selects a safe uv target from requires-python

  • Uses simple safe selectors directly, such as >=3.133.13 and ==3.13.13.13.1.
  • Queries uv's advertised versions for bounded or exclusion-heavy constraints.
  • Restricts catalog candidates to default CPython 3 builds satisfying the complete specifier.
  • Handles exclusions such as >=3.13.2,!=3.13.2 without installing the excluded floor.
  • Uses an advertised release for bounded ranges rather than fabricating a potentially unavailable patch-zero release.
  • Applies full PEP 440 prerelease semantics consistently across discovery, catalog selection, direct resolution, and cache validation.
  • Normalizes accepted prerelease aliases (for example, c1rc1) before passing a version to uv.

Extends the uv installer's consent flow

  • Adds an inline-script-specific prompt that shows both the script requirement and selected Python version.
  • Sanitizes and caps script-controlled prompt details.
  • Validates install selectors before forwarding them to uv.
  • If catalog lookup is required and uv is missing, asks for consent to install uv first.
  • Re-checks whether a newly installed uv is usable by the current extension host and surfaces the existing restart-required message when needed.

Handles stale discovery after installation

  • Refreshes environment discovery after uv installs Python.
  • Treats refresh/discovery failures as recoverable.
  • Resolves the executable returned by uv directly, verifies that it satisfies requires-python, and canonicalizes its path before creating the cached environment.

Examples

requires-python Fallback behavior
>=3.13 Request uv's 3.13 selector
==3.13.1 Request exactly 3.13.1 without requiring a catalog lookup
>=3.11,<3.12 Choose an advertised compatible 3.11.x release
>=3.13.2,!=3.13.2 Skip 3.13.2 and choose a compatible advertised release
>=3.15.0a1,<3.16 Permit an explicitly requested prerelease
>=3.14,<3.16 Do not select a prerelease implicitly

Safety and concurrency

  • No uv or Python installation occurs without explicit user consent.
  • Script-derived values cannot inject arbitrary uv arguments.
  • Declined, cancelled, or failed installations do not mutate the script-environment cache.
  • The fallback queue does not globally serialize environment selection or normal cache creation.
  • Existing cross-process cache locking and rollback behavior from PR 5 remains unchanged.

Tests

Coverage includes:

  • simple, exact, bounded, exclusion, and prerelease requirements;
  • uv catalog filtering and consented uv bootstrap;
  • declined and failed installations;
  • refresh and discovery failures with direct resolution;
  • simultaneous same- and different-constraint requests;
  • quick-create prompt suppression;
  • strict post-install and cache compatibility checks;
  • prompt sanitization and install-selector validation.

npm run compile-tests, npm run lint, the full unit suite, and the focused inline-script/uv suites are clean.

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.

When those entry points are wired, users whose scripts require an unavailable Python will be able to approve installing a compatible interpreter rather than having environment creation stop.

Add consent-gated uv installation when no installed interpreter satisfies a script. Coalesce matching installs, skip prompts for quick create, and directly resolve a successful installation when discovery is stale or unavailable.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 1a9f6ba1-9bd3-4664-bc25-a0d34d7a2e91
@StellaHuang95

Copy link
Copy Markdown
Contributor Author

🔒 Automated review in progress — Stella Huang (@StellaHuang95) is auto-reviewing this PR.

Comment on lines +389 to +400
private matchesInstallConstraint(requiresPython: string, version: string): boolean {
try {
return satisfiesPep440(version, requiresPython, {
prereleases: /(?:(?:a|alpha|b|beta|c|rc|pre|preview)[._-]?\d+|dev[._-]?\d+)/i.test(
requiresPython,
),
});
} catch (error) {
this.log.warn(`Unable to evaluate requires-python '${requiresPython}': ${getErrorMessage(error)}`);
return false;
}
}

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.

I would probably extract this function off the class since it is not bound to an instance and can be reused elsewhere

Comment on lines +211 to +215
...[...this.directlyResolvedBaseInterpreters.values()].filter(
(environment) =>
!metadata.requiresPython ||
this.matchesInstallConstraint(metadata.requiresPython, environment.version),
),

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.

Suggested change
...[...this.directlyResolvedBaseInterpreters.values()].filter(
(environment) =>
!metadata.requiresPython ||
this.matchesInstallConstraint(metadata.requiresPython, environment.version),
),
...this.directlyResolvedBaseInterpreters.values().filter(
(environment) =>
!metadata.requiresPython ||
this.matchesInstallConstraint(metadata.requiresPython, environment.version),
),

Comment on lines 231 to +238
let candidates = derivedChecks
.filter((candidate) => !candidate.derived)
.map((candidate) => candidate.environment);
.map((candidate) => candidate.environment)
.filter(
(candidate) =>
!metadata.requiresPython ||
this.matchesInstallConstraint(metadata.requiresPython, candidate.version),
);

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.

Maybe thes can be combined into something like

let candidates = derivedChecks
    .filter((candidate) => 
        !candidate.derived 
        && (!metadata.requiresPython || this.matchesInstallConstraint(metadata.requiresPython, candidate.version)
    )
    .map((candidate) => candidate.environment)


while (candidates.length > 0) {
const environment = pickCompatibleInterpreter(candidates, metadata.requiresPython);
const environment = pickCompatibleInterpreter(candidates, undefined);

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.

Suggested change
const environment = pickCompatibleInterpreter(candidates, undefined);
const environment = pickCompatibleInterpreter(candidates);

Comment on lines +269 to +272
this.baseInterpreterInstallationQueue = run.then(
() => undefined,
() => undefined,
);

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.

I am not entirely sure what does this mean


const installedPath = await this.installPythonAndRefresh(requiresPython, version);
if (!installedPath) {
return undefined;

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.

Maybe we can try catching this?

return selected;
}

private async selectInstallablePythonVersion(

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.

Maybe this is a followup PR, but I think we could create a PythonVersion class that can handle extracting lower/upper bounds, testing regexes, comparing, ensuring versions match constraints, etc.

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

Labels

feature-request Request for new features or functionality

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants