Skip to content

Commit e0eaf5d

Browse files
eleanorjboydCopilot
andcommitted
Merge main into fix/issue-1885-default-interpreter-path-not-venv
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2 parents 8c37342 + d44725a commit e0eaf5d

62 files changed

Lines changed: 6629 additions & 602 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

‎.github/instructions/testing-workflow.instructions.md‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ This guide covers the full testing lifecycle:
2020

2121
- Pip commands that return JSON must pass `--disable-pip-version-check`; the process helper combines stderr with stdout, so update notices can otherwise make valid JSON unparseable (1).
2222
- When a view subscribes to a newly added provider event, TypeMoq-based view tests must return a real `EventEmitter.event`; an unstubbed event yields an undefined disposable and fails during teardown (1).
23+
- Test agent selections across a full reload with `python.defaultInterpreterPath` set. An effective manager value equal to the extension default does not prove a workspace value was saved; tool-owned persistence must inspect `workspaceValue` or startup can restore the global interpreter (1).
2324

2425
### When to Use This Guide
2526

‎CONTRIBUTING.md‎

Lines changed: 146 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -98,6 +98,152 @@ The npm package under [`api/`](./api) is the public API facade other extensions
9898
- `src/api.ts`, `src/types.ts`, and `src/publicErrors.ts` are validated on every PR by the extension's own lint and TypeScript compile.
9999
- **Versioning and compatibility:** the published package version in [`api/package.json`](./api/package.json) is maintained independently of the extension version in [`package.json`](./package.json) — the two do not need to match. Compatibility is based on the API shape exported by the installed Python Environments extension at runtime. Package updates must preserve backwards-compatible contracts unless the API package version intentionally communicates a breaking change; consumers should treat newly added members as optional when they may run against older installed extension versions. Any PR that edits `src/api.ts`, `src/types.ts`, or `src/publicErrors.ts` must bump `api/package.json` (use the `skip api version` label to bypass) and add an entry to [`api/CHANGELOG.md`](./api/CHANGELOG.md) (use the `skip api changelog` label to bypass).
100100

101+
## Internal Python agent-tool bridge
102+
103+
The extension's existing **flat** export also provides `__pythonTools.version === 1`.
104+
This is an internal convention, not a security boundary or a supported public API.
105+
Its types live only in `src/internal/pythonToolsApi.ts`; do not copy them into
106+
`src/types.ts`, `src/api.ts`, or the published API package. Consumers must check the
107+
version and all three methods at runtime before using the private API. The Python
108+
consumer uses its previous public Environments integration when that capability
109+
is absent or incompatible; that compatibility route can still prompt. It never
110+
falls back after a private operation starts, including errors, partial results,
111+
timeouts, or cancellation. Environments-disabled users retain the legacy Python
112+
route.
113+
Explicit compatibility selection persists through the public Environments setter, not
114+
just the Python extension's cached interpreter path, so later package operations
115+
use the same environment. Reusing an already-selected environment is read-only;
116+
it does not reselect or wait for an interpreter-change event.
117+
118+
| Method | Request | Result |
119+
| --- | --- | --- |
120+
| `configureEnvironment(request, token)` | `{ resourcePath?: string, pythonPath?: string }` | Select an explicit interpreter exactly; otherwise reuse an isolated environment or create one for the project. Success includes `created`. |
121+
| `getEnvironment(request, token)` | `{ resourcePath?: string, includePackages?: boolean }` | Resolve the real selected environment, not the public API's short-timeout cache. `includePackages: true` returns `{ name, version?: string }[]`, including `[]`, or an error. |
122+
| `installPackages(request, token)` | `{ resourcePath?: string, packages: string[] }` | Install into an isolated environment. Does not create or change selection implicitly, or modify any global/base interpreter. |
123+
124+
Automatic configuration uses isolation by default, regardless of whether a global
125+
interpreter was selected automatically or by a person. Virtual environments are
126+
recognized by `pyvenv.cfg`; non-base Conda and inline-script environments are also
127+
isolated. Existing isolated environments outside the workspace remain reusable.
128+
When no isolated environment is selected, configuration can resolve Poetry/Pipenv
129+
caches through the native finder's project association. It does not infer ownership
130+
from the cache location or record selection history; ambiguous project matches
131+
require an explicit `pythonPath`. Read-only queries do not select these candidates.
132+
There is no selection-origin bookkeeping or persistence. Old origin records are
133+
ignored; ordinary current-interpreter selection and its persistence are unchanged.
134+
An explicit `pythonPath` selects that interpreter without creating an environment.
135+
This can select global Python for execution or queries, but does not authorize
136+
package installation into it: `ENVIRONMENT_NOT_ISOLATED` directs the caller to
137+
configure without `pythonPath` or select an existing isolated environment.
138+
139+
All methods require a VS Code `CancellationToken` and a trusted workspace.
140+
`resourcePath` accepts an absolute path or a `file:` URI within an open workspace.
141+
The Python tools default an omitted or empty path to the first workspace folder and
142+
pass it explicitly. They disclose the target in preparation/results and instruct agents
143+
to reuse the returned `resourcePath`. Editor focus does not change this default.
144+
At the direct private API boundary, omission selects the only workspace root;
145+
zero/multiple roots still produce actionable errors. The consumer default does not
146+
relax that backend validation.
147+
Other explicit input is never replaced by that default. Because the route is
148+
workspace scoped, read-only tools keep their previous Environments behaviour when
149+
no folder is open. Configuration and installation with a compatible private API
150+
report `NO_WORKSPACE` instead of opening a picker or modifying global Python.
151+
Nested projects and per-script selections retain their scope; a file is
152+
never used as an environment creation directory. There is no User/global settings
153+
fallback. An explicit `pythonPath` can switch away from an unavailable manager.
154+
New nested targets persist as exact project entries without changing root defaults;
155+
their automatic dependency discovery is restricted to that target directory.
156+
Tool selections at the workspace root persist an explicit workspace manager, even
157+
when it matches the extension default. This prevents `python.defaultInterpreterPath`
158+
from restoring global Python on reload without rewriting that interpreter setting.
159+
Ordinary human selections retain their existing settings-write behavior.
160+
161+
Results are `{ status: 'success', environment, resourcePath?, created?, packages? }`
162+
or `{ status: 'error', code, message, environment?, resourcePath? }`. Errors retain
163+
an already-created environment when subsequent package installation or selection
164+
fails; retries reuse a resolvable `.venv`/`.conda` rather than creating a suffixed
165+
directory. Selecting an existing environment does not reinstall project
166+
dependencies. A resolvable `.venv`/`.conda` is only reused when it actually contains
167+
the resolved isolated environment. A new subdirectory can reuse an isolated parent-project
168+
selection; `resourcePath` chooses the target, not a requirement to create a fresh
169+
environment.
170+
Register an independent project when it needs a separate selection.
171+
Reads and writes for the same effective target share a cancellable
172+
queue; waiting behind an operation does not use the discovery timeout. Different
173+
targets remain independent. Discovery timeouts report `NOT_READY`; a configured
174+
environment manager that never registers reports `UNSUPPORTED_MANAGER` naming the id;
175+
subprocess
176+
timeouts report `TIMEOUT`. Cancellation throws `CancellationError` after owned
177+
process cleanup. Cancellation is cooperative, not a rollback: completed writes or
178+
package changes are not undone. A completed action remains successful if a
179+
cancellation request arrives on its return. If cleanup fails,
180+
`PROCESS_TERMINATION_FAILED` takes precedence:
181+
the process may still be modifying files, so consumers must not blindly retry.
182+
Cleanup waits for process closure and owned POSIX group completion; an exited
183+
Windows parent cannot turn a failed tree termination into successful cancellation.
184+
If a Windows parent has already exited when cancellation starts, descendant
185+
cleanup cannot be confirmed; the tool reports that uncertainty instead of
186+
signalling a potentially reused PID.
187+
Only internal `toolExecution` calls enable these subprocess policies. Public
188+
progress tokens do not enable agent timeouts, input suppression, or process-tree
189+
cleanup semantics. Public package calls retain their existing execution defaults.
190+
191+
Internal symbol capabilities opt built-in managers into prompt-free operations;
192+
they are unrelated to the human UI's `quickCreateConfig`. Creation supports venv,
193+
Conda, and enabled PEP 723 inline scripts (the latter retain their existing cache
194+
ownership/metadata rules). System and Pyenv base interpreters route creation to
195+
venv; Conda base routes to an isolated Conda prefix. Poetry and Pipenv environments
196+
can be reused, but unsupported creation and contributed managers without this
197+
capability fail explicitly. Private package operations use the existing headless pip/uv, Conda, and
198+
Poetry helpers with the real token and strict, uncached package listing. Venv
199+
dependency validation fails rather than asking to continue. No tool installs a
200+
base Python automatically. Conda response descriptors clone `execInfo.activatedRun`
201+
to a standalone `conda run --prefix ... --no-capture-output ...` command; shared
202+
public descriptors are not changed.
203+
Venv creation uses the selected Python 3 as its base, falling back to the latest
204+
discovered Python 3 only when none is selected. Conda creation requests the selected
205+
Python major/minor version. This does not add a project version-constraint resolver
206+
for `pyproject.toml` or `.python-version`. To choose a particular base, select it
207+
first and then configure without `pythonPath`; the path parameter itself is an
208+
exact selection request, not a venv base-interpreter argument.
209+
Bare Conda commands configured through `python.condaPath` are resolved through
210+
PATH for private execution and returned command prefixes.
211+
Conda base is isolated even when inherited through `CONDA_PREFIX`, and failed
212+
Conda discovery does not authorize replacement creation. Public Conda refresh remains best-effort while
213+
the private readiness check retains the discovery error. Poetry verifies the
214+
requested project's actual environment prefix before package operations. Agent
215+
inventory uses `poetry run pip list --format=json`, including when Poetry supplies
216+
pip itself; the public lockfile-based `poetry show` path is unchanged.
217+
Private Poetry installs also apply the lockfile with `install --no-root`, since
218+
`add` alone skips already-declared dependencies missing from the environment.
219+
Direct installs into shared PEP 723
220+
caches return `IMMUTABLE_ENVIRONMENT`; edit script metadata and configure again.
221+
With the inline-script feature enabled, configuring a Python file with PEP 723
222+
metadata can create its cache on the first tool invocation, without first using a
223+
CodeLens or registering a script project manually. Invalid metadata fails rather
224+
than falling back to ordinary project creation. This opt-in configuration route
225+
does not change human or read-only routing, which still requires a validated
226+
association. Target the project directory or supply `pythonPath` for ordinary
227+
project/exact-interpreter configuration instead.
228+
An agent joining a human-owned inline cache build gets `ENVIRONMENT_BUSY` without
229+
waiting for or cancelling the human operation.
230+
231+
Private discovery is separate from public initialization and its onboarding.
232+
Agent reads never start or await installation or missing-manager questions, even
233+
when a concurrent public initialization is waiting for input. Ordinary startup,
234+
public API, and human workflows retain their prompts; independently triggered
235+
startup UI can still appear while a tool runs. There is no global "suppress UI"
236+
switch. Exact nested-project persistence is also opt-in to the private setter;
237+
ordinary manager-setting updates retain their existing defaults.
238+
Human venv initialization retries discovery and base preparation after a failure,
239+
including failures after discovery has completed. A partial initialization is not
240+
reused as a successful cache on the next public lookup; private discovery still
241+
does not wait for human onboarding.
242+
243+
Keep tests for both internal and human routes when changing these helpers. The
244+
focused suites are in `src/test/internal`, with shared creation/package and
245+
inline-script regressions under `src/test/managers`.
246+
101247
## Questions or Issues?
102248

103249
- **Questions**: Start a [discussion](https://github.com/microsoft/vscode-python/discussions/categories/q-a)

‎README.md‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -130,7 +130,7 @@ All commands can be accessed via the Command Palette (`ctrl/cmd + Shift + P`):
130130
| pythonProjects | `[]` | A list of Python workspaces, specified by the path, in which you can set particular environment and package managers. You can set information for a workspace as `[{"path": "/path/to/workspace", "envManager": "ms-python.python:venv", "packageManager": "ms-python.python:pip"]}`. |
131131
| terminal.showActivateButton | `false` | (experimental) Show a button in the terminal to activate/deactivate the current environment for the terminal. This button is only shown if the active terminal is associated with a project that has an activatable environment. |
132132
| terminal.autoActivationType | `"command"` | Specifies how the extension can activate an environment in a terminal. Accepted values: `command` (execute activation command in terminal), `shellStartup` (`terminal.integrated.shellIntegration.enabled` successfully enabled or we may modify shell startup scripts ), `off` (no auto-activation). Shell startup is only supported for: zsh, fish, pwsh, bash, cmd. **Takes precedence over** `python.terminal.activateEnvironment`. Restart terminals after changing this setting. To revert shell startup changes, run `Python Envs: Revert Shell Startup Script Changes`. |
133-
| alwaysUseUv | `true` | When `true`, [uv](https://github.com/astral-sh/uv) will be used to manage all virtual environments if available. When `false`, uv will only manage virtual environments explicitly created by uv. The extension prefers `uv` on its PATH; if unavailable in a trusted workspace, it also checks that workspace's `.pyprojectx/main/uv` (`uv.exe` on Windows) for environments inside that workspace. External environments still require uv on PATH. This does not alter the integrated terminal's PATH. |
133+
| alwaysUseUv | `true` | When `true`, [uv](https://github.com/astral-sh/uv) will be used to manage all virtual environments if available. When `false`, uv will only manage virtual environments explicitly created by uv. The extension uses `uv` on its PATH, or the executable it installed during the current session. If unavailable in a trusted workspace, it also checks that workspace's `.pyprojectx/main/uv` (`uv.exe` on Windows) for environments inside that workspace. External environments require uv on PATH or a current-session installation. This does not alter the integrated terminal's PATH. |
134134
| globalSearchPaths | `[]` | Global search paths for Python environments. Array of absolute directory paths to search for environments at the user level. This setting is merged with the legacy `python.venvPath` and `python.venvFolders` settings. |
135135
| workspaceSearchPaths | `[]` | Workspace search paths for Python environments. Can be absolute paths or relative directory paths searched within the workspace. |
136136

‎package-lock.json‎

Lines changed: 2 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

‎package.json‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
"name": "vscode-python-envs",
33
"displayName": "Python Environments",
44
"description": "Provides a unified python environment experience",
5-
"version": "1.39.0",
5+
"version": "1.41.0",
66
"publisher": "ms-python",
77
"engines": {
88
"vscode": "^1.110.0-20260204"

‎src/common/inlineScript/metadata.ts‎

Lines changed: 19 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33

44
import * as tomljs from '@iarna/toml';
55
import * as fs from 'fs/promises';
6-
import { Uri } from 'vscode';
6+
import { l10n, Uri } from 'vscode';
77
import { traceVerbose, traceWarn } from '../logging';
88
import { PythonVersion } from '../pythonVersion';
99
import { PythonVersionSpecifier } from '../pythonVersionSpecifier';
@@ -654,8 +654,13 @@ export function sliceHeaderBytes(text: string): string {
654654
* - any I/O error (logged at `traceVerbose`);
655655
* - any of the malformed-metadata cases handled by
656656
* `readInlineScriptMetadata`.
657+
* @param uri The local script to read.
658+
* @param strict Throw on I/O errors or invalid metadata instead of treating them as absent.
657659
*/
658-
export async function readInlineScriptMetadataFromFile(uri: Uri): Promise<InlineScriptMetadata | undefined> {
660+
export async function readInlineScriptMetadataFromFile(
661+
uri: Uri,
662+
strict = false,
663+
): Promise<InlineScriptMetadata | undefined> {
659664
if (uri.scheme !== 'file') {
660665
traceVerbose(`inline script metadata: skipping non-file URI scheme '${uri.scheme}'`);
661666
return undefined;
@@ -671,11 +676,22 @@ export async function readInlineScriptMetadataFromFile(uri: Uri): Promise<Inline
671676
await handle.close();
672677
}
673678
} catch (err) {
679+
if (strict) {
680+
throw err;
681+
}
674682
traceVerbose(`inline script metadata: failed to read ${uri.fsPath}:`, err);
675683
return undefined;
676684
}
677685

678-
return readInlineScriptMetadata(text, uri.fsPath);
686+
const result = parseInlineScriptMetadata(text, uri.fsPath);
687+
if (
688+
strict &&
689+
(result.kind === 'invalid' ||
690+
(result.kind === 'parsed' && result.problems.some((problem) => problem.severity === 'error')))
691+
) {
692+
throw new Error(l10n.t('Fix the PEP 723 metadata in {0} before configuring its environment.', uri.fsPath));
693+
}
694+
return result.kind === 'parsed' ? result.metadata : undefined;
679695
}
680696

681697
/**

‎src/common/lockfile.apis.ts‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import * as path from 'path';
88
export interface AcquireFileLockOptions {
99
readonly timeoutMs: number;
1010
readonly retryIntervalMs: number;
11+
readonly checkCancellation?: () => void;
1112
}
1213

1314
export interface AcquiredFileLock {
@@ -52,6 +53,7 @@ export async function acquireFileLock(filePath: string, options: AcquireFileLock
5253
const deadline = Date.now() + options.timeoutMs;
5354

5455
while (true) {
56+
options.checkCancellation?.();
5557
try {
5658
await fsapi.mkdir(lockPath);
5759
try {

0 commit comments

Comments
 (0)