Skip to content

Commit ad7f69b

Browse files
Honor effective environment extension enablement consistently
Co-authored-by: eleanorjboyd <26030610+eleanorjboyd@users.noreply.github.com>
1 parent 93fd328 commit ad7f69b

4 files changed

Lines changed: 145 additions & 70 deletions

File tree

‎README.md‎

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,9 @@ These extensions are optional dependencies, meaning the Python extension will re
2222
You may now see that the **Python Environments Extension** is installed for you, but it may or may not be "enabled" in your VS Code experience. Enablement is controlled by the setting `"python.useEnvironmentsExtension": true` (or `false`).
2323

2424
- If you set this setting to `true`, you will manually opt in to using the Python Environments Extension for environment management.
25-
- If you do not have this setting specified, you may be randomly assigned to have it turned on as we roll it out until it becomes the default experience for all users.
25+
- If you set this setting to `false`, you will opt out. Explicit settings take precedence over experiment assignments, with folder settings overriding workspace settings and workspace settings overriding user settings.
26+
- If you do not have this setting specified, an experiment assignment can enable or disable it. Without an assignment (including when experiments or telemetry are disabled), it defaults to enabled.
27+
- In multi-root workspaces, an effectively disabled folder disables Python Environments integration for the entire window. Reload the window after changing this setting.
2628

2729
The Python Environments Extension is still under active development and experimentation. Its goal is to provide a dedicated view and improved workflows for creating, deleting, and switching between Python environments, as well as managing packages. If you have feedback, please let us know via [issues](https://github.com/microsoft/vscode-python/issues).
2830

‎package.json‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -447,7 +447,7 @@
447447
"type": "string"
448448
},
449449
"python.useEnvironmentsExtension": {
450-
"default": false,
450+
"default": true,
451451
"description": "%python.useEnvironmentsExtension.description%",
452452
"scope": "machine-overridable",
453453
"type": "boolean",

‎src/client/envExt/api.internal.ts‎

Lines changed: 11 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -24,45 +24,26 @@ export function isEnvExtensionInstalled(): boolean {
2424
}
2525

2626
/**
27-
* Returns true if the Python Environments extension is installed and not explicitly
28-
* disabled by the user. Mirrors the envs extension's own activation logic: it
29-
* deactivates only when `python.useEnvironmentsExtension` is explicitly set to false
30-
* at the global, workspace, or workspace-folder level.
27+
* Uses the same window-wide decision for terminal activation and UI ownership as
28+
* environment discovery and API routing.
3129
*/
3230
export function shouldEnvExtHandleActivation(): boolean {
33-
if (!isEnvExtensionInstalled()) {
34-
return false;
35-
}
36-
const config = getConfiguration('python');
37-
const inspection = config.inspect<boolean>('useEnvironmentsExtension');
38-
if (inspection?.globalValue === false || inspection?.workspaceValue === false) {
39-
return false;
40-
}
41-
// The envs extension also checks folder-scoped settings in multi-root workspaces.
42-
// Any single folder with the setting set to false causes the envs extension to
43-
// deactivate entirely (window-wide), so we must mirror that here.
44-
const workspaceFolders = getWorkspaceFolders();
45-
if (workspaceFolders) {
46-
for (const folder of workspaceFolders) {
47-
const folderConfig = getConfiguration('python', folder.uri);
48-
const folderInspection = folderConfig.inspect<boolean>('useEnvironmentsExtension');
49-
if (folderInspection?.workspaceFolderValue === false) {
50-
return false;
51-
}
52-
}
53-
}
54-
return true;
31+
return useEnvExtension();
5532
}
5633

5734
let _useExt: boolean | undefined;
5835
export function useEnvExtension(): boolean {
5936
if (_useExt !== undefined) {
6037
return _useExt;
6138
}
62-
const config = getConfiguration('python');
63-
const inExpSetting = config?.get<boolean>('useEnvironmentsExtension', false) ?? false;
64-
// If extension is installed and in experiment, then use it.
65-
_useExt = !!getExtension(ENVS_EXTENSION_ID) && inExpSetting;
39+
// Configuration.get resolves folder > workspace > user > experiment/manifest default.
40+
// Any effectively disabled folder disables environment management for the window.
41+
const isEnabled = (resource?: Uri) =>
42+
getConfiguration('python', resource).get<boolean>('useEnvironmentsExtension') ?? true;
43+
const folders = getWorkspaceFolders();
44+
// Keep all consumers on the same decision until reload, as required by the setting.
45+
_useExt =
46+
isEnvExtensionInstalled() && (folders?.length ? folders.every((folder) => isEnabled(folder.uri)) : isEnabled());
6647
return _useExt;
6748
}
6849

‎src/test/common/terminals/activator/index.unit.test.ts‎

Lines changed: 130 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
'use strict';
55

66
import { assert } from 'chai';
7+
import rewiremock from 'rewiremock';
78
import * as sinon from 'sinon';
89
import * as TypeMoq from 'typemoq';
910
import { Terminal, Uri } from 'vscode';
@@ -138,15 +139,46 @@ suite('Terminal Activator', () => {
138139
});
139140

140141
suite('shouldEnvExtHandleActivation', () => {
142+
let api: typeof extapi;
141143
let getExtensionStub: sinon.SinonStub;
142144
let getConfigurationStub: sinon.SinonStub;
143145
let getWorkspaceFoldersStub: sinon.SinonStub;
144146

147+
type SettingValues = {
148+
defaultValue?: boolean;
149+
globalValue?: boolean;
150+
workspaceValue?: boolean;
151+
workspaceFolderValue?: boolean;
152+
};
153+
154+
function configuration(values: SettingValues = {}) {
155+
return {
156+
get: sinon
157+
.stub()
158+
.withArgs('useEnvironmentsExtension')
159+
.returns(
160+
values.workspaceFolderValue ?? values.workspaceValue ?? values.globalValue ?? values.defaultValue,
161+
),
162+
inspect: sinon.stub().returns(values),
163+
};
164+
}
165+
166+
function assertOwnership(expected: boolean) {
167+
assert.strictEqual(api.shouldEnvExtHandleActivation(), expected);
168+
assert.strictEqual(api.useEnvExtension(), expected);
169+
}
170+
145171
setup(() => {
146172
getExtensionStub = sinon.stub(extensionsApi, 'getExtension');
173+
getExtensionStub.returns({ id: extapi.ENVS_EXTENSION_ID });
147174
getConfigurationStub = sinon.stub(workspaceApis, 'getConfiguration');
148-
getWorkspaceFoldersStub = sinon.stub(workspaceApis, 'getWorkspaceFolders');
149-
getWorkspaceFoldersStub.returns(undefined);
175+
getConfigurationStub.returns(configuration());
176+
getWorkspaceFoldersStub = sinon.stub(workspaceApis, 'getWorkspaceFolders').returns(undefined);
177+
// A fresh module represents a new window, including its cached ownership decision.
178+
api = rewiremock.proxy(() => require('../../../../client/envExt/api.internal'), {
179+
[require.resolve('../../../../client/common/vscodeApis/extensionsApi')]: extensionsApi,
180+
[require.resolve('../../../../client/common/vscodeApis/workspaceApis')]: workspaceApis,
181+
});
150182
});
151183

152184
teardown(() => {
@@ -155,55 +187,115 @@ suite('shouldEnvExtHandleActivation', () => {
155187

156188
test('Returns false when envs extension is not installed', () => {
157189
getExtensionStub.returns(undefined);
158-
assert.strictEqual(extapi.shouldEnvExtHandleActivation(), false);
190+
assertOwnership(false);
159191
});
160192

161-
test('Returns true when envs extension is installed and setting is not explicitly set', () => {
162-
getExtensionStub.returns({ id: extapi.ENVS_EXTENSION_ID });
163-
getConfigurationStub.returns({
164-
inspect: () => ({ globalValue: undefined, workspaceValue: undefined }),
165-
});
166-
assert.strictEqual(extapi.shouldEnvExtHandleActivation(), true);
193+
test('Registers an enabled fallback that experiments can override', () => {
194+
const manifest = require('../../../../../package.json');
195+
const setting = manifest.contributes.configuration.properties['python.useEnvironmentsExtension'];
196+
assert.strictEqual(setting.default, true);
197+
assert.include(setting.tags, 'onExP');
167198
});
168199

169-
test('Returns false when envs extension is installed but globalValue is false', () => {
170-
getExtensionStub.returns({ id: extapi.ENVS_EXTENSION_ID });
171-
getConfigurationStub.returns({
172-
inspect: () => ({ globalValue: false, workspaceValue: undefined }),
200+
[undefined, true, false].forEach((defaultValue) => {
201+
[undefined, true, false].forEach((globalValue) => {
202+
[undefined, true, false].forEach((workspaceValue) => {
203+
test(`Uses effective value: default=${defaultValue}, user=${globalValue}, workspace=${workspaceValue}`, () => {
204+
const config = configuration({ defaultValue, globalValue, workspaceValue });
205+
getConfigurationStub.returns(config);
206+
207+
assertOwnership(workspaceValue ?? globalValue ?? defaultValue ?? true);
208+
sinon.assert.calledWithExactly(config.get, 'useEnvironmentsExtension');
209+
sinon.assert.notCalled(config.inspect);
210+
});
211+
});
173212
});
174-
assert.strictEqual(extapi.shouldEnvExtHandleActivation(), false);
175213
});
176214

177-
test('Returns false when envs extension is installed but workspaceValue is false', () => {
178-
getExtensionStub.returns({ id: extapi.ENVS_EXTENSION_ID });
179-
getConfigurationStub.returns({
180-
inspect: () => ({ globalValue: undefined, workspaceValue: false }),
215+
[undefined, []].forEach((folders) => {
216+
test(`Uses window configuration when workspace folders are ${JSON.stringify(folders)}`, () => {
217+
getWorkspaceFoldersStub.returns(folders);
218+
getConfigurationStub.returns(configuration({ defaultValue: false }));
219+
220+
assertOwnership(false);
221+
sinon.assert.calledWithExactly(getConfigurationStub, 'python', undefined);
181222
});
182-
assert.strictEqual(extapi.shouldEnvExtHandleActivation(), false);
183223
});
184224

185-
test('Returns true when envs extension is installed and setting is explicitly true', () => {
186-
getExtensionStub.returns({ id: extapi.ENVS_EXTENSION_ID });
187-
getConfigurationStub.returns({
188-
inspect: () => ({ globalValue: true, workspaceValue: undefined }),
225+
const folderCases: {
226+
name: string;
227+
inherited: SettingValues;
228+
folders: (boolean | undefined)[];
229+
expected: boolean;
230+
}[] = [
231+
{
232+
name: 'a single folder true overrides workspace and user false',
233+
inherited: { globalValue: false, workspaceValue: false },
234+
folders: [true],
235+
expected: true,
236+
},
237+
{
238+
name: 'all folders true override workspace false',
239+
inherited: { workspaceValue: false },
240+
folders: [true, true],
241+
expected: true,
242+
},
243+
{
244+
name: 'one folder false disables the whole window',
245+
inherited: { globalValue: true },
246+
folders: [true, false],
247+
expected: false,
248+
},
249+
{
250+
name: 'an unset folder inherits workspace false',
251+
inherited: { workspaceValue: false },
252+
folders: [true, undefined],
253+
expected: false,
254+
},
255+
{
256+
name: 'workspace true overrides user and experiment false in every folder',
257+
inherited: { defaultValue: false, globalValue: false, workspaceValue: true },
258+
folders: [undefined, undefined],
259+
expected: true,
260+
},
261+
{
262+
name: 'an unset folder inherits experiment false',
263+
inherited: { defaultValue: false },
264+
folders: [true, undefined],
265+
expected: false,
266+
},
267+
{
268+
name: 'unset folders without a treatment default to enabled',
269+
inherited: {},
270+
folders: [undefined, undefined],
271+
expected: true,
272+
},
273+
];
274+
folderCases.forEach(({ name, inherited, folders, expected }) => {
275+
test(`Uses folder precedence: ${name}`, () => {
276+
const workspaceFolders = folders.map((workspaceFolderValue, index) => {
277+
const uri = Uri.parse(`file:///workspace/folder${index}`);
278+
getConfigurationStub
279+
.withArgs('python', uri)
280+
.returns(configuration({ ...inherited, workspaceFolderValue }));
281+
return { uri, name: `folder${index}`, index };
282+
});
283+
getWorkspaceFoldersStub.returns(workspaceFolders);
284+
getConfigurationStub.withArgs('python', undefined).returns(configuration(inherited));
285+
286+
assertOwnership(expected);
287+
assert.isTrue(getConfigurationStub.firstCall.args[1] instanceof Uri);
189288
});
190-
assert.strictEqual(extapi.shouldEnvExtHandleActivation(), true);
191289
});
192290

193-
test('Returns false when a workspace folder has workspaceFolderValue set to false', () => {
194-
getExtensionStub.returns({ id: extapi.ENVS_EXTENSION_ID });
195-
const folderUri = Uri.parse('file:///workspace/folder1');
196-
getWorkspaceFoldersStub.returns([{ uri: folderUri, name: 'folder1', index: 0 }]);
197-
getConfigurationStub.callsFake((_section: string, scope?: Uri) => {
198-
if (scope) {
199-
return {
200-
inspect: () => ({ workspaceFolderValue: false }),
201-
};
202-
}
203-
return {
204-
inspect: () => ({ globalValue: undefined, workspaceValue: undefined }),
205-
};
291+
[false, true].forEach((enabled) => {
292+
test(`Keeps ownership aligned until reload after an initial ${enabled} decision`, () => {
293+
getConfigurationStub.returns(configuration({ defaultValue: enabled }));
294+
assert.strictEqual(api.useEnvExtension(), enabled);
295+
296+
getConfigurationStub.returns(configuration({ globalValue: !enabled }));
297+
298+
assertOwnership(enabled);
206299
});
207-
assert.strictEqual(extapi.shouldEnvExtHandleActivation(), false);
208300
});
209301
});

0 commit comments

Comments
 (0)