Hide installation options in Configure Python wizard if python is already setup. (#10635)

This commit is contained in:
Cory Rivera
2020-05-29 17:28:02 -07:00
committed by GitHub
parent d1b1550ce5
commit 98ce3c74e6
9 changed files with 393 additions and 162 deletions

View File

@@ -15,23 +15,37 @@ const localize = nls.loadMessageBundle();
export class ConfigurePathPage extends BasePage { export class ConfigurePathPage extends BasePage {
private readonly BrowseButtonText = localize('configurePython.browseButtonText', "Browse"); private readonly BrowseButtonText = localize('configurePython.browseButtonText', "Browse");
private readonly LocationTextBoxTitle = localize('configurePython.locationTextBoxText', "Python Install Location");
private readonly SelectFileLabel = localize('configurePython.selectFileLabel', "Select"); private readonly SelectFileLabel = localize('configurePython.selectFileLabel', "Select");
private pythonLocationDropdown: azdata.DropDownComponent; private pythonLocationDropdown: azdata.DropDownComponent;
private pythonDropdownLoader: azdata.LoadingComponent; private pythonDropdownLoader: azdata.LoadingComponent;
private browseButton: azdata.ButtonComponent;
private newInstallButton: azdata.RadioButtonComponent; private newInstallButton: azdata.RadioButtonComponent;
private existingInstallButton: azdata.RadioButtonComponent; private existingInstallButton: azdata.RadioButtonComponent;
private selectInstallEnabled: boolean;
private usingCustomPath: boolean = false; private usingCustomPath: boolean = false;
public async initialize(): Promise<boolean> { public async initialize(): Promise<boolean> {
let wizardDescription: string;
if (this.model.kernelName) {
wizardDescription = localize('configurePython.descriptionWithKernel', "The {0} kernel requires a Python runtime to be configured and dependencies to be installed.", this.model.kernelName);
} else {
wizardDescription = localize('configurePython.descriptionWithoutKernel', "Notebook kernels require a Python runtime to be configured and dependencies to be installed.");
}
let wizardDescriptionLabel = this.view.modelBuilder.text()
.withProperties<azdata.TextComponentProperties>({
value: wizardDescription,
CSSStyles: {
'padding': '0px',
'margin': '0px'
}
}).component();
this.pythonLocationDropdown = this.view.modelBuilder.dropDown() this.pythonLocationDropdown = this.view.modelBuilder.dropDown()
.withProperties<azdata.DropDownProperties>({ .withProperties<azdata.DropDownProperties>({
value: undefined, value: undefined,
values: [], values: [],
width: '100%' width: '400px'
}).component(); }).component();
this.pythonDropdownLoader = this.view.modelBuilder.loadingComponent() this.pythonDropdownLoader = this.view.modelBuilder.loadingComponent()
.withItem(this.pythonLocationDropdown) .withItem(this.pythonLocationDropdown)
@@ -39,17 +53,16 @@ export class ConfigurePathPage extends BasePage {
loading: false loading: false
}) })
.component(); .component();
let browseButton = this.view.modelBuilder.button()
this.browseButton = this.view.modelBuilder.button()
.withProperties<azdata.ButtonProperties>({ .withProperties<azdata.ButtonProperties>({
label: this.BrowseButtonText, label: this.BrowseButtonText,
width: '70px' width: '70px'
}).component(); }).component();
this.browseButton.onDidClick(() => this.handleBrowse()); browseButton.onDidClick(() => this.handleBrowse());
this.createInstallRadioButtons(this.view.modelBuilder, this.model.useExistingPython); this.createInstallRadioButtons(this.view.modelBuilder, this.model.useExistingPython);
let formModel = this.view.modelBuilder.formContainer() let selectInstallForm = this.view.modelBuilder.formContainer()
.withFormItems([{ .withFormItems([{
component: this.newInstallButton, component: this.newInstallButton,
title: localize('configurePython.installationType', "Installation Type") title: localize('configurePython.installationType', "Installation Type")
@@ -58,14 +71,66 @@ export class ConfigurePathPage extends BasePage {
title: '' title: ''
}, { }, {
component: this.pythonDropdownLoader, component: this.pythonDropdownLoader,
title: this.LocationTextBoxTitle title: localize('configurePython.locationTextBoxText', "Python Install Location")
}, { }, {
component: this.browseButton, component: browseButton,
title: '' title: ''
}]).component(); }]).component();
let selectInstallContainer = this.view.modelBuilder.divContainer()
.withItems([selectInstallForm])
.withProperties<azdata.DivContainerProperties>({
clickable: false
}).component();
await this.view.initializeModel(formModel); let allParentItems = [selectInstallContainer];
if (this.model.pythonLocation) {
let installedPathTextBox = this.view.modelBuilder.inputBox().withProperties<azdata.TextComponentProperties>({
value: this.model.pythonLocation,
enabled: false,
width: '400px'
}).component();
let editPathButton = this.view.modelBuilder.button().withProperties<azdata.ButtonProperties>({
label: 'Edit',
width: '70px'
}).component();
let editPathForm = this.view.modelBuilder.formContainer()
.withFormItems([{
title: localize('configurePython.pythonConfigured', "Python runtime configured!"),
component: installedPathTextBox
}, {
title: '',
component: editPathButton
}]).component();
let editPathContainer = this.view.modelBuilder.divContainer()
.withItems([editPathForm])
.withProperties<azdata.DivContainerProperties>({
clickable: false
}).component();
allParentItems.push(editPathContainer);
editPathButton.onDidClick(async () => {
editPathContainer.display = 'none';
selectInstallContainer.display = 'block';
this.selectInstallEnabled = true;
});
selectInstallContainer.display = 'none';
this.selectInstallEnabled = false;
} else {
this.selectInstallEnabled = true;
}
let parentContainer = this.view.modelBuilder.flexContainer()
.withLayout({ flexFlow: 'column' }).component();
parentContainer.addItem(wizardDescriptionLabel, {
CSSStyles: {
'padding': '0px 30px 0px 30px'
}
});
parentContainer.addItems(allParentItems);
await this.view.initializeModel(parentContainer);
await this.updatePythonPathsDropdown(this.model.useExistingPython); await this.updatePythonPathsDropdown(this.model.useExistingPython);
return true; return true;
@@ -75,19 +140,24 @@ export class ConfigurePathPage extends BasePage {
} }
public async onPageLeave(): Promise<boolean> { public async onPageLeave(): Promise<boolean> {
let pythonLocation = utils.getDropdownValue(this.pythonLocationDropdown); if (this.pythonDropdownLoader.loading) {
if (!pythonLocation || pythonLocation.length === 0) {
this.instance.showErrorMessage(this.instance.InvalidLocationMsg);
return false; return false;
} }
if (this.selectInstallEnabled) {
let pythonLocation = utils.getDropdownValue(this.pythonLocationDropdown);
if (!pythonLocation || pythonLocation.length === 0) {
this.instance.showErrorMessage(this.instance.InvalidLocationMsg);
return false;
}
this.model.pythonLocation = pythonLocation; this.model.pythonLocation = pythonLocation;
this.model.useExistingPython = !!this.existingInstallButton.checked; this.model.useExistingPython = !!this.existingInstallButton.checked;
}
return true; return true;
} }
private async updatePythonPathsDropdown(useExistingPython: boolean): Promise<void> { private async updatePythonPathsDropdown(useExistingPython: boolean): Promise<void> {
this.instance.wizard.nextButton.enabled = false;
this.pythonDropdownLoader.loading = true; this.pythonDropdownLoader.loading = true;
try { try {
let pythonPaths: PythonPathInfo[]; let pythonPaths: PythonPathInfo[];
@@ -121,6 +191,7 @@ export class ConfigurePathPage extends BasePage {
values: dropdownValues values: dropdownValues
}); });
} finally { } finally {
this.instance.wizard.nextButton.enabled = true;
this.pythonDropdownLoader.loading = false; this.pythonDropdownLoader.loading = false;
} }
} }

View File

@@ -55,6 +55,7 @@ export class ConfigurePythonWizard {
kernelName: kernelName, kernelName: kernelName,
pythonPathsPromise: this.pythonPathsPromise, pythonPathsPromise: this.pythonPathsPromise,
installation: this.jupyterInstallation, installation: this.jupyterInstallation,
pythonLocation: JupyterServerInstallation.getPythonPathSetting(this.apiWrapper),
useExistingPython: JupyterServerInstallation.getExistingPythonSetting(this.apiWrapper) useExistingPython: JupyterServerInstallation.getExistingPythonSetting(this.apiWrapper)
}; };

View File

@@ -6,20 +6,31 @@
import * as azdata from 'azdata'; import * as azdata from 'azdata';
import * as nls from 'vscode-nls'; import * as nls from 'vscode-nls';
import { BasePage } from './basePage'; import { BasePage } from './basePage';
import { JupyterServerInstallation, PythonPkgDetails } from '../../jupyter/jupyterServerInstallation'; import { JupyterServerInstallation } from '../../jupyter/jupyterServerInstallation';
import { python3DisplayName, pysparkDisplayName, sparkScalaDisplayName, sparkRDisplayName, powershellDisplayName, allKernelsName } from '../../common/constants'; import { python3DisplayName, pysparkDisplayName, sparkScalaDisplayName, sparkRDisplayName, powershellDisplayName, allKernelsName } from '../../common/constants';
import { getDropdownValue } from '../../common/utils'; import { getDropdownValue } from '../../common/utils';
const localize = nls.loadMessageBundle(); const localize = nls.loadMessageBundle();
interface RequiredPackageInfo {
name: string;
existingVersion: string;
requiredVersion: string;
}
namespace cssStyles {
export const tableHeader = { 'text-align': 'left', 'font-weight': 'lighter', 'font-size': '10px', 'user-select': 'text', 'border': 'none' };
export const tableRow = { 'border-top': 'solid 1px #ccc', 'border-bottom': 'solid 1px #ccc', 'border-left': 'none', 'border-right': 'none' };
}
export class PickPackagesPage extends BasePage { export class PickPackagesPage extends BasePage {
private kernelLabel: azdata.TextComponent | undefined; private kernelLabel: azdata.TextComponent | undefined;
private kernelDropdown: azdata.DropDownComponent | undefined; private kernelDropdown: azdata.DropDownComponent | undefined;
private requiredPackagesTable: azdata.DeclarativeTableComponent; private requiredPackagesTable: azdata.DeclarativeTableComponent;
private packageTableSpinner: azdata.LoadingComponent; private packageTableSpinner: azdata.LoadingComponent;
private installedPackagesPromise: Promise<PythonPkgDetails[]>; private packageVersionRetrieval: Promise<void>;
private installedPackages: PythonPkgDetails[]; private packageVersionMap = new Map<string, string>();
public async initialize(): Promise<boolean> { public async initialize(): Promise<boolean> {
if (this.model.kernelName) { if (this.model.kernelName) {
@@ -39,22 +50,46 @@ export class PickPackagesPage extends BasePage {
}); });
} }
let nameColumn = localize('configurePython.pkgNameColumn', "Name");
let existingVersionColumn = localize('configurePython.existingVersionColumn', "Existing Version");
let requiredVersionColumn = localize('configurePython.requiredVersionColumn', "Required Version");
this.requiredPackagesTable = this.view.modelBuilder.declarativeTable().withProperties<azdata.DeclarativeTableProperties>({ this.requiredPackagesTable = this.view.modelBuilder.declarativeTable().withProperties<azdata.DeclarativeTableProperties>({
columns: [{ columns: [{
displayName: localize('configurePython.pkgNameColumn', "Name"), displayName: nameColumn,
ariaLabel: nameColumn,
valueType: azdata.DeclarativeDataType.string, valueType: azdata.DeclarativeDataType.string,
isReadOnly: true, isReadOnly: true,
width: '200px' width: '200px',
headerCssStyles: {
...cssStyles.tableHeader
},
rowCssStyles: {
...cssStyles.tableRow
}
}, { }, {
displayName: localize('configurePython.existingVersionColumn', "Existing Version"), displayName: existingVersionColumn,
ariaLabel: existingVersionColumn,
valueType: azdata.DeclarativeDataType.string, valueType: azdata.DeclarativeDataType.string,
isReadOnly: true, isReadOnly: true,
width: '200px' width: '200px',
headerCssStyles: {
...cssStyles.tableHeader
},
rowCssStyles: {
...cssStyles.tableRow
}
}, { }, {
displayName: localize('configurePython.requiredVersionColumn', "Required Version"), displayName: requiredVersionColumn,
ariaLabel: requiredVersionColumn,
valueType: azdata.DeclarativeDataType.string, valueType: azdata.DeclarativeDataType.string,
isReadOnly: true, isReadOnly: true,
width: '200px' width: '200px',
headerCssStyles: {
...cssStyles.tableHeader
},
rowCssStyles: {
...cssStyles.tableRow
}
}], }],
data: [[]] data: [[]]
}).component(); }).component();
@@ -74,9 +109,16 @@ export class PickPackagesPage extends BasePage {
} }
public async onPageEnter(): Promise<void> { public async onPageEnter(): Promise<void> {
this.packageVersionMap.clear();
let pythonExe = JupyterServerInstallation.getPythonExePath(this.model.pythonLocation, this.model.useExistingPython); let pythonExe = JupyterServerInstallation.getPythonExePath(this.model.pythonLocation, this.model.useExistingPython);
this.installedPackagesPromise = this.model.installation.getInstalledPipPackages(pythonExe); this.packageVersionRetrieval = this.model.installation.getInstalledPipPackages(pythonExe)
this.installedPackages = undefined; .then(installedPackages => {
if (installedPackages) {
installedPackages.forEach(pkg => {
this.packageVersionMap.set(pkg.name, pkg.version);
});
}
});
if (this.kernelDropdown) { if (this.kernelDropdown) {
if (this.model.kernelName) { if (this.model.kernelName) {
@@ -89,45 +131,39 @@ export class PickPackagesPage extends BasePage {
} }
public async onPageLeave(): Promise<boolean> { public async onPageLeave(): Promise<boolean> {
return true; return !this.packageTableSpinner.loading;
} }
private async updateRequiredPackages(kernelName: string): Promise<void> { private async updateRequiredPackages(kernelName: string): Promise<void> {
this.instance.wizard.doneButton.enabled = false;
this.packageTableSpinner.loading = true; this.packageTableSpinner.loading = true;
try { try {
let pkgVersionMap = new Map<string, { currentVersion: string, newVersion: string }>();
// Fetch list of required packages for the specified kernel // Fetch list of required packages for the specified kernel
let requiredPackages = JupyterServerInstallation.getRequiredPackagesForKernel(kernelName); let requiredPkgVersions: RequiredPackageInfo[] = [];
let requiredPackages = this.model.installation.getRequiredPackagesForKernel(kernelName);
requiredPackages.forEach(pkg => { requiredPackages.forEach(pkg => {
pkgVersionMap.set(pkg.name, { currentVersion: undefined, newVersion: pkg.version }); requiredPkgVersions.push({ name: pkg.name, existingVersion: undefined, requiredVersion: pkg.version });
}); });
// For each required package, check if there is another version of that package already installed // For each required package, check if there is another version of that package already installed
if (!this.installedPackages) { await this.packageVersionRetrieval;
this.installedPackages = await this.installedPackagesPromise; requiredPkgVersions.forEach(pkgVersion => {
} let installedPackageVersion = this.packageVersionMap.get(pkgVersion.name);
this.installedPackages.forEach(pkg => { if (installedPackageVersion) {
let info = pkgVersionMap.get(pkg.name); pkgVersion.existingVersion = installedPackageVersion;
if (info) {
info.currentVersion = pkg.version;
pkgVersionMap.set(pkg.name, info);
} }
}); });
if (pkgVersionMap.size > 0) { if (requiredPkgVersions.length > 0) {
let packageData = []; this.requiredPackagesTable.data = requiredPkgVersions.map(pkg => [pkg.name, pkg.existingVersion ?? '-', pkg.requiredVersion]);
for (let [key, value] of pkgVersionMap.entries()) {
packageData.push([key, value.currentVersion ?? '-', value.newVersion]);
}
this.requiredPackagesTable.data = packageData;
this.model.packagesToInstall = requiredPackages; this.model.packagesToInstall = requiredPackages;
} else { } else {
this.instance.showErrorMessage(localize('msgUnsupportedKernel', "Could not retrieve packages for unsupported kernel {0}", kernelName)); this.instance.showErrorMessage(localize('msgUnsupportedKernel', "Could not retrieve packages for kernel {0}", kernelName));
this.requiredPackagesTable.data = [['-', '-', '-']]; this.requiredPackagesTable.data = [['-', '-', '-']];
this.model.packagesToInstall = undefined; this.model.packagesToInstall = undefined;
} }
} finally { } finally {
this.instance.wizard.doneButton.enabled = true;
this.packageTableSpinner.loading = false; this.packageTableSpinner.loading = false;
} }
} }

View File

@@ -251,8 +251,7 @@ export class JupyterController implements vscode.Disposable {
} }
public doConfigurePython(jupyterInstaller: JupyterServerInstallation): void { public doConfigurePython(jupyterInstaller: JupyterServerInstallation): void {
let enablePreviewFeatures = this.apiWrapper.getConfiguration('workbench').get('enablePreviewFeatures'); if (jupyterInstaller.previewFeaturesEnabled) {
if (enablePreviewFeatures) {
let pythonWizard = new ConfigurePythonWizard(this.apiWrapper, jupyterInstaller); let pythonWizard = new ConfigurePythonWizard(this.apiWrapper, jupyterInstaller);
pythonWizard.start().catch((err: any) => { pythonWizard.start().catch((err: any) => {
this.apiWrapper.showErrorMessage(utils.getErrorMessage(err)); this.apiWrapper.showErrorMessage(utils.getErrorMessage(err));

View File

@@ -34,7 +34,6 @@ const msgTaskName = localize('msgTaskName', "Installing Notebook dependencies");
const msgInstallPkgStart = localize('msgInstallPkgStart', "Installing Notebook dependencies, see Tasks view for more information"); const msgInstallPkgStart = localize('msgInstallPkgStart', "Installing Notebook dependencies, see Tasks view for more information");
const msgInstallPkgFinish = localize('msgInstallPkgFinish', "Notebook dependencies installation is complete"); const msgInstallPkgFinish = localize('msgInstallPkgFinish', "Notebook dependencies installation is complete");
const msgPythonRunningError = localize('msgPythonRunningError', "Cannot overwrite an existing Python installation while python is running. Please close any active notebooks before proceeding."); const msgPythonRunningError = localize('msgPythonRunningError', "Cannot overwrite an existing Python installation while python is running. Please close any active notebooks before proceeding.");
const msgSkipPythonInstall = localize('msgSkipPythonInstall', "Python already exists at the specific location. Skipping install.");
const msgWaitingForInstall = localize('msgWaitingForInstall', "Another Python installation is currently in progress. Waiting for it to complete."); const msgWaitingForInstall = localize('msgWaitingForInstall', "Another Python installation is currently in progress. Waiting for it to complete.");
function msgDependenciesInstallationFailed(errorMessage: string): string { return localize('msgDependenciesInstallationFailed', "Installing Notebook dependencies failed with error: {0}", errorMessage); } function msgDependenciesInstallationFailed(errorMessage: string): string { return localize('msgDependenciesInstallationFailed', "Installing Notebook dependencies failed with error: {0}", errorMessage); }
function msgDownloadPython(platform: string, pythonDownloadUrl: string): string { return localize('msgDownloadPython', "Downloading local python for platform: {0} to {1}", platform, pythonDownloadUrl); } function msgDownloadPython(platform: string, pythonDownloadUrl: string): string { return localize('msgDownloadPython', "Downloading local python for platform: {0} to {1}", platform, pythonDownloadUrl); }
@@ -110,6 +109,8 @@ export class JupyterServerInstallation implements IJupyterServerInstallation {
private readonly _expectedCondaPackages: PythonPkgDetails[]; private readonly _expectedCondaPackages: PythonPkgDetails[];
private _kernelSetupCache: Map<string, boolean>; private _kernelSetupCache: Map<string, boolean>;
private readonly _requiredKernelPackages: Map<string, PythonPkgDetails[]>;
private readonly _requiredPackagesSet: Set<string>;
constructor(extensionPath: string, outputChannel: OutputChannel, apiWrapper: ApiWrapper, pythonInstallationPath?: string) { constructor(extensionPath: string, outputChannel: OutputChannel, apiWrapper: ApiWrapper, pythonInstallationPath?: string) {
this.extensionPath = extensionPath; this.extensionPath = extensionPath;
@@ -129,28 +130,66 @@ export class JupyterServerInstallation implements IJupyterServerInstallation {
} }
this._kernelSetupCache = new Map<string, boolean>(); this._kernelSetupCache = new Map<string, boolean>();
this._requiredKernelPackages = new Map<string, PythonPkgDetails[]>();
let jupyterPkg = {
name: 'jupyter',
version: '1.0.0'
};
this._requiredKernelPackages.set(constants.python3DisplayName, [jupyterPkg]);
let powershellPkg = {
name: 'powershell-kernel',
version: '0.1.3'
};
this._requiredKernelPackages.set(constants.powershellDisplayName, [jupyterPkg, powershellPkg]);
let sparkPackages = [
jupyterPkg,
{
name: 'sparkmagic',
version: '0.12.9'
}, {
name: 'pandas',
version: '0.24.2'
}, {
name: 'prose-codeaccelerator',
version: '1.3.0'
}];
this._requiredKernelPackages.set(constants.pysparkDisplayName, sparkPackages);
this._requiredKernelPackages.set(constants.sparkScalaDisplayName, sparkPackages);
this._requiredKernelPackages.set(constants.sparkRDisplayName, sparkPackages);
let allPackages = sparkPackages.concat(powershellPkg);
this._requiredKernelPackages.set(constants.allKernelsName, allPackages);
this._requiredPackagesSet = new Set<string>();
allPackages.forEach(pkg => {
this._requiredPackagesSet.add(pkg.name);
});
} }
private async installDependencies(backgroundOperation: azdata.BackgroundOperation, forceInstall: boolean, specificPackages?: PythonPkgDetails[]): Promise<void> { private async installDependencies(backgroundOperation: azdata.BackgroundOperation, forceInstall: boolean, specificPackages?: PythonPkgDetails[]): Promise<void> {
if (!(await utils.exists(this._pythonExecutable)) || forceInstall || this._usingExistingPython) { window.showInformationMessage(msgInstallPkgStart);
window.showInformationMessage(msgInstallPkgStart);
this.outputChannel.show(true); this.outputChannel.show(true);
this.outputChannel.appendLine(msgInstallPkgProgress); this.outputChannel.appendLine(msgInstallPkgProgress);
backgroundOperation.updateStatus(azdata.TaskStatus.InProgress, msgInstallPkgProgress); backgroundOperation.updateStatus(azdata.TaskStatus.InProgress, msgInstallPkgProgress);
try { try {
let pythonExists = await utils.exists(this._pythonExecutable);
if (!pythonExists || forceInstall) {
await this.installPythonPackage(backgroundOperation, this._usingExistingPython, this._pythonInstallationPath, this.outputChannel); await this.installPythonPackage(backgroundOperation, this._usingExistingPython, this._pythonInstallationPath, this.outputChannel);
await this.upgradePythonPackages(false, forceInstall, specificPackages);
} catch (err) {
this.outputChannel.appendLine(msgDependenciesInstallationFailed(utils.getErrorMessage(err)));
throw err;
} }
await this.upgradePythonPackages(false, forceInstall, specificPackages);
this.outputChannel.appendLine(msgInstallPkgFinish); } catch (err) {
backgroundOperation.updateStatus(azdata.TaskStatus.Succeeded, msgInstallPkgFinish); this.outputChannel.appendLine(msgDependenciesInstallationFailed(utils.getErrorMessage(err)));
window.showInformationMessage(msgInstallPkgFinish); throw err;
} }
this.outputChannel.appendLine(msgInstallPkgFinish);
backgroundOperation.updateStatus(azdata.TaskStatus.Succeeded, msgInstallPkgFinish);
window.showInformationMessage(msgInstallPkgFinish);
} }
public installPythonPackage(backgroundOperation: azdata.BackgroundOperation, usingExistingPython: boolean, pythonInstallationPath: string, outputChannel: OutputChannel): Promise<void> { public installPythonPackage(backgroundOperation: azdata.BackgroundOperation, usingExistingPython: boolean, pythonInstallationPath: string, outputChannel: OutputChannel): Promise<void> {
@@ -388,41 +427,29 @@ export class JupyterServerInstallation implements IJupyterServerInstallation {
this._usingExistingPython = installSettings.existingPython; this._usingExistingPython = installSettings.existingPython;
await this.configurePackagePaths(); await this.configurePackagePaths();
let updateConfig = async () => { this.apiWrapper.startBackgroundOperation({
let notebookConfig = this.apiWrapper.getConfiguration(constants.notebookConfigKey); displayName: msgTaskName,
await notebookConfig.update(constants.pythonPathConfigKey, this._pythonInstallationPath, ConfigurationTarget.Global); description: msgTaskName,
await notebookConfig.update(constants.existingPythonConfigKey, this._usingExistingPython, ConfigurationTarget.Global); isCancelable: false,
await this.configurePackagePaths(); operation: op => {
}; this.installDependencies(op, forceInstall, installSettings.specificPackages)
.then(async () => {
let notebookConfig = this.apiWrapper.getConfiguration(constants.notebookConfigKey);
await notebookConfig.update(constants.pythonPathConfigKey, this._pythonInstallationPath, ConfigurationTarget.Global);
await notebookConfig.update(constants.existingPythonConfigKey, this._usingExistingPython, ConfigurationTarget.Global);
await this.configurePackagePaths();
if (!(await utils.exists(this._pythonExecutable)) || forceInstall || this._usingExistingPython) { this._installCompletion.resolve();
this.apiWrapper.startBackgroundOperation({ this._installInProgress = false;
displayName: msgTaskName, })
description: msgTaskName, .catch(err => {
isCancelable: false, let errorMsg = msgDependenciesInstallationFailed(utils.getErrorMessage(err));
operation: op => { op.updateStatus(azdata.TaskStatus.Failed, errorMsg);
this.installDependencies(op, forceInstall, installSettings.specificPackages) this._installCompletion.reject(errorMsg);
.then(async () => { this._installInProgress = false;
await updateConfig(); });
this._installCompletion.resolve(); }
this._installInProgress = false; });
})
.catch(err => {
let errorMsg = msgDependenciesInstallationFailed(utils.getErrorMessage(err));
op.updateStatus(azdata.TaskStatus.Failed, errorMsg);
this._installCompletion.reject(errorMsg);
this._installInProgress = false;
});
}
});
} else {
// Python executable already exists, but the path setting wasn't defined,
// so update it here
await updateConfig();
this._installCompletion.resolve();
this._installInProgress = false;
this.apiWrapper.showInfoMessage(msgSkipPythonInstall);
}
return this._installCompletion.promise; return this._installCompletion.promise;
} }
@@ -430,12 +457,20 @@ export class JupyterServerInstallation implements IJupyterServerInstallation {
* Opens a dialog for configuring the installation path for the Notebook Python dependencies. * Opens a dialog for configuring the installation path for the Notebook Python dependencies.
*/ */
public async promptForPythonInstall(kernelDisplayName: string): Promise<void> { public async promptForPythonInstall(kernelDisplayName: string): Promise<void> {
if (!JupyterServerInstallation.isPythonInstalled(this.apiWrapper)) { if (this._installInProgress) {
let enablePreviewFeatures = this.apiWrapper.getConfiguration('workbench').get('enablePreviewFeatures'); this.apiWrapper.showInfoMessage(msgWaitingForInstall);
if (enablePreviewFeatures) { return this._installCompletion.promise;
}
let isPythonInstalled = JupyterServerInstallation.isPythonInstalled(this.apiWrapper);
let areRequiredPackagesInstalled = await this.areRequiredPackagesInstalled(kernelDisplayName);
if (!isPythonInstalled || !areRequiredPackagesInstalled) {
if (this.previewFeaturesEnabled) {
let pythonWizard = new ConfigurePythonWizard(this.apiWrapper, this); let pythonWizard = new ConfigurePythonWizard(this.apiWrapper, this);
await pythonWizard.start(kernelDisplayName, true); await pythonWizard.start(kernelDisplayName, true);
return pythonWizard.setupComplete; return pythonWizard.setupComplete.then(() => {
this._kernelSetupCache.set(kernelDisplayName, true);
});
} else { } else {
let pythonDialog = new ConfigurePythonDialog(this.apiWrapper, this); let pythonDialog = new ConfigurePythonDialog(this.apiWrapper, this);
return pythonDialog.showDialog(true); return pythonDialog.showDialog(true);
@@ -453,12 +488,11 @@ export class JupyterServerInstallation implements IJupyterServerInstallation {
} }
let requiredPackages: PythonPkgDetails[]; let requiredPackages: PythonPkgDetails[];
let enablePreviewFeatures = this.apiWrapper.getConfiguration('workbench').get('enablePreviewFeatures'); if (this.previewFeaturesEnabled) {
if (enablePreviewFeatures) {
if (this._kernelSetupCache.get(kernelName)) { if (this._kernelSetupCache.get(kernelName)) {
return; return;
} }
requiredPackages = JupyterServerInstallation.getRequiredPackagesForKernel(kernelName); requiredPackages = this.getRequiredPackagesForKernel(kernelName);
} }
this._installInProgress = true; this._installInProgress = true;
@@ -477,6 +511,27 @@ export class JupyterServerInstallation implements IJupyterServerInstallation {
return this._installCompletion.promise; return this._installCompletion.promise;
} }
private async areRequiredPackagesInstalled(kernelDisplayName: string): Promise<boolean> {
if (this._kernelSetupCache.get(kernelDisplayName)) {
return true;
}
let installedPackages = await this.getInstalledPipPackages();
let installedPackageMap = new Map<string, string>();
installedPackages.forEach(pkg => {
installedPackageMap.set(pkg.name, pkg.version);
});
let requiredPackages = this.getRequiredPackagesForKernel(kernelDisplayName);
for (let pkg of requiredPackages) {
let installedVersion = installedPackageMap.get(pkg.name);
if (!installedVersion || utils.comparePackageVersions(installedVersion, pkg.version) < 0) {
return false;
}
}
this._kernelSetupCache.set(kernelDisplayName, true);
return true;
}
private async upgradePythonPackages(promptForUpgrade: boolean, forceInstall: boolean, specificPackages?: PythonPkgDetails[]): Promise<void> { private async upgradePythonPackages(promptForUpgrade: boolean, forceInstall: boolean, specificPackages?: PythonPkgDetails[]): Promise<void> {
let expectedCondaPackages: PythonPkgDetails[]; let expectedCondaPackages: PythonPkgDetails[];
let expectedPipPackages: PythonPkgDetails[]; let expectedPipPackages: PythonPkgDetails[];
@@ -625,6 +680,13 @@ export class JupyterServerInstallation implements IJupyterServerInstallation {
} }
public uninstallPipPackages(packages: PythonPkgDetails[]): Promise<void> { public uninstallPipPackages(packages: PythonPkgDetails[]): Promise<void> {
for (let pkg of packages) {
if (this._requiredPackagesSet.has(pkg.name)) {
this._kernelSetupCache.clear();
break;
}
}
let packagesStr = packages.map(pkg => `"${pkg.name}==${pkg.version}"`).join(' '); let packagesStr = packages.map(pkg => `"${pkg.name}==${pkg.version}"`).join(' ');
let cmd = `"${this.pythonExecutable}" -m pip uninstall -y ${packagesStr}`; let cmd = `"${this.pythonExecutable}" -m pip uninstall -y ${packagesStr}`;
return this.executeStreamedCommand(cmd); return this.executeStreamedCommand(cmd);
@@ -669,6 +731,13 @@ export class JupyterServerInstallation implements IJupyterServerInstallation {
} }
public uninstallCondaPackages(packages: PythonPkgDetails[]): Promise<void> { public uninstallCondaPackages(packages: PythonPkgDetails[]): Promise<void> {
for (let pkg of packages) {
if (this._requiredPackagesSet.has(pkg.name)) {
this._kernelSetupCache.clear();
break;
}
}
let condaExe = this.getCondaExePath(); let condaExe = this.getCondaExePath();
let packagesStr = packages.map(pkg => `"${pkg.name}==${pkg.version}"`).join(' '); let packagesStr = packages.map(pkg => `"${pkg.name}==${pkg.version}"`).join(' ');
let cmd = `"${condaExe}" uninstall -y ${packagesStr}`; let cmd = `"${condaExe}" uninstall -y ${packagesStr}`;
@@ -751,7 +820,7 @@ export class JupyterServerInstallation implements IJupyterServerInstallation {
return useExistingPython; return useExistingPython;
} }
private static getPythonPathSetting(apiWrapper: ApiWrapper): string { public static getPythonPathSetting(apiWrapper: ApiWrapper): string {
let path = undefined; let path = undefined;
if (apiWrapper) { if (apiWrapper) {
let notebookConfig = apiWrapper.getConfiguration(constants.notebookConfigKey); let notebookConfig = apiWrapper.getConfiguration(constants.notebookConfigKey);
@@ -813,53 +882,12 @@ export class JupyterServerInstallation implements IJupyterServerInstallation {
return undefined; return undefined;
} }
public static getRequiredPackagesForKernel(kernelName: string): PythonPkgDetails[] { public getRequiredPackagesForKernel(kernelName: string): PythonPkgDetails[] {
let packages = [{ return this._requiredKernelPackages.get(kernelName) ?? [];
name: 'jupyter', }
version: '1.0.0'
}]; public get previewFeaturesEnabled(): boolean {
switch (kernelName) { return this.apiWrapper.getConfiguration('workbench').get('enablePreviewFeatures');
case constants.python3DisplayName:
break;
case constants.pysparkDisplayName:
case constants.sparkScalaDisplayName:
case constants.sparkRDisplayName:
packages.push({
name: 'sparkmagic',
version: '0.12.9'
}, {
name: 'pandas',
version: '0.24.2'
}, {
name: 'prose-codeaccelerator',
version: '1.3.0'
});
break;
case constants.powershellDisplayName:
packages.push({
name: 'powershell-kernel',
version: '0.1.3'
});
break;
case constants.allKernelsName:
packages.push({
name: 'sparkmagic',
version: '0.12.9'
}, {
name: 'pandas',
version: '0.24.2'
}, {
name: 'prose-codeaccelerator',
version: '1.3.0'
}, {
name: 'powershell-kernel',
version: '0.1.3'
});
break;
default:
return undefined;
}
return packages;
} }
} }

View File

@@ -109,7 +109,9 @@ export class LocalJupyterServerManager implements nb.ServerManager, vscode.Dispo
private async doStartServer(kernelSpec: nb.IKernelSpec): Promise<IServerInstance> { // We can't find or create servers until the installation is complete private async doStartServer(kernelSpec: nb.IKernelSpec): Promise<IServerInstance> { // We can't find or create servers until the installation is complete
let installation = this.options.jupyterInstallation; let installation = this.options.jupyterInstallation;
await installation.promptForPythonInstall(kernelSpec.display_name); await installation.promptForPythonInstall(kernelSpec.display_name);
await installation.promptForPackageUpgrade(kernelSpec.display_name); if (!installation.previewFeaturesEnabled) {
await installation.promptForPackageUpgrade(kernelSpec.display_name);
}
this._apiWrapper.setCommandContext(CommandContext.NotebookPythonInstalled, true); this._apiWrapper.setCommandContext(CommandContext.NotebookPythonInstalled, true);
// Calculate the path to use as the notebook-dir for Jupyter based on the path of the uri of the // Calculate the path to use as the notebook-dir for Jupyter based on the path of the uri of the

View File

@@ -237,7 +237,11 @@ export class JupyterSession implements nb.ISession {
public async changeKernel(kernelInfo: nb.IKernelSpec): Promise<nb.IKernel> { public async changeKernel(kernelInfo: nb.IKernelSpec): Promise<nb.IKernel> {
if (this._installation) { if (this._installation) {
try { try {
await this._installation.promptForPackageUpgrade(kernelInfo.display_name); if (this._installation.previewFeaturesEnabled) {
await this._installation.promptForPythonInstall(kernelInfo.display_name);
} else {
await this._installation.promptForPackageUpgrade(kernelInfo.display_name);
}
} catch (err) { } catch (err) {
// Have to swallow the error here to prevent hangs when changing back to the old kernel. // Have to swallow the error here to prevent hangs when changing back to the old kernel.
console.error(err.toString()); console.error(err.toString());

View File

@@ -337,6 +337,45 @@ class TestFormContainer extends TestComponentBase implements azdata.FormContaine
} }
} }
class TestDivContainer extends TestComponentBase implements azdata.DivContainer {
onDidClick: vscode.Event<any>;
items: azdata.Component[] = [];
clearItems(): void {
}
addItems(itemConfigs: azdata.Component[], itemLayout?: azdata.DivItemLayout): void {
}
addItem(component: azdata.Component, itemLayout?: azdata.DivItemLayout): void {
}
insertItem(component: azdata.Component, index: number, itemLayout?: azdata.DivItemLayout): void {
}
removeItem(component: azdata.Component): boolean {
return true;
}
setLayout(layout: azdata.DivLayout): void {
}
setItemLayout(component: azdata.Component, layout: azdata.DivItemLayout): void {
}
}
class TestFlexContainer extends TestComponentBase implements azdata.FlexContainer {
items: azdata.Component[] = [];
clearItems(): void {
}
addItems(itemConfigs: azdata.Component[], itemLayout?: azdata.FlexItemLayout): void {
}
addItem(component: azdata.Component, itemLayout?: azdata.FlexItemLayout): void {
}
insertItem(component: azdata.Component, index: number, itemLayout?: azdata.FlexItemLayout): void {
}
removeItem(component: azdata.Component): boolean {
return true;
}
setLayout(layout: azdata.FlexLayout): void {
}
setItemLayout(component: azdata.Component, layout: azdata.FlexItemLayout): void {
}
}
class TestComponentBuilder<T extends azdata.Component> implements azdata.ComponentBuilder<T> { class TestComponentBuilder<T extends azdata.Component> implements azdata.ComponentBuilder<T> {
constructor(private _component: T) { constructor(private _component: T) {
} }
@@ -383,6 +422,34 @@ export function createViewContext(): TestContext {
withLayout: () => formBuilder withLayout: () => formBuilder
}); });
let div: azdata.DivContainer = new TestDivContainer();
let divBuilder: azdata.DivBuilder = Object.assign({}, {
component: () => div,
addFormItem: () => { },
insertFormItem: () => { },
removeFormItem: () => true,
addFormItems: () => { },
withFormItems: () => divBuilder,
withProperties: () => divBuilder,
withValidation: () => divBuilder,
withItems: () => divBuilder,
withLayout: () => divBuilder
});
let flex: azdata.FlexContainer = new TestFlexContainer();
let flexBuilder: azdata.FlexBuilder = Object.assign({}, {
component: () => flex,
addFormItem: () => { },
insertFormItem: () => { },
removeFormItem: () => true,
addFormItems: () => { },
withFormItems: () => flexBuilder,
withProperties: () => flexBuilder,
withValidation: () => flexBuilder,
withItems: () => flexBuilder,
withLayout: () => flexBuilder
});
let view: azdata.ModelView = { let view: azdata.ModelView = {
onClosed: undefined!, onClosed: undefined!,
connection: undefined!, connection: undefined!,
@@ -398,7 +465,9 @@ export function createViewContext(): TestContext {
dropDown: () => dropdownBuilder, dropDown: () => dropdownBuilder,
declarativeTable: () => declarativeTableBuilder, declarativeTable: () => declarativeTableBuilder,
formContainer: () => formBuilder, formContainer: () => formBuilder,
loadingComponent: () => loadingBuilder loadingComponent: () => loadingBuilder,
divContainer: () => divBuilder,
flexContainer: () => flexBuilder
} }
}; };
@@ -412,4 +481,13 @@ export interface TestContext {
view: azdata.ModelView; view: azdata.ModelView;
onClick: vscode.EventEmitter<any>; onClick: vscode.EventEmitter<any>;
} }
export class TestButton implements azdata.window.Button {
label: string;
enabled: boolean;
hidden: boolean;
constructor(private onClickEmitter: vscode.EventEmitter<void>) {
}
onClick: vscode.Event<void> = this.onClickEmitter.event;
}
//#endregion //#endregion

View File

@@ -12,7 +12,8 @@ import { ConfigurePathPage } from '../dialog/configurePython/configurePathPage';
import * as should from 'should'; import * as should from 'should';
import { PickPackagesPage } from '../dialog/configurePython/pickPackagesPage'; import { PickPackagesPage } from '../dialog/configurePython/pickPackagesPage';
import { python3DisplayName, allKernelsName } from '../common/constants'; import { python3DisplayName, allKernelsName } from '../common/constants';
import { TestContext, createViewContext } from './common'; import { TestContext, createViewContext, TestButton } from './common';
import { EventEmitter } from 'vscode';
describe('Configure Python Wizard', function () { describe('Configure Python Wizard', function () {
let apiWrapper: ApiWrapper = new ApiWrapper(); let apiWrapper: ApiWrapper = new ApiWrapper();
@@ -23,11 +24,20 @@ describe('Configure Python Wizard', function () {
beforeEach(() => { beforeEach(() => {
let mockInstall = TypeMoq.Mock.ofType(JupyterServerInstallation); let mockInstall = TypeMoq.Mock.ofType(JupyterServerInstallation);
mockInstall.setup(i => i.getInstalledPipPackages(TypeMoq.It.isAnyString())).returns(() => Promise.resolve([])); mockInstall.setup(i => i.getInstalledPipPackages(TypeMoq.It.isAnyString())).returns(() => Promise.resolve([]));
mockInstall.setup(i => i.getRequiredPackagesForKernel(TypeMoq.It.isAnyString())).returns(() => [{ name: 'TestPkg', version: '1.0.0'}]);
testInstallation = mockInstall.object; testInstallation = mockInstall.object;
let mockWizard = TypeMoq.Mock.ofType(ConfigurePythonWizard); let mockDoneButton = new TestButton(new EventEmitter<void>());
mockWizard.setup(w => w.showErrorMessage(TypeMoq.It.isAnyString())); let mockNextButton = new TestButton(new EventEmitter<void>());
testWizard = mockWizard.object;
let mockWizard = TypeMoq.Mock.ofType<azdata.window.Wizard>();
mockWizard.setup(w => w.doneButton).returns(() => mockDoneButton);
mockWizard.setup(w => w.nextButton).returns(() => mockNextButton);
let mockPythonWizard = TypeMoq.Mock.ofType(ConfigurePythonWizard);
mockPythonWizard.setup(w => w.showErrorMessage(TypeMoq.It.isAnyString()));
mockPythonWizard.setup(w => w.wizard).returns(() => mockWizard.object);
testWizard = mockPythonWizard.object;
viewContext = createViewContext(); viewContext = createViewContext();
}); });
@@ -82,6 +92,7 @@ describe('Configure Python Wizard', function () {
// First page, so onPageEnter should do nothing // First page, so onPageEnter should do nothing
await should(configurePathPage.onPageEnter()).be.resolved(); await should(configurePathPage.onPageEnter()).be.resolved();
should(testWizard.wizard.nextButton.enabled).be.true();
should(await configurePathPage.onPageLeave()).be.true(); should(await configurePathPage.onPageLeave()).be.true();
should(model.useExistingPython).be.true(); should(model.useExistingPython).be.true();
@@ -108,7 +119,8 @@ describe('Configure Python Wizard', function () {
should(await pickPackagesPage.onPageLeave()).be.true(); should(await pickPackagesPage.onPageLeave()).be.true();
await should(pickPackagesPage.onPageEnter()).be.resolved(); await should(pickPackagesPage.onPageEnter()).be.resolved();
should(model.packagesToInstall).be.deepEqual(JupyterServerInstallation.getRequiredPackagesForKernel(allKernelsName)); should(testWizard.wizard.doneButton.enabled).be.true();
should(model.packagesToInstall).be.deepEqual(testInstallation.getRequiredPackagesForKernel(allKernelsName));
}); });
it('Undefined kernel test', async () => { it('Undefined kernel test', async () => {
@@ -128,6 +140,6 @@ describe('Configure Python Wizard', function () {
should((<any>pickPackagesPage).kernelDropdown).not.be.undefined(); should((<any>pickPackagesPage).kernelDropdown).not.be.undefined();
await should(pickPackagesPage.onPageEnter()).be.resolved(); await should(pickPackagesPage.onPageEnter()).be.resolved();
should(model.packagesToInstall).be.deepEqual(JupyterServerInstallation.getRequiredPackagesForKernel(python3DisplayName)); should(model.packagesToInstall).be.deepEqual(testInstallation.getRequiredPackagesForKernel(python3DisplayName));
}); });
}); });