Skip to content

Commit 36ab28e

Browse files
committed
feat: package manager permissions when using API
1 parent 771bd3c commit 36ab28e

13 files changed

+391
-37
lines changed

package.json

+20
Original file line numberDiff line numberDiff line change
@@ -230,6 +230,18 @@
230230
"title": "%python-envs.copyProjectPath.title%",
231231
"category": "Python Envs",
232232
"icon": "$(copy)"
233+
},
234+
{
235+
"command": "python-envs.permissions",
236+
"title": "%python-envs.permissions.title%",
237+
"category": "Python Envs",
238+
"icon": "$(shield)"
239+
},
240+
{
241+
"command": "python-envs.resetPermissions",
242+
"title": "%python-envs.resetPermissions.title%",
243+
"category": "Python Envs",
244+
"icon": "$(sync)"
233245
}
234246
],
235247
"menus": {
@@ -409,9 +421,17 @@
409421
},
410422
{
411423
"command": "python-envs.refreshAllManagers",
424+
"when": "view == env-managers"
425+
},
426+
{
427+
"command": "python-envs.permissions",
412428
"group": "navigation",
413429
"when": "view == env-managers"
414430
},
431+
{
432+
"command": "python-envs.resetPermissions",
433+
"when": "view == env-managers"
434+
},
415435
{
416436
"command": "python-envs.terminal.activate",
417437
"group": "navigation",

package.nls.json

+3-1
Original file line numberDiff line numberDiff line change
@@ -28,5 +28,7 @@
2828
"python-envs.runAsTask.title": "Run as Task",
2929
"python-envs.terminal.activate.title": "Activate Environment in Current Terminal",
3030
"python-envs.terminal.deactivate.title": "Deactivate Environment in Current Terminal",
31-
"python-envs.uninstallPackage.title": "Uninstall Package"
31+
"python-envs.uninstallPackage.title": "Uninstall Package",
32+
"python-envs.permissions.title": "Package Manager Permissions",
33+
"python-envs.resetPermissions.title": "Reset Package Manager Permissions"
3234
}

src/api.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -935,7 +935,7 @@ export interface PythonPackageManagementApi {
935935
* @param environment The Python Environment from which packages are to be uninstalled.
936936
* @param packages The packages to uninstall.
937937
*/
938-
uninstallPackages(environment: PythonEnvironment, packages: PackageInfo[] | string[]): Promise<void>;
938+
uninstallPackages(environment: PythonEnvironment, packages: string[]): Promise<void>;
939939
}
940940

941941
export interface PythonPackageManagerApi

src/common/command.api.ts

+5
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,11 @@
11
/* eslint-disable @typescript-eslint/no-explicit-any */
22
import { commands } from 'vscode';
3+
import { Disposable } from 'vscode-jsonrpc';
34

45
export function executeCommand<T = unknown>(command: string, ...rest: any[]): Thenable<T> {
56
return commands.executeCommand(command, ...rest);
67
}
8+
9+
export function registerCommand(command: string, callback: (...args: any[]) => any, thisArg?: any): Disposable {
10+
return commands.registerCommand(command, callback, thisArg);
11+
}

src/common/localize.ts

+7
Original file line numberDiff line numberDiff line change
@@ -144,3 +144,10 @@ export namespace EnvViewStrings {
144144
export const selectedGlobalTooltip = l10n.t('This environment is selected for non-workspace files');
145145
export const selectedWorkspaceTooltip = l10n.t('This environment is selected for workspace files');
146146
}
147+
148+
export namespace PermissionsCommon {
149+
export const allow = l10n.t('Allow');
150+
export const deny = l10n.t('Deny');
151+
export const ask = l10n.t('Ask');
152+
export const setPermissions = l10n.t('Set Permissions');
153+
}

src/common/utils/frameUtils.ts

+48-20
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,9 @@
1+
import * as path from 'path';
2+
import { Uri } from 'vscode';
13
import { ENVS_EXTENSION_ID, PYTHON_EXTENSION_ID } from '../constants';
24
import { parseStack } from '../errors/utils';
35
import { allExtensions, getExtension } from '../extension.apis';
4-
6+
import { normalizePath } from './pathUtils';
57
interface FrameData {
68
filePath: string;
79
functionName: string;
@@ -15,38 +17,64 @@ function getFrameData(): FrameData[] {
1517
}));
1618
}
1719

20+
function getPathFromFrame(frame: FrameData): string {
21+
if (frame.filePath && frame.filePath.startsWith('file://')) {
22+
return Uri.parse(frame.filePath).fsPath;
23+
}
24+
return frame.filePath;
25+
}
26+
1827
export function getCallingExtension(): string {
1928
const pythonExts = [ENVS_EXTENSION_ID, PYTHON_EXTENSION_ID];
20-
29+
const execPath = normalizePath(path.dirname(process.execPath));
2130
const extensions = allExtensions();
2231
const otherExts = extensions.filter((ext) => !pythonExts.includes(ext.id));
23-
const frames = getFrameData().filter((frame) => !!frame.filePath);
32+
const frames = getFrameData();
33+
const filePaths: string[] = [];
2434

2535
for (const frame of frames) {
26-
const filename = frame.filePath;
27-
if (filename) {
28-
const ext = otherExts.find((ext) => filename.includes(ext.id));
29-
if (ext) {
30-
return ext.id;
31-
}
36+
if (!frame || !frame.filePath) {
37+
continue;
38+
}
39+
const filePath = normalizePath(getPathFromFrame(frame));
40+
if (!filePath) {
41+
continue;
42+
}
43+
44+
if (filePath.startsWith(execPath) && filePath.endsWith('extensionhostprocess.js')) {
45+
continue;
46+
}
47+
48+
if (filePath.startsWith('node:')) {
49+
continue;
50+
}
51+
52+
filePaths.push(filePath);
53+
54+
const ext = otherExts.find((ext) => filePath.includes(ext.id));
55+
if (ext) {
56+
return ext.id;
3257
}
3358
}
3459

3560
// `ms-python.vscode-python-envs` extension in Development mode
36-
const candidates = frames.filter((frame) => otherExts.some((s) => frame.filePath.includes(s.extensionPath)));
37-
const envsExtPath = getExtension(ENVS_EXTENSION_ID)?.extensionPath;
38-
if (!envsExtPath) {
61+
const candidates = filePaths.filter((filePath) =>
62+
otherExts.some((s) => filePath.includes(normalizePath(s.extensionPath))),
63+
);
64+
const envExt = getExtension(ENVS_EXTENSION_ID);
65+
66+
if (!envExt) {
3967
throw new Error('Something went wrong with feature registration');
4068
}
41-
42-
if (candidates.length === 0 && frames.every((frame) => frame.filePath.startsWith(envsExtPath))) {
69+
const envsExtPath = normalizePath(envExt.extensionPath);
70+
if (candidates.length === 0 && filePaths.every((filePath) => filePath.startsWith(envsExtPath))) {
4371
return PYTHON_EXTENSION_ID;
44-
}
45-
46-
// 3rd party extension in Development mode
47-
const candidateExt = otherExts.find((ext) => candidates[0].filePath.includes(ext.extensionPath));
48-
if (candidateExt) {
49-
return candidateExt.id;
72+
} else if (candidates.length > 0) {
73+
// 3rd party extension in Development mode
74+
const candidateExt = otherExts.find((ext) => candidates[0].includes(ext.extensionPath));
75+
if (candidateExt) {
76+
return candidateExt.id;
77+
}
5078
}
5179

5280
throw new Error('Unable to determine calling extension id, registration failed');

src/common/utils/pathUtils.ts

+7-8
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,9 @@
1-
import * as path from 'path';
1+
import { isWindows } from '../../managers/common/utils';
22

3-
export function areSamePaths(a: string, b: string): boolean {
4-
return path.resolve(a) === path.resolve(b);
5-
}
6-
7-
export function isParentPath(parent: string, child: string): boolean {
8-
const relative = path.relative(path.resolve(parent), path.resolve(child));
9-
return !!relative && !relative.startsWith('..') && !path.isAbsolute(relative);
3+
export function normalizePath(path: string): string {
4+
const path1 = path.replace(/\\/g, '/');
5+
if (isWindows()) {
6+
return path1.toLowerCase();
7+
}
8+
return path1;
109
}

src/common/window.apis.ts

+18
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,8 @@ import {
77
InputBox,
88
InputBoxOptions,
99
LogOutputChannel,
10+
MessageItem,
11+
MessageOptions,
1012
OpenDialogOptions,
1113
OutputChannel,
1214
Progress,
@@ -288,6 +290,22 @@ export function showWarningMessage(message: string, ...items: string[]): Thenabl
288290
return window.showWarningMessage(message, ...items);
289291
}
290292

293+
export function showInformationMessage<T extends string>(message: string, ...items: T[]): Thenable<T | undefined>;
294+
export function showInformationMessage<T extends string>(
295+
message: string,
296+
options: MessageOptions,
297+
...items: T[]
298+
): Thenable<T | undefined>;
299+
export function showInformationMessage<T extends MessageItem>(message: string, ...items: T[]): Thenable<T | undefined>;
300+
export function showInformationMessage<T extends MessageItem>(
301+
message: string,
302+
options: MessageOptions,
303+
...items: T[]
304+
): Thenable<T | undefined>;
305+
export function showInformationMessage(message: string, ...items: any[]): Thenable<string | undefined> {
306+
return window.showInformationMessage(message, ...items);
307+
}
308+
291309
export function showInputBox(options?: InputBoxOptions, token?: CancellationToken): Thenable<string | undefined> {
292310
return window.showInputBox(options, token);
293311
}

src/extension.ts

+27-2
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ import {
2323
runInDedicatedTerminalCommand,
2424
handlePackageUninstall,
2525
copyPathToClipboard,
26+
getUninstallPackages,
2627
} from './features/envCommands';
2728
import { registerCondaFeatures } from './managers/conda/main';
2829
import { registerSystemPythonFeatures } from './managers/builtin/main';
@@ -57,6 +58,11 @@ import { registerTools } from './common/lm.apis';
5758
import { GetPackagesTool } from './features/copilotTools';
5859
import { TerminalActivationImpl } from './features/terminal/terminalActivationState';
5960
import { getEnvironmentForTerminal } from './features/terminal/utils';
61+
import {
62+
checkPackageManagementPermissions,
63+
handlePermissionsCommand,
64+
PackageManagerPermissionsImpl,
65+
} from './features/permissions/packageManagerPermissions';
6066

6167
export async function activate(context: ExtensionContext): Promise<PythonEnvironmentApi> {
6268
const start = new StopWatch();
@@ -93,7 +99,9 @@ export async function activate(context: ExtensionContext): Promise<PythonEnviron
9399
projectCreators.registerPythonProjectCreator(new AutoFindProjects(projectManager)),
94100
);
95101

96-
setPythonApi(envManagers, projectManager, projectCreators, terminalManager, envVarManager);
102+
const pkgPerm = new PackageManagerPermissionsImpl(context.secrets);
103+
104+
setPythonApi(envManagers, projectManager, projectCreators, terminalManager, envVarManager, pkgPerm);
97105

98106
const managerView = new EnvManagerView(envManagers);
99107
context.subscriptions.push(managerView);
@@ -109,6 +117,12 @@ export async function activate(context: ExtensionContext): Promise<PythonEnviron
109117
context.subscriptions.push(
110118
registerCompletionProvider(envManagers),
111119
registerTools('python_get_packages', new GetPackagesTool(api)),
120+
commands.registerCommand('python-envs.permissions', async () => {
121+
await handlePermissionsCommand(pkgPerm);
122+
}),
123+
commands.registerCommand('python-envs.resetPermissions', async () => {
124+
await pkgPerm.resetPermissions();
125+
}),
112126
commands.registerCommand('python-envs.viewLogs', () => outputChannel.show()),
113127
commands.registerCommand('python-envs.refreshManager', async (item) => {
114128
await refreshManagerCommand(item);
@@ -138,9 +152,20 @@ export async function activate(context: ExtensionContext): Promise<PythonEnviron
138152
envManagers,
139153
projectManager,
140154
);
141-
await handlePackagesCommand(packageManager, environment);
155+
156+
const result = await checkPackageManagementPermissions(pkgPerm, 'changes');
157+
if (!result) {
158+
return;
159+
}
160+
161+
await handlePackagesCommand(environment, packageManager);
142162
}),
143163
commands.registerCommand('python-envs.uninstallPackage', async (context: unknown) => {
164+
const result = await checkPackageManagementPermissions(pkgPerm, 'uninstall', getUninstallPackages(context));
165+
if (!result) {
166+
return;
167+
}
168+
144169
await handlePackageUninstall(context, envManagers);
145170
}),
146171
commands.registerCommand('python-envs.set', async (item) => {

src/features/envCommands.ts

+11-2
Original file line numberDiff line numberDiff line change
@@ -177,8 +177,8 @@ export async function removeEnvironmentCommand(context: unknown, managers: Envir
177177
}
178178

179179
export async function handlePackagesCommand(
180-
packageManager: InternalPackageManager,
181180
environment: PythonEnvironment,
181+
packageManager: InternalPackageManager,
182182
): Promise<void> {
183183
const action = await pickPackageOptions();
184184

@@ -190,12 +190,21 @@ export async function handlePackagesCommand(
190190
}
191191
} catch (ex) {
192192
if (ex === QuickInputButtons.Back) {
193-
return handlePackagesCommand(packageManager, environment);
193+
return handlePackagesCommand(environment, packageManager);
194194
}
195195
throw ex;
196196
}
197197
}
198198

199+
export function getUninstallPackages(context: unknown): string[] | undefined {
200+
if (context instanceof PackageTreeItem) {
201+
return [(context as PackageTreeItem).pkg.name];
202+
} else if (context instanceof ProjectPackage) {
203+
return [(context as ProjectPackage).pkg.name];
204+
}
205+
return undefined;
206+
}
207+
199208
export async function handlePackageUninstall(context: unknown, em: EnvironmentManagers) {
200209
if (context instanceof PackageTreeItem || context instanceof ProjectPackage) {
201210
const moduleName = context.pkg.name;

0 commit comments

Comments
 (0)