diff --git a/extensions/notebook/package.json b/extensions/notebook/package.json index e6ef3883e3..972ca80daf 100644 --- a/extensions/notebook/package.json +++ b/extensions/notebook/package.json @@ -111,8 +111,8 @@ } }, { - "command": "jupyter.cmd.installPackages", - "title": "%title.installPackages%", + "command": "jupyter.cmd.managePackages", + "title": "%title.managePackages%", "icon": { "dark": "resources/dark/manage_inverse.svg", "light": "resources/light/manage.svg" @@ -188,7 +188,7 @@ "when": "false" }, { - "command": "jupyter.cmd.installPackages", + "command": "jupyter.cmd.managePackages", "when": "false" } ], @@ -223,7 +223,7 @@ ], "notebook/toolbar": [ { - "command": "jupyter.cmd.installPackages", + "command": "jupyter.cmd.managePackages", "when": "providerId == jupyter" } ] diff --git a/extensions/notebook/package.nls.json b/extensions/notebook/package.nls.json index 076302509d..347cf607ef 100644 --- a/extensions/notebook/package.nls.json +++ b/extensions/notebook/package.nls.json @@ -24,5 +24,5 @@ "config.jupyter.kernelConfigValuesDescription": "Configuration options for Jupyter kernels. This is automatically managed and not recommended to be manually edited.", "title.reinstallNotebookDependencies": "Reinstall Notebook dependencies", "title.configurePython": "Configure Python for Notebooks", - "title.installPackages": "Install Packages" + "title.managePackages": "Manage Packages" } \ No newline at end of file diff --git a/extensions/notebook/src/common/constants.ts b/extensions/notebook/src/common/constants.ts index cd43c3fa72..91fbf770f5 100644 --- a/extensions/notebook/src/common/constants.ts +++ b/extensions/notebook/src/common/constants.ts @@ -39,7 +39,7 @@ export const jupyterCommandSetContext = 'jupyter.setContext'; export const jupyterCommandSetKernel = 'jupyter.setKernel'; export const jupyterReinstallDependenciesCommand = 'jupyter.reinstallDependencies'; export const jupyterAnalyzeCommand = 'jupyter.cmd.analyzeNotebook'; -export const jupyterInstallPackages = 'jupyter.cmd.installPackages'; +export const jupyterManagePackages = 'jupyter.cmd.managePackages'; export const jupyterConfigurePython = 'jupyter.cmd.configurePython'; export enum BuiltInCommands { diff --git a/extensions/notebook/src/dialog/managePackages/addNewPackageTab.ts b/extensions/notebook/src/dialog/managePackages/addNewPackageTab.ts new file mode 100644 index 0000000000..147de1b32d --- /dev/null +++ b/extensions/notebook/src/dialog/managePackages/addNewPackageTab.ts @@ -0,0 +1,285 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the Source EULA. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import * as nls from 'vscode-nls'; +import * as azdata from 'azdata'; +import * as request from 'request'; + +import JupyterServerInstallation from '../../jupyter/jupyterServerInstallation'; +import * as utils from '../../common/utils'; +import { ManagePackagesDialog } from './managePackagesDialog'; + +const localize = nls.loadMessageBundle(); + +export class AddNewPackageTab { + private addNewPkgTab: azdata.window.DialogTab; + + private newPackagesSearchBar: azdata.InputBoxComponent; + private packagesSearchButton: azdata.ButtonComponent; + private newPackagesName: azdata.TextComponent; + private newPackagesNameLoader: azdata.LoadingComponent; + private newPackagesVersions: azdata.DropDownComponent; + private newPackagesVersionsLoader: azdata.LoadingComponent; + private newPackagesSummary: azdata.TextComponent; + private newPackagesSummaryLoader: azdata.LoadingComponent; + private packageInstallButton: azdata.ButtonComponent; + + private readonly InvalidTextPlaceholder = localize('managePackages.invalidTextPlaceholder', "N/A"); + + constructor(private dialog: ManagePackagesDialog, private jupyterInstallation: JupyterServerInstallation) { + this.addNewPkgTab = azdata.window.createTab(localize('managePackages.addNewTabTitle', "Add new")); + + this.addNewPkgTab.registerContent(async view => { + this.newPackagesSearchBar = view.modelBuilder.inputBox() + .withProperties({ + placeHolder: localize('managePackages.searchBarPlaceholder', "Search for packages") + }).component(); + + this.packagesSearchButton = view.modelBuilder.button() + .withProperties({ + label: localize('managePackages.searchButtonLabel', "Search"), + width: '80px' + }).component(); + this.packagesSearchButton.onDidClick(() => { + this.loadNewPackageInfo(); + }); + + this.newPackagesName = view.modelBuilder.text().withProperties({ + value: this.InvalidTextPlaceholder + }).component(); + this.newPackagesNameLoader = view.modelBuilder.loadingComponent() + .withItem(this.newPackagesName) + .withProperties({ loading: false }) + .component(); + + this.newPackagesVersions = view.modelBuilder.dropDown().withProperties({ + value: this.InvalidTextPlaceholder, + values: [this.InvalidTextPlaceholder] + }).component(); + this.newPackagesVersionsLoader = view.modelBuilder.loadingComponent() + .withItem(this.newPackagesVersions) + .withProperties({ loading: false }) + .component(); + + this.newPackagesSummary = view.modelBuilder.text().withProperties({ + value: this.InvalidTextPlaceholder + }).component(); + this.newPackagesSummaryLoader = view.modelBuilder.loadingComponent() + .withItem(this.newPackagesSummary) + .withProperties({ loading: false }) + .component(); + + this.packageInstallButton = view.modelBuilder.button().withProperties({ + label: localize('managePackages.installButtonText', "Install"), + enabled: false, + width: '80px' + }).component(); + this.packageInstallButton.onDidClick(() => { + this.doPackageInstall(); + }); + + let formModel = view.modelBuilder.formContainer() + .withFormItems([{ + component: this.newPackagesSearchBar, + title: '' + }, { + component: this.packagesSearchButton, + title: '' + }, { + component: this.newPackagesNameLoader, + title: localize('managePackages.packageNameTitle', "Package Name") + }, { + component: this.newPackagesVersionsLoader, + title: localize('managePackages.packageVersionTitle', "Package Version") + }, { + component: this.newPackagesSummaryLoader, + title: localize('managePackages.packageSummaryTitle', "Package Summary") + }, { + component: this.packageInstallButton, + title: '' + }]).component(); + + await view.initializeModel(formModel); + }); + } + + public get tab(): azdata.window.DialogTab { + return this.addNewPkgTab; + } + + private async toggleNewPackagesFields(enable: boolean): Promise { + await this.packagesSearchButton.updateProperties({ enabled: enable }); + await this.newPackagesNameLoader.updateProperties({ loading: !enable }); + await this.newPackagesVersionsLoader.updateProperties({ loading: !enable }); + await this.newPackagesSummaryLoader.updateProperties({ loading: !enable }); + } + + private async loadNewPackageInfo(): Promise { + await this.packageInstallButton.updateProperties({ enabled: false }); + await this.toggleNewPackagesFields(false); + try { + let packageName = this.newPackagesSearchBar.value; + if (!packageName || packageName.length === 0) { + return; + } + + let pipPackage = await this.fetchPypiPackage(packageName); + if (!pipPackage.versions || pipPackage.versions.length === 0) { + this.dialog.showErrorMessage( + localize('managePackages.noVersionsFound', + "Could not find any valid versions for the specified package")); + return; + } + + await this.newPackagesName.updateProperties({ + value: packageName + }); + await this.newPackagesVersions.updateProperties({ + values: pipPackage.versions, + value: pipPackage.versions[0], + }); + await this.newPackagesSummary.updateProperties({ + value: pipPackage.summary + }); + + // Only re-enable install on success + await this.packageInstallButton.updateProperties({ enabled: true }); + } catch (err) { + this.dialog.showErrorMessage(utils.getErrorMessage(err)); + + await this.newPackagesName.updateProperties({ + value: this.InvalidTextPlaceholder + }); + await this.newPackagesVersions.updateProperties({ + value: this.InvalidTextPlaceholder, + values: [this.InvalidTextPlaceholder], + }); + await this.newPackagesSummary.updateProperties({ + value: this.InvalidTextPlaceholder + }); + } finally { + await this.toggleNewPackagesFields(true); + } + } + + private async fetchPypiPackage(packageName: string): Promise { + return new Promise((resolve, reject) => { + request.get(`https://pypi.org/pypi/${packageName}/json`, { timeout: 10000 }, (error, response, body) => { + if (error) { + return reject(error); + } + + if (response.statusCode === 404) { + return reject(localize('managePackages.packageNotFound', "Could not find the specified package")); + } + + if (response.statusCode !== 200) { + return reject( + localize('managePackages.packageRequestError', + "Package info request failed with error: {0} {1}", + response.statusCode, + response.statusMessage)); + } + + let versionNums: string[] = []; + let packageSummary = ''; + + let packagesJson = JSON.parse(body); + if (packagesJson) { + if (packagesJson.releases) { + let versionKeys = Object.keys(packagesJson.releases); + versionKeys = versionKeys.filter(versionKey => { + let releaseInfo = packagesJson.releases[versionKey]; + return Array.isArray(releaseInfo) && releaseInfo.length > 0; + }); + versionKeys.sort((first, second) => { + // sort in descending order + let firstVersion = first.split('.').map(numStr => Number.parseInt(numStr)); + let secondVersion = second.split('.').map(numStr => Number.parseInt(numStr)); + + // If versions have different lengths, then append zeroes to the shorter one + if (firstVersion.length > secondVersion.length) { + let diff = firstVersion.length - secondVersion.length; + secondVersion = secondVersion.concat(new Array(diff).fill(0)); + } else if (secondVersion.length > firstVersion.length) { + let diff = secondVersion.length - firstVersion.length; + firstVersion = firstVersion.concat(new Array(diff).fill(0)); + } + + for (let i = 0; i < firstVersion.length; ++i) { + if (firstVersion[i] > secondVersion[i]) { + return -1; + } else if (firstVersion[i] < secondVersion[i]) { + return 1; + } + } + return 0; + }); + versionNums = versionKeys; + } + + if (packagesJson.info && packagesJson.info.summary) { + packageSummary = packagesJson.info.summary; + } + } + + resolve({ + name: packageName, + versions: versionNums, + summary: packageSummary + }); + }); + }); + } + + private async doPackageInstall(): Promise { + let packageName = this.newPackagesName.value; + let packageVersion = this.newPackagesVersions.value as string; + if (!packageName || packageName.length === 0 || + !packageVersion || packageVersion.length === 0) { + return; + } + + let taskName = localize('managePackages.backgroundInstallStarted', + "Installing {0} {1}", + packageName, + packageVersion); + this.jupyterInstallation.apiWrapper.startBackgroundOperation({ + displayName: taskName, + description: taskName, + isCancelable: false, + operation: op => { + this.jupyterInstallation.installPipPackage(packageName, packageVersion) + .then(async () => { + let installMsg = localize('managePackages.backgroundInstallComplete', + "Completed install for {0} {1}", + packageName, + packageVersion); + + op.updateStatus(azdata.TaskStatus.Succeeded, installMsg); + this.jupyterInstallation.outputChannel.appendLine(installMsg); + + await this.dialog.refreshInstalledPackages(); + }) + .catch(err => { + let installFailedMsg = localize('managePackages.backgroundInstallFailed', + "Failed to install {0} {1}. Error: {2}", + packageName, + packageVersion, + utils.getErrorMessage(err)); + + op.updateStatus(azdata.TaskStatus.Failed, installFailedMsg); + this.jupyterInstallation.outputChannel.appendLine(installFailedMsg); + }); + } + }); + } +} + +interface PipPackageData { + name: string; + versions: string[]; + summary: string; +} \ No newline at end of file diff --git a/extensions/notebook/src/dialog/managePackages/installedPackagesTab.ts b/extensions/notebook/src/dialog/managePackages/installedPackagesTab.ts new file mode 100644 index 0000000000..66c2df6f60 --- /dev/null +++ b/extensions/notebook/src/dialog/managePackages/installedPackagesTab.ts @@ -0,0 +1,193 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the Source EULA. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import * as nls from 'vscode-nls'; +import * as azdata from 'azdata'; + +import JupyterServerInstallation, { PythonPkgDetails } from '../../jupyter/jupyterServerInstallation'; +import * as utils from '../../common/utils'; +import { ManagePackagesDialog } from './managePackagesDialog'; +import CodeAdapter from '../../prompts/adapter'; +import { QuestionTypes, IQuestion } from '../../prompts/question'; + +const localize = nls.loadMessageBundle(); + +export class InstalledPackagesTab { + private prompter: CodeAdapter; + + private installedPkgTab: azdata.window.DialogTab; + + private installedPackageCount: azdata.TextComponent; + private installedPackagesTable: azdata.TableComponent; + private installedPackagesLoader: azdata.LoadingComponent; + private uninstallPackageButton: azdata.ButtonComponent; + + constructor(private dialog: ManagePackagesDialog, private jupyterInstallation: JupyterServerInstallation) { + this.prompter = new CodeAdapter(); + + this.installedPkgTab = azdata.window.createTab(localize('managePackages.installedTabTitle', "Installed")); + + this.installedPkgTab.registerContent(async view => { + this.installedPackageCount = view.modelBuilder.text().withProperties({ + value: '' + }).component(); + + this.installedPackagesTable = view.modelBuilder.table() + .withProperties({ + columns: [ + localize('managePackages.pkgNameColumn', "Name"), + localize('managePackages.newPkgVersionColumn', "Version") + ], + data: [[]], + height: '600px', + width: '400px' + }).component(); + + this.uninstallPackageButton = view.modelBuilder.button() + .withProperties({ + label: localize('managePackages.uninstallButtonText', "Uninstall selected packages"), + width: '200px' + }).component(); + this.uninstallPackageButton.onDidClick(() => this.doUninstallPackage()); + + let formModel = view.modelBuilder.formContainer() + .withFormItems([{ + component: this.installedPackageCount, + title: '' + }, { + component: this.installedPackagesTable, + title: '' + }, { + component: this.uninstallPackageButton, + title: '' + }]).component(); + + this.installedPackagesLoader = view.modelBuilder.loadingComponent() + .withItem(formModel) + .withProperties({ + loading: true + }).component(); + + await view.initializeModel(this.installedPackagesLoader); + + await this.loadInstalledPackagesInfo(); + }); + } + + public get tab(): azdata.window.DialogTab { + return this.installedPkgTab; + } + + public async loadInstalledPackagesInfo(): Promise { + let pythonPackages: PythonPkgDetails[]; + let packagesLocation: string; + + await this.installedPackagesLoader.updateProperties({ loading: true }); + await this.uninstallPackageButton.updateProperties({ enabled: false }); + try { + pythonPackages = await this.jupyterInstallation.getInstalledPipPackages(); + packagesLocation = await this.jupyterInstallation.getPythonPackagesPath(); + } catch (err) { + this.dialog.showErrorMessage(utils.getErrorMessage(err)); + } finally { + await this.installedPackagesLoader.updateProperties({ loading: false }); + } + + let packageData: string[][]; + let packageCount: number; + if (pythonPackages) { + packageCount = pythonPackages.length; + packageData = pythonPackages.map(pkg => [pkg.name, pkg.version]); + } else { + packageCount = 0; + } + + let countMsg: string; + if (packagesLocation && packagesLocation.length > 0) { + countMsg = localize('managePackages.packageCount', "{0} packages found in '{1}'", + packageCount, + packagesLocation); + } else { + countMsg = localize('managePackages.packageCountNoPath', "{0} packages found", + packageCount); + } + await this.installedPackageCount.updateProperties({ + value: countMsg + }); + + if (packageData && packageData.length > 0) { + await this.installedPackagesTable.updateProperties({ + data: packageData, + selectedRows: [0] + }); + await this.uninstallPackageButton.updateProperties({ enabled: true }); + } + } + + private async doUninstallPackage(): Promise { + let rowNums = this.installedPackagesTable.selectedRows; + if (!rowNums || rowNums.length === 0) { + return; + } + + this.uninstallPackageButton.updateProperties({ enabled: false }); + let doUninstall = await this.prompter.promptSingle({ + type: QuestionTypes.confirm, + message: localize('managePackages.confirmUninstall', 'Are you sure you want to uninstall the specified packages?'), + default: false + }); + + if (doUninstall) { + try { + let packages: PythonPkgDetails[] = rowNums.map(rowNum => { + let row = this.installedPackagesTable.data[rowNum]; + return { + name: row[0], + version: row[1] + }; + }); + + let packagesStr = packages.map(pkg => { + return `${pkg.name} ${pkg.version}`; + }).join(', '); + let taskName = localize('managePackages.backgroundUninstallStarted', + "Uninstalling {0}", + packagesStr); + + this.jupyterInstallation.apiWrapper.startBackgroundOperation({ + displayName: taskName, + description: taskName, + isCancelable: false, + operation: op => { + this.jupyterInstallation.uninstallPipPackages(packages) + .then(async () => { + let uninstallMsg = localize('managePackages.backgroundUninstallComplete', + "Completed uninstall for {0}", + packagesStr); + + op.updateStatus(azdata.TaskStatus.Succeeded, uninstallMsg); + this.jupyterInstallation.outputChannel.appendLine(uninstallMsg); + + await this.loadInstalledPackagesInfo(); + }) + .catch(err => { + let uninstallFailedMsg = localize('managePackages.backgroundUninstallFailed', + "Failed to uninstall {0}. Error: {1}", + packagesStr, + utils.getErrorMessage(err)); + + op.updateStatus(azdata.TaskStatus.Failed, uninstallFailedMsg); + this.jupyterInstallation.outputChannel.appendLine(uninstallFailedMsg); + }); + } + }); + } catch (err) { + this.dialog.showErrorMessage(utils.getErrorMessage(err)); + } + } + + this.uninstallPackageButton.updateProperties({ enabled: true }); + } +} \ No newline at end of file diff --git a/extensions/notebook/src/dialog/managePackages/managePackagesDialog.ts b/extensions/notebook/src/dialog/managePackages/managePackagesDialog.ts new file mode 100644 index 0000000000..4e828af338 --- /dev/null +++ b/extensions/notebook/src/dialog/managePackages/managePackagesDialog.ts @@ -0,0 +1,57 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the Source EULA. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import * as nls from 'vscode-nls'; +import * as azdata from 'azdata'; + +import JupyterServerInstallation from '../../jupyter/jupyterServerInstallation'; +import { InstalledPackagesTab } from './installedPackagesTab'; +import { AddNewPackageTab } from './addNewPackageTab'; + +const localize = nls.loadMessageBundle(); + +export class ManagePackagesDialog { + private dialog: azdata.window.Dialog; + private installedPkgTab: InstalledPackagesTab; + private addNewPkgTab: AddNewPackageTab; + + constructor(private jupyterInstallation: JupyterServerInstallation) { + } + + /** + * Opens a dialog to manage packages used by notebooks. + */ + public showDialog(): void { + this.dialog = azdata.window.createModelViewDialog(localize('managePackages.dialogName', "Manage Packages")); + + this.installedPkgTab = new InstalledPackagesTab(this, this.jupyterInstallation); + this.addNewPkgTab = new AddNewPackageTab(this, this.jupyterInstallation); + + this.dialog.okButton.hidden = true; + this.dialog.cancelButton.label = localize('managePackages.cancelButtonText', "Close"); + + this.dialog.content = [this.installedPkgTab.tab, this.addNewPkgTab.tab]; + + azdata.window.openDialog(this.dialog); + } + + public refreshInstalledPackages(): Promise { + return this.installedPkgTab.loadInstalledPackagesInfo(); + } + + public showInfoMessage(message: string): void { + this.dialog.message = { + text: message, + level: azdata.window.MessageLevel.Information + }; + } + + public showErrorMessage(message: string): void { + this.dialog.message = { + text: message, + level: azdata.window.MessageLevel.Error + }; + } +} \ No newline at end of file diff --git a/extensions/notebook/src/integrationTest/notebookIntegration.test.ts b/extensions/notebook/src/integrationTest/notebookIntegration.test.ts index 1c8f605674..b8c831eebb 100644 --- a/extensions/notebook/src/integrationTest/notebookIntegration.test.ts +++ b/extensions/notebook/src/integrationTest/notebookIntegration.test.ts @@ -10,7 +10,7 @@ import * as path from 'path'; import 'mocha'; import { JupyterController } from '../jupyter/jupyterController'; -import JupyterServerInstallation from '../jupyter/jupyterServerInstallation'; +import JupyterServerInstallation, { PythonPkgDetails } from '../jupyter/jupyterServerInstallation'; import { pythonBundleVersion } from '../common/constants'; import { executeStreamedCommand } from '../common/utils'; @@ -80,4 +80,28 @@ describe('Notebook Extension Python Installation', function () { should(JupyterServerInstallation.getExistingPythonSetting(apiWrapper)).be.false(); console.log('Existing Python Installation is done'); }); + + it('Python Install Utilities Test', async function () { + let install = jupyterController.jupyterInstallation; + + let packagePath = await install.getPythonPackagesPath(); + should(packagePath).not.be.undefined(); + should(packagePath.length).be.greaterThan(0); + + let testPkg = 'pandas'; + let testPkgVersion = '0.24.2'; + let expectedPkg: PythonPkgDetails = { name: testPkg, version: testPkgVersion }; + + await install.installPipPackage(testPkg, testPkgVersion); + let packages = await install.getInstalledPipPackages(); + should(packages).containEql(expectedPkg); + + await install.uninstallPipPackages([{ name: testPkg, version: testPkgVersion}]); + packages = await install.getInstalledPipPackages(); + should(packages).not.containEql(expectedPkg); + + await install.installPipPackage(testPkg, testPkgVersion); + packages = await install.getInstalledPipPackages(); + should(packages).containEql(expectedPkg); + }); }); diff --git a/extensions/notebook/src/jupyter/jupyterController.ts b/extensions/notebook/src/jupyter/jupyterController.ts index a6ac2f71e2..4222a1fdff 100644 --- a/extensions/notebook/src/jupyter/jupyterController.ts +++ b/extensions/notebook/src/jupyter/jupyterController.ts @@ -24,6 +24,7 @@ import { NotebookCompletionItemProvider } from '../intellisense/completionItemPr import { JupyterNotebookProvider } from './jupyterNotebookProvider'; import { ConfigurePythonDialog } from '../dialog/configurePythonDialog'; import CodeAdapter from '../prompts/adapter'; +import { ManagePackagesDialog } from '../dialog/managePackages/managePackagesDialog'; let untitledCounter = 0; @@ -73,7 +74,7 @@ export class JupyterController implements vscode.Disposable { }); this.apiWrapper.registerCommand(constants.jupyterReinstallDependenciesCommand, () => { return this.handleDependenciesReinstallation(); }); - this.apiWrapper.registerCommand(constants.jupyterInstallPackages, () => { return this.doManagePackages(); }); + this.apiWrapper.registerCommand(constants.jupyterManagePackages, () => { return this.doManagePackages(); }); this.apiWrapper.registerCommand(constants.jupyterConfigurePython, () => { return this.doConfigurePython(this._jupyterInstallation); }); let supportedFileFilter: vscode.DocumentFilter[] = [ @@ -194,10 +195,8 @@ export class JupyterController implements vscode.Disposable { public doManagePackages(): void { try { - let terminal = this.apiWrapper.createTerminalWithOptions({ cwd: this.getPythonBinDir() }); - terminal.show(true); - let shellType = this.apiWrapper.getConfiguration().get('terminal.integrated.shell.windows'); - terminal.sendText(this.getTextToSendToTerminal(shellType), true); + let packagesDialog = new ManagePackagesDialog(this._jupyterInstallation); + packagesDialog.showDialog(); } catch (error) { let message = utils.getErrorMessage(error); this.apiWrapper.showErrorMessage(message); @@ -225,10 +224,6 @@ export class JupyterController implements vscode.Disposable { } } - private getPythonBinDir(): string { - return JupyterServerInstallation.getPythonBinPath(this.apiWrapper); - } - public get jupyterInstallation() { return this._jupyterInstallation; } diff --git a/extensions/notebook/src/jupyter/jupyterServerInstallation.ts b/extensions/notebook/src/jupyter/jupyterServerInstallation.ts index 2a03800f9d..dcd6201c5c 100644 --- a/extensions/notebook/src/jupyter/jupyterServerInstallation.ts +++ b/extensions/notebook/src/jupyter/jupyterServerInstallation.ts @@ -347,6 +347,28 @@ export default class JupyterServerInstallation { } } + public async getInstalledPipPackages(): Promise { + let cmd = `"${this.pythonExecutable}" -m pip list --format=json`; + let packagesInfo = await this.executeBufferedCommand(cmd); + + let packagesResult: PythonPkgDetails[] = []; + if (packagesInfo) { + packagesResult = JSON.parse(packagesInfo); + } + return packagesResult; + } + + public installPipPackage(packageName: string, version: string): Promise { + let cmd = `"${this.pythonExecutable}" -m pip install ${packageName}==${version}`; + return this.executeStreamedCommand(cmd); + } + + public uninstallPipPackages(packages: PythonPkgDetails[]): Promise { + let packagesStr = packages.map(pkg => `${pkg.name}==${pkg.version}`).join(' '); + let cmd = `"${this.pythonExecutable}" -m pip uninstall -y ${packagesStr}`; + return this.executeStreamedCommand(cmd); + } + private async installOfflinePipPackages(): Promise { let installJupyterCommand: string; if (process.platform === constants.winPlatform) { @@ -357,7 +379,7 @@ export default class JupyterServerInstallation { if (installJupyterCommand) { this.outputChannel.show(true); this.outputChannel.appendLine(localize('msgInstallStart', "Installing required packages to run Notebooks...")); - await this.executeCommand(installJupyterCommand); + await this.executeStreamedCommand(installJupyterCommand); this.outputChannel.appendLine(localize('msgJupyterInstallDone', "... Jupyter installation complete.")); } else { return Promise.resolve(); @@ -378,7 +400,7 @@ export default class JupyterServerInstallation { if (installSparkMagic) { this.outputChannel.show(true); this.outputChannel.appendLine(localize('msgInstallingSpark', "Installing SparkMagic...")); - await this.executeCommand(installSparkMagic); + await this.executeStreamedCommand(installSparkMagic); } } @@ -387,10 +409,10 @@ export default class JupyterServerInstallation { this.outputChannel.appendLine(localize('msgInstallStart', "Installing required packages to run Notebooks...")); let installCommand = `"${this._pythonExecutable}" -m pip install jupyter==1.0.0 pandas==0.24.2`; - await this.executeCommand(installCommand); + await this.executeStreamedCommand(installCommand); installCommand = `"${this._pythonExecutable}" -m pip install prose-codeaccelerator==1.3.0 --extra-index-url https://prose-python-packages.azurewebsites.net`; - await this.executeCommand(installCommand); + await this.executeStreamedCommand(installCommand); this.outputChannel.appendLine(localize('msgJupyterInstallDone', "... Jupyter installation complete.")); } @@ -403,18 +425,22 @@ export default class JupyterServerInstallation { if (process.platform !== constants.winPlatform) { installCommand = `${installCommand} pykerberos==1.2.1`; } - await this.executeCommand(installCommand); + await this.executeStreamedCommand(installCommand); installCommand = `"${this._pythonExecutable}" -m pip install prose-codeaccelerator==1.3.0 --extra-index-url https://prose-python-packages.azurewebsites.net`; - await this.executeCommand(installCommand); + await this.executeStreamedCommand(installCommand); this.outputChannel.appendLine(localize('msgJupyterInstallDone', "... Jupyter installation complete.")); } - private async executeCommand(command: string): Promise { + private async executeStreamedCommand(command: string): Promise { await utils.executeStreamedCommand(command, { env: this.execOptions.env }, this.outputChannel); } + private async executeBufferedCommand(command: string): Promise { + return await utils.executeBufferedCommand(command, { env: this.execOptions.env }); + } + public get pythonExecutable(): string { return this._pythonExecutable; } @@ -500,10 +526,20 @@ export default class JupyterServerInstallation { pythonBinPathSuffix); } + public async getPythonPackagesPath(): Promise { + let cmd = `"${this.pythonExecutable}" -c "import site; print(site.getsitepackages()[0])"`; + return await this.executeBufferedCommand(cmd); + } + public static getPythonExePath(pythonInstallPath: string, useExistingInstall: boolean): string { return path.join( pythonInstallPath, useExistingInstall ? '' : constants.pythonBundleVersion, process.platform === constants.winPlatform ? 'python.exe' : 'bin/python3'); } +} + +export interface PythonPkgDetails { + name: string; + version: string; } \ No newline at end of file