Skip to content
Closed
Show file tree
Hide file tree
Changes from 7 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
15 changes: 15 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,21 @@
"onExP",
"preview"
]
},
"python-envs.allowAutoPackageManagement": {
"type": "object",
"scope": "user",
"properties": {
"pub.name": {
"type": "string",
"enum": [
"alwaysAllow",
"alwaysAsk"
],
"default": "alwaysAsk",
"description": "Sets how installs by a given extension will be handled from the environment extension."
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
"description": "Sets how installs by a given extension will be handled from the environment extension."
"description": "Configures package installation permissions for a given extension."

}
}
}
}
},
Expand Down
79 changes: 79 additions & 0 deletions src/features/packageManagement.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
import { window } from 'vscode';
import { traceInfo } from '../common/logging';
import { getCallingExtension } from '../common/utils/frameUtils';
import { getConfiguration } from '../common/workspace.apis';
import { SettingsPackageTrust, promptForInstallPermissions, promptForAlwaysAsk } from './utils';

export enum InstallPermissionEnum {
AlwaysAllow = 'alwaysAllow',
AlwaysAsk = 'alwaysAsk',
InstallNoConfigure = 'installNoConfigure',
Cancel = 'cancel',
}

export enum SimpleResponseEnum {
YesInstall = 'yesInstall',
NoInstall = 'noInstall',
Cancel = 'cancel',
}
export async function packageManagementFlow(packages: string[]): Promise<void> {
// what does it mean to return, will we tell the calling extension about it?
//check to see if pkg was already installed?
const callingExtension = getCallingExtension();
traceInfo(`Python API: Installing packages for extension: '${callingExtension}'`);
const config = getConfiguration('python-envs');
let extPkgTrustConfig: SettingsPackageTrust | undefined =
config.get<SettingsPackageTrust>('allowAutoPackageManagement');
let callingExtensionTrustLevel;
let isConfigured = true;
if (extPkgTrustConfig === undefined) {
// TODO:s THIS DOESN'T WORK
// no package trust config, default to alwaysAsk
callingExtensionTrustLevel = InstallPermissionEnum.AlwaysAsk;
isConfigured = false;
} else {
// check for package trust settings
callingExtensionTrustLevel = extPkgTrustConfig[callingExtension];
if (callingExtensionTrustLevel === undefined) {
// no specific package trust settings, checking wildcard in config
callingExtensionTrustLevel = extPkgTrustConfig['*'];
if (callingExtensionTrustLevel === undefined) {
// no wildcard in config, default to alwaysAsk
callingExtensionTrustLevel = InstallPermissionEnum.AlwaysAsk;
isConfigured = false;
}
}
}
traceInfo(`package trust settings for '${callingExtension}' is ${callingExtensionTrustLevel}`);

if (!isConfigured) {
// calling extension has no config, user has no wildcard setup
// prompt user to "alwaysAsk" or "alwaysAllow"
const selectedOption = await promptForInstallPermissions(callingExtension, packages.join(', '));
if (selectedOption === InstallPermissionEnum.Cancel) {
// user cancelled the prompt, exit
window.showErrorMessage(`Installation of ${packages.join(', ')} was canceled by the user.`);
return Promise.reject('User cancelled the package installation.');
}
if (selectedOption !== InstallPermissionEnum.InstallNoConfigure) {
// meaning the user selected "alwaysAsk" or "alwaysAllow", update the config
const newExtTrustConfig = { ...extPkgTrustConfig, [callingExtension]: selectedOption };
config.update('allowAutoPackageManagement', newExtTrustConfig, true);
}
} else {
// user has already configured package trust settings for this extension
if (callingExtensionTrustLevel === InstallPermissionEnum.AlwaysAsk) {
traceInfo('Installation is pending user confirmation due to permission settings.');
// prompt user to allow or deny package installation
const simpleResponse = await promptForAlwaysAsk(callingExtension, packages.join(', '));
if (simpleResponse === SimpleResponseEnum.NoInstall || simpleResponse === SimpleResponseEnum.Cancel) {
// user cancelled the prompt, exit
window.showErrorMessage(`Installation of ${packages.join(', ')} was canceled by the user.`);
return Promise.reject('User cancelled the package installation.');
}
}
// if callingExtensionTrustLevel is 'alwaysAllow' just continue to install
}
// actually install the packages
return Promise.resolve();
}
11 changes: 9 additions & 2 deletions src/features/pythonApi.ts
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@ import { runAsTask } from './execution/runAsTask';
import { runInTerminal } from './terminal/runInTerminal';
import { runInBackground } from './execution/runInBackground';
import { EnvVarManager } from './execution/envVariableManager';

import { packageManagementFlow } from './packageManagement';
class PythonEnvironmentApiImpl implements PythonEnvironmentApi {
private readonly _onDidChangeEnvironments = new EventEmitter<DidChangeEnvironmentsEventArgs>();
private readonly _onDidChangeEnvironment = new EventEmitter<DidChangeEnvironmentEventArgs>();
Expand Down Expand Up @@ -216,11 +216,18 @@ class PythonEnvironmentApiImpl implements PythonEnvironmentApi {
}
return new Disposable(() => disposables.forEach((d) => d.dispose()));
}
installPackages(context: PythonEnvironment, packages: string[], options: PackageInstallOptions): Promise<void> {
async installPackages(
context: PythonEnvironment,
packages: string[],
options: PackageInstallOptions,
): Promise<void> {
// get the package manager to exit if not found
const manager = this.envManagers.getPackageManager(context);
if (!manager) {
return Promise.reject(new Error('No package manager found'));
}
Comment thread
eleanorjboyd marked this conversation as resolved.
await packageManagementFlow(packages);
traceInfo(`Python API: Triggering install for packages: ${packages.join(', ')}`);
return manager.install(context, packages, options);
}
uninstallPackages(context: PythonEnvironment, packages: Package[] | string[]): Promise<void> {
Expand Down
73 changes: 73 additions & 0 deletions src/features/utils.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
import { window } from 'vscode';
import { InstallPermissionEnum, SimpleResponseEnum } from './packageManagement';

export type SettingsPackageTrust = {
[key: string]: InstallPermissionEnum.AlwaysAllow | InstallPermissionEnum.AlwaysAsk;
};

export const ALWAYS_ALLOW = 'Always allow installs';
export const ALWAYS_ASK = 'Always ask before installing';
export const INSTALL_NO_CONFIGURE = 'Install without configuring permissions';

export const YES_INSTALL = 'Yes, Install';
export const NO_INSTALL = 'Do Not Install';

export function promptForInstallPermissions(extensionName: string, packages: string): Thenable<InstallPermissionEnum> {
return new Promise((resolve) => {
window
.showInformationMessage(
'Would you like to set permissions for future package installs from the ' + extensionName + ' extension?',
{
detail: `package/s: "${packages}"`,
modal: true,
},
ALWAYS_ASK,
ALWAYS_ALLOW,
INSTALL_NO_CONFIGURE,
)
.then((selectedOption) => {
switch (selectedOption) {
case ALWAYS_ALLOW:
resolve(InstallPermissionEnum.AlwaysAllow);
break;
case ALWAYS_ASK:
resolve(InstallPermissionEnum.AlwaysAsk);
break;
case INSTALL_NO_CONFIGURE:
resolve(InstallPermissionEnum.InstallNoConfigure);
break;
default:
resolve(InstallPermissionEnum.Cancel);
break;
}
});
});
}

export function promptForAlwaysAsk(extensionName: string, packages: string): Thenable<string | undefined> {
return new Promise((resolve) => {
window
.showInformationMessage(
'Do you want to install the following package/s from the ' + extensionName + ' extension?',
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Will we be referencing the package names in this message as well?

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, but if there are multiple then we may want to say something like pandas, … or “pandas and other packages”.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I added it in the detail: package/s: "${packages}", section. Is this what you mean? What is the best way to word it?

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oh I see! I missed that part in the initial review

{
detail: `package/s: "${packages}"`,
modal: true,
},
YES_INSTALL,
NO_INSTALL,
)
.then((selectedOption) => {
switch (selectedOption) {
case YES_INSTALL:
resolve(SimpleResponseEnum.YesInstall);
break;
case NO_INSTALL:
resolve(SimpleResponseEnum.NoInstall);
break;
default:
resolve(SimpleResponseEnum.Cancel);
break;
}
});
});
}
Loading