Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
26 changes: 13 additions & 13 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -198,7 +198,7 @@
"title": "%python-envs.setEnvManager.title%",
"category": "Python",
"icon": "$(gear)",
"when": "config.python.useEnvironmentsExtension != false"
"enablement": "pythonEnvs.enabled"
},
{
"command": "python-envs.setPkgManager",
Expand Down Expand Up @@ -403,7 +403,7 @@
"title": "%python-envs.runPetInTerminal.title%",
"category": "Python",
"icon": "$(terminal)",
"when": "config.python.useEnvironmentsExtension != false"
"enablement": "pythonEnvs.enabled"
}
],
"menus": {
Expand Down Expand Up @@ -442,19 +442,19 @@
},
{
"command": "python-envs.setEnvManager",
"when": "config.python.useEnvironmentsExtension != false"
"when": "pythonEnvs.enabled"
},
{
"command": "python-envs.packages",
"when": "config.python.useEnvironmentsExtension != false"
"when": "pythonEnvs.enabled"
},
{
"command": "python-envs.set",
"when": "config.python.useEnvironmentsExtension != false"
"when": "pythonEnvs.enabled"
},
{
"command": "python-envs.setPkgManager",
"when": "config.python.useEnvironmentsExtension != false"
"when": "pythonEnvs.enabled"
},
{
"command": "python-envs.removePythonProject",
Expand All @@ -470,7 +470,7 @@
},
{
"command": "python-envs.runAsTask",
"when": "config.python.useEnvironmentsExtension != false"
"when": "pythonEnvs.enabled"
},
{
"command": "python-envs.clearScriptEnvCache",
Expand Down Expand Up @@ -522,15 +522,15 @@
},
{
"command": "python-envs.createNewProjectFromTemplate",
"when": "config.python.useEnvironmentsExtension != false"
"when": "pythonEnvs.enabled"
},
{
"command": "python-envs.terminal.revertStartupScriptChanges",
"when": "config.python.useEnvironmentsExtension != false"
"when": "pythonEnvs.enabled"
},
{
"command": "python-envs.reportIssue",
"when": "config.python.useEnvironmentsExtension != false"
"when": "pythonEnvs.enabled"
}
],
"view/item/context": [
Expand Down Expand Up @@ -734,7 +734,7 @@
"id": "python",
"title": "Python",
"icon": "files/logo.svg",
"when": "config.python.useEnvironmentsExtension != false && python-envs.workspaceHasPython"
"when": "pythonEnvs.enabled && python-envs.workspaceHasPython"
}
]
},
Expand All @@ -745,14 +745,14 @@
"name": "Python Projects",
"icon": "files/logo.svg",
"contextualTitle": "Python Projects",
"when": "config.python.useEnvironmentsExtension != false && python-envs.workspaceHasPython"
"when": "pythonEnvs.enabled && python-envs.workspaceHasPython"
},
{
"id": "env-managers",
"name": "Environment Managers",
"icon": "files/logo.svg",
"contextualTitle": "Environment Managers",
"when": "config.python.useEnvironmentsExtension != false && python-envs.workspaceHasPython"
"when": "pythonEnvs.enabled && python-envs.workspaceHasPython"
}
]
},
Expand Down
1 change: 1 addition & 0 deletions src/common/constants.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import * as path from 'path';

export const ENVS_EXTENSION_ID = 'ms-python.vscode-python-envs';
export const PYTHON_ENVIRONMENTS_ENABLED_CONTEXT_KEY = 'pythonEnvs.enabled';
export const PYTHON_EXTENSION_ID = 'ms-python.python';
export const PYLANCE_EXTENSION_ID = 'ms-python.vscode-pylance';

Expand Down
4 changes: 4 additions & 0 deletions src/extension.ts
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,7 @@ import { registerCompletionProvider } from './features/settings/settingCompletio
import {
isEnvironmentsExtensionEnabled,
migrateGlobalDefaultEnvManagerSetting,
setEnvironmentsExtensionEnabledContext,
} from './features/settings/settingHelpers';
import { setActivateMenuButtonContext } from './features/terminal/activateMenuButton';
import { normalizeShellPath } from './features/terminal/shells/common/shellUtils';
Expand Down Expand Up @@ -145,6 +146,8 @@ export async function activate(context: ExtensionContext): Promise<PythonEnviron
const outputChannel: LogOutputChannel = createLogOutputChannel('Python Environments');
context.subscriptions.push(outputChannel, registerLogger(outputChannel));

await setEnvironmentsExtensionEnabledContext(true);

// Track whether this is a Python workspace (drives the activity bar icon). Registered after the
// `useEnvironmentsExtension` guard and the logger so failures are reported.
registerWorkspacePythonContext(context.subscriptions);
Expand Down Expand Up @@ -850,6 +853,7 @@ export async function disposeAll(disposables: IDisposable[]): Promise<void> {
}

export async function deactivate(context: ExtensionContext) {
await setEnvironmentsExtensionEnabledContext(false);
await disposeAll(context.subscriptions);
if (context.subscriptions?.length) {
context.subscriptions.length = 0; // Clear subscriptions to prevent memory leaks
Expand Down
15 changes: 15 additions & 0 deletions src/features/settings/settingHelpers.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,12 @@
import * as path from 'path';
import { ConfigurationScope, ConfigurationTarget, Uri, WorkspaceConfiguration, WorkspaceFolder } from 'vscode';
import { PythonProject } from '../../api';
import { executeCommand } from '../../common/command.api';
import {
DEFAULT_ENV_MANAGER_ID,
DEFAULT_PACKAGE_MANAGER_ID,
INLINE_SCRIPT_MANAGER_ID,
PYTHON_ENVIRONMENTS_ENABLED_CONTEXT_KEY,
SYSTEM_MANAGER_ID,
} from '../../common/constants';
import { traceError, traceInfo, traceVerbose, traceWarn } from '../../common/logging';
Expand Down Expand Up @@ -1012,6 +1014,19 @@ export function isEnvironmentsExtensionEnabled(): boolean {
);
}

/**
* Publishes whether the Python Environments extension is enabled for contribution visibility.
*
* @param enabled - The activation decision to expose through the VS Code context key.
*/
export async function setEnvironmentsExtensionEnabledContext(enabled: boolean): Promise<void> {
try {
await executeCommand('setContext', PYTHON_ENVIRONMENTS_ENABLED_CONTEXT_KEY, enabled);
} catch (err) {
traceError(`Failed to set the Python Environments enabled context to ${enabled}`, err);
}
}

/**
* Gets user-configured setting for user-scoped settings.
* Only checks globalValue (ignores defaultValue).
Expand Down
82 changes: 82 additions & 0 deletions src/test/contributionVisibility.unit.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License.

import * as assert from 'assert';
import * as fs from 'fs';
import * as path from 'path';
import { PYTHON_ENVIRONMENTS_ENABLED_CONTEXT_KEY } from '../common/constants';

type ConditionalContribution = {
command?: string;
enablement?: string;
id?: string;
when?: string;
};

type PackageJson = {
contributes: {
commands: ConditionalContribution[];
menus: {
commandPalette: ConditionalContribution[];
};
viewsContainers: {
activitybar: ConditionalContribution[];
};
views: {
python: ConditionalContribution[];
};
};
};

const packageJson = JSON.parse(
fs.readFileSync(path.resolve(__dirname, '..', '..', 'package.json'), 'utf8'),
) as PackageJson;

suite('Contribution visibility', () => {
test('gates environment commands on the activation-owned context key', () => {
const commandIds = ['python-envs.setEnvManager', 'python-envs.runPetInTerminal'];
const commandPaletteIds = [
'python-envs.setEnvManager',
'python-envs.packages',
'python-envs.set',
'python-envs.setPkgManager',
'python-envs.runAsTask',
'python-envs.createNewProjectFromTemplate',
'python-envs.terminal.revertStartupScriptChanges',
'python-envs.reportIssue',
];

for (const commandId of commandIds) {
const contribution = packageJson.contributes.commands.find(({ command }) => command === commandId);
assert.ok(contribution, `Missing command contribution for ${commandId}`);
assert.strictEqual(contribution.enablement, PYTHON_ENVIRONMENTS_ENABLED_CONTEXT_KEY);
}

for (const commandId of commandPaletteIds) {
const contribution = packageJson.contributes.menus.commandPalette.find(
({ command }) => command === commandId,
);
assert.ok(contribution, `Missing Command Palette contribution for ${commandId}`);
assert.strictEqual(contribution.when, PYTHON_ENVIRONMENTS_ENABLED_CONTEXT_KEY);
}

assert.ok(
!JSON.stringify(packageJson.contributes).includes('config.python.useEnvironmentsExtension'),
'Contribution visibility must not evaluate the raw setting independently of activation',
);
});

test('gates the Python views on activation and Python workspace contexts', () => {
const when = `${PYTHON_ENVIRONMENTS_ENABLED_CONTEXT_KEY} && python-envs.workspaceHasPython`;
const activityBar = packageJson.contributes.viewsContainers.activitybar.find(({ id }) => id === 'python');
const pythonProjects = packageJson.contributes.views.python.find(({ id }) => id === 'python-projects');
const environmentManagers = packageJson.contributes.views.python.find(({ id }) => id === 'env-managers');

assert.ok(activityBar, 'Missing Python activity bar contribution');
assert.ok(pythonProjects, 'Missing Python Projects view contribution');
assert.ok(environmentManagers, 'Missing Environment Managers view contribution');
assert.strictEqual(activityBar.when, when);
assert.strictEqual(pythonProjects.when, when);
assert.strictEqual(environmentManagers.when, when);
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,14 @@
import * as assert from 'assert';
import * as sinon from 'sinon';
import { Uri, WorkspaceConfiguration, WorkspaceFolder } from 'vscode';
import * as commandApis from '../../../common/command.api';
import { PYTHON_ENVIRONMENTS_ENABLED_CONTEXT_KEY } from '../../../common/constants';
import * as logging from '../../../common/logging';
import * as workspaceApis from '../../../common/workspace.apis';
import { isEnvironmentsExtensionEnabled } from '../../../features/settings/settingHelpers';
import {
isEnvironmentsExtensionEnabled,
setEnvironmentsExtensionEnabledContext,
} from '../../../features/settings/settingHelpers';

type SettingInspection = {
defaultValue?: boolean;
Expand Down Expand Up @@ -33,6 +39,16 @@ suite('isEnvironmentsExtensionEnabled', () => {
sinon.restore();
});

test('enables when the setting is not registered', () => {
sinon.stub(workspaceApis, 'getWorkspaceFolders').returns(undefined);
sinon
.stub(workspaceApis, 'getConfiguration')
.withArgs('python', undefined)
.returns(createConfiguration(undefined));

assert.strictEqual(isEnvironmentsExtensionEnabled(), true);
});

test('ignores a contributed default when no value is explicitly configured', () => {
sinon.stub(workspaceApis, 'getWorkspaceFolders').returns(undefined);
sinon
Expand Down Expand Up @@ -100,4 +116,35 @@ suite('isEnvironmentsExtensionEnabled', () => {

assert.strictEqual(isEnvironmentsExtensionEnabled(), true);
});

test('publishes enabled and disabled decisions to the contribution context', async () => {
const executeCommand = sinon.stub(commandApis, 'executeCommand').resolves(undefined);

await setEnvironmentsExtensionEnabledContext(true);
await setEnvironmentsExtensionEnabledContext(false);

assert.deepStrictEqual(executeCommand.firstCall.args, [
'setContext',
PYTHON_ENVIRONMENTS_ENABLED_CONTEXT_KEY,
true,
]);
assert.deepStrictEqual(executeCommand.secondCall.args, [
'setContext',
PYTHON_ENVIRONMENTS_ENABLED_CONTEXT_KEY,
false,
]);
});

test('logs context publication failures without rejecting activation', async () => {
const error = new Error('setContext failed');
sinon.stub(commandApis, 'executeCommand').rejects(error);
const traceError = sinon.stub(logging, 'traceError');

await setEnvironmentsExtensionEnabledContext(true);

assert.deepStrictEqual(traceError.firstCall.args, [
'Failed to set the Python Environments enabled context to true',
error,
]);
});
});
Loading