diff --git a/extensions/notebook/src/common/constants.ts b/extensions/notebook/src/common/constants.ts index e16101d7d9..b7d6a9f765 100644 --- a/extensions/notebook/src/common/constants.ts +++ b/extensions/notebook/src/common/constants.ts @@ -36,6 +36,11 @@ export enum CommandContext { NotebookPythonInstalled = 'notebook:pythonInstalled' } +export enum PythonPkgType { + Pip = 'Pip', + Anaconda = 'Anaconda' +} + export const pythonOfflinePipPackagesUrl = 'https://go.microsoft.com/fwlink/?linkid=2092867'; export const pythonWindowsInstallUrl = 'https://go.microsoft.com/fwlink/?linkid=2092866'; export const pythonMacInstallUrl = 'https://go.microsoft.com/fwlink/?linkid=2092865'; diff --git a/extensions/notebook/src/common/localizedConstants.ts b/extensions/notebook/src/common/localizedConstants.ts index 198df92b38..d626e4a9e8 100644 --- a/extensions/notebook/src/common/localizedConstants.ts +++ b/extensions/notebook/src/common/localizedConstants.ts @@ -13,7 +13,4 @@ export const msgYes = localize('msgYes', 'Yes'); export const msgNo = localize('msgNo', 'No'); // Jupyter Constants /////////////////////////////////////////////////////// -export const msgManagePackagesPowershell = localize('msgManagePackagesPowershell', '<#\n--------------------------------------------------------------------------------\n\tThis is the sandboxed instance of python used by Jupyter server.\n\tTo install packages used by the python kernel use \'.\\python.exe -m pip install\'\n--------------------------------------------------------------------------------\n#>'); -export const msgManagePackagesBash = localize('msgJupyterManagePackagesBash', ': \'\n--------------------------------------------------------------------------------\n\tThis is the sandboxed instance of python used by Jupyter server.\n\tTo install packages used by the python kernel use \'./python3 -m pip install\'\n--------------------------------------------------------------------------------\n\''); -export const msgManagePackagesCmd = localize('msgJupyterManagePackagesCmd', 'REM This is the sandboxed instance of python used by Jupyter server. To install packages used by the python kernel use \'.\\python.exe -m pip install\''); export const msgSampleCodeDataFrame = localize('msgSampleCodeDataFrame', 'This sample code loads the file into a data frame and shows the first 10 results.'); diff --git a/extensions/notebook/src/dialog/configurePythonDialog.ts b/extensions/notebook/src/dialog/configurePythonDialog.ts index 3cc505c40d..73c213a2ec 100644 --- a/extensions/notebook/src/dialog/configurePythonDialog.ts +++ b/extensions/notebook/src/dialog/configurePythonDialog.ts @@ -9,7 +9,7 @@ import * as azdata from 'azdata'; import * as fs from 'fs'; import * as utils from '../common/utils'; -import JupyterServerInstallation from '../jupyter/jupyterServerInstallation'; +import { JupyterServerInstallation } from '../jupyter/jupyterServerInstallation'; import { ApiWrapper } from '../common/apiWrapper'; import { Deferred } from '../common/promise'; import { PythonPathLookup, PythonPathInfo } from './pythonPathLookup'; diff --git a/extensions/notebook/src/dialog/managePackages/addNewPackageTab.ts b/extensions/notebook/src/dialog/managePackages/addNewPackageTab.ts index 147de1b32d..2e253c7aa5 100644 --- a/extensions/notebook/src/dialog/managePackages/addNewPackageTab.ts +++ b/extensions/notebook/src/dialog/managePackages/addNewPackageTab.ts @@ -7,9 +7,10 @@ import * as nls from 'vscode-nls'; import * as azdata from 'azdata'; import * as request from 'request'; -import JupyterServerInstallation from '../../jupyter/jupyterServerInstallation'; +import { JupyterServerInstallation, PipPackageOverview } from '../../jupyter/jupyterServerInstallation'; import * as utils from '../../common/utils'; import { ManagePackagesDialog } from './managePackagesDialog'; +import { PythonPkgType } from '../../common/constants'; const localize = nls.loadMessageBundle(); @@ -27,15 +28,14 @@ export class AddNewPackageTab { private packageInstallButton: azdata.ButtonComponent; private readonly InvalidTextPlaceholder = localize('managePackages.invalidTextPlaceholder', "N/A"); + private readonly PackageNotFoundError = localize('managePackages.packageNotFound', "Could not find the specified package"); + private readonly SearchPlaceholder = (pkgType: string) => localize('managePackages.searchBarPlaceholder', "Search {0} packages", pkgType); 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.newPackagesSearchBar = view.modelBuilder.inputBox().component(); this.packagesSearchButton = view.modelBuilder.button() .withProperties({ @@ -46,34 +46,23 @@ export class AddNewPackageTab { this.loadNewPackageInfo(); }); - this.newPackagesName = view.modelBuilder.text().withProperties({ - value: this.InvalidTextPlaceholder - }).component(); + this.newPackagesName = view.modelBuilder.text().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.newPackagesVersions = view.modelBuilder.dropDown().component(); this.newPackagesVersionsLoader = view.modelBuilder.loadingComponent() .withItem(this.newPackagesVersions) - .withProperties({ loading: false }) .component(); - this.newPackagesSummary = view.modelBuilder.text().withProperties({ - value: this.InvalidTextPlaceholder - }).component(); + this.newPackagesSummary = view.modelBuilder.text().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(() => { @@ -102,6 +91,8 @@ export class AddNewPackageTab { }]).component(); await view.initializeModel(formModel); + + await this.resetPageFields(); }); } @@ -109,6 +100,26 @@ export class AddNewPackageTab { return this.addNewPkgTab; } + public async resetPageFields(): Promise { + await this.toggleNewPackagesFields(false); + try { + await this.packageInstallButton.updateProperties({ enabled: false }); + + await this.newPackagesSearchBar.updateProperties({ + value: '', + placeHolder: this.SearchPlaceholder(this.dialog.currentPkgType) + }); + await this.newPackagesName.updateProperties({ value: this.InvalidTextPlaceholder }); + await this.newPackagesVersions.updateProperties({ + values: [this.InvalidTextPlaceholder], + value: this.InvalidTextPlaceholder + }); + await this.newPackagesSummary.updateProperties({ value: this.InvalidTextPlaceholder }); + } finally { + await this.toggleNewPackagesFields(true); + } + } + private async toggleNewPackagesFields(enable: boolean): Promise { await this.packagesSearchButton.updateProperties({ enabled: enable }); await this.newPackagesNameLoader.updateProperties({ loading: !enable }); @@ -125,7 +136,12 @@ export class AddNewPackageTab { return; } - let pipPackage = await this.fetchPypiPackage(packageName); + let pipPackage: PipPackageOverview; + if (this.dialog.currentPkgType === PythonPkgType.Anaconda) { + pipPackage = await this.fetchCondaPackage(packageName); + } else { + pipPackage = await this.fetchPypiPackage(packageName); + } if (!pipPackage.versions || pipPackage.versions.length === 0) { this.dialog.showErrorMessage( localize('managePackages.noVersionsFound', @@ -164,15 +180,15 @@ export class AddNewPackageTab { } } - private async fetchPypiPackage(packageName: string): Promise { - return new Promise((resolve, reject) => { + 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")); + return reject(this.PackageNotFoundError); } if (response.statusCode !== 200) { @@ -194,30 +210,7 @@ export class AddNewPackageTab { 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; + versionNums = AddNewPackageTab.sortPackageVersions(versionKeys); } if (packagesJson.info && packagesJson.info.summary) { @@ -234,6 +227,66 @@ export class AddNewPackageTab { }); } + private async fetchCondaPackage(packageName: string): Promise { + let condaExe = this.jupyterInstallation.getCondaExePath(); + let cmd = `"${condaExe}" search --json ${packageName}`; + let packageResult: string; + try { + packageResult = await this.jupyterInstallation.executeBufferedCommand(cmd); + } catch (err) { + throw new Error(this.PackageNotFoundError); + } + + if (packageResult) { + let packageJson = JSON.parse(packageResult); + if (packageJson) { + if (packageJson.error) { + throw new Error(packageJson.error); + } + + let packages = packageJson[packageName]; + if (Array.isArray(packages)) { + let allVersions = packages.filter(pkg => pkg && pkg.version).map(pkg => pkg.version); + let singletonVersions = new Set(allVersions); + let sortedVersions = AddNewPackageTab.sortPackageVersions(Array.from(singletonVersions)); + return { + name: packageName, + versions: sortedVersions, + summary: undefined + }; + } + } + } + + return undefined; + } + + public static sortPackageVersions(versions: string[]): string[] { + return versions.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; + }); + } + private async doPackageInstall(): Promise { let packageName = this.newPackagesName.value; let packageVersion = this.newPackagesVersions.value as string; @@ -251,7 +304,13 @@ export class AddNewPackageTab { description: taskName, isCancelable: false, operation: op => { - this.jupyterInstallation.installPipPackage(packageName, packageVersion) + let installPromise: Promise; + if (this.dialog.currentPkgType === PythonPkgType.Anaconda) { + installPromise = this.jupyterInstallation.installCondaPackage(packageName, packageVersion); + } else { + installPromise = this.jupyterInstallation.installPipPackage(packageName, packageVersion); + } + installPromise .then(async () => { let installMsg = localize('managePackages.backgroundInstallComplete', "Completed install for {0} {1}", @@ -276,10 +335,4 @@ export class AddNewPackageTab { } }); } -} - -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 index 66c2df6f60..791de08284 100644 --- a/extensions/notebook/src/dialog/managePackages/installedPackagesTab.ts +++ b/extensions/notebook/src/dialog/managePackages/installedPackagesTab.ts @@ -6,11 +6,12 @@ import * as nls from 'vscode-nls'; import * as azdata from 'azdata'; -import JupyterServerInstallation, { PythonPkgDetails } from '../../jupyter/jupyterServerInstallation'; +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'; +import { PythonPkgType } from '../../common/constants'; const localize = nls.loadMessageBundle(); @@ -19,6 +20,7 @@ export class InstalledPackagesTab { private installedPkgTab: azdata.window.DialogTab; + private packageTypeDropdown: azdata.DropDownComponent; private installedPackageCount: azdata.TextComponent; private installedPackagesTable: azdata.TableComponent; private installedPackagesLoader: azdata.LoadingComponent; @@ -30,6 +32,23 @@ export class InstalledPackagesTab { this.installedPkgTab = azdata.window.createTab(localize('managePackages.installedTabTitle', "Installed")); this.installedPkgTab.registerContent(async view => { + let dropdownValues: string[]; + if (this.dialog.currentPkgType === PythonPkgType.Anaconda) { + dropdownValues = [PythonPkgType.Anaconda, PythonPkgType.Pip]; + } else { + dropdownValues = [PythonPkgType.Pip]; + } + this.packageTypeDropdown = view.modelBuilder.dropDown().withProperties({ + values: dropdownValues, + value: dropdownValues[0] + }).component(); + this.packageTypeDropdown.onValueChanged(() => { + this.dialog.resetPages(this.packageTypeDropdown.value as PythonPkgType) + .catch(err => { + this.dialog.showErrorMessage(utils.getErrorMessage(err)); + }); + }); + this.installedPackageCount = view.modelBuilder.text().withProperties({ value: '' }).component(); @@ -54,6 +73,9 @@ export class InstalledPackagesTab { let formModel = view.modelBuilder.formContainer() .withFormItems([{ + component: this.packageTypeDropdown, + title: localize('managePackages.packageType', "Package Type") + }, { component: this.installedPackageCount, title: '' }, { @@ -82,13 +104,15 @@ export class InstalledPackagesTab { 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(); + if (this.dialog.currentPkgType === PythonPkgType.Anaconda) { + pythonPackages = await this.jupyterInstallation.getInstalledCondaPackages(); + } else { + pythonPackages = await this.jupyterInstallation.getInstalledPipPackages(); + } } catch (err) { this.dialog.showErrorMessage(utils.getErrorMessage(err)); } finally { @@ -104,17 +128,10 @@ export class InstalledPackagesTab { 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 + value: localize('managePackages.packageCount', "{0} {1} packages found", + packageCount, + this.dialog.currentPkgType) }); if (packageData && packageData.length > 0) { @@ -161,7 +178,13 @@ export class InstalledPackagesTab { description: taskName, isCancelable: false, operation: op => { - this.jupyterInstallation.uninstallPipPackages(packages) + let uninstallPromise: Promise; + if (this.dialog.currentPkgType === PythonPkgType.Anaconda) { + uninstallPromise = this.jupyterInstallation.uninstallCondaPackages(packages); + } else { + uninstallPromise = this.jupyterInstallation.uninstallPipPackages(packages); + } + uninstallPromise .then(async () => { let uninstallMsg = localize('managePackages.backgroundUninstallComplete', "Completed uninstall for {0}", diff --git a/extensions/notebook/src/dialog/managePackages/managePackagesDialog.ts b/extensions/notebook/src/dialog/managePackages/managePackagesDialog.ts index 4e828af338..e7dfb9d958 100644 --- a/extensions/notebook/src/dialog/managePackages/managePackagesDialog.ts +++ b/extensions/notebook/src/dialog/managePackages/managePackagesDialog.ts @@ -6,9 +6,10 @@ import * as nls from 'vscode-nls'; import * as azdata from 'azdata'; -import JupyterServerInstallation from '../../jupyter/jupyterServerInstallation'; +import { JupyterServerInstallation } from '../../jupyter/jupyterServerInstallation'; import { InstalledPackagesTab } from './installedPackagesTab'; import { AddNewPackageTab } from './addNewPackageTab'; +import { PythonPkgType } from '../../common/constants'; const localize = nls.loadMessageBundle(); @@ -17,7 +18,10 @@ export class ManagePackagesDialog { private installedPkgTab: InstalledPackagesTab; private addNewPkgTab: AddNewPackageTab; + public currentPkgType: PythonPkgType; + constructor(private jupyterInstallation: JupyterServerInstallation) { + this.currentPkgType = this.jupyterInstallation.usingConda ? PythonPkgType.Anaconda : PythonPkgType.Pip; } /** @@ -41,6 +45,12 @@ export class ManagePackagesDialog { return this.installedPkgTab.loadInstalledPackagesInfo(); } + public async resetPages(newPkgType: PythonPkgType): Promise { + this.currentPkgType = newPkgType; + await this.installedPkgTab.loadInstalledPackagesInfo(); + await this.addNewPkgTab.resetPageFields(); + } + public showInfoMessage(message: string): void { this.dialog.message = { text: message, diff --git a/extensions/notebook/src/integrationTest/notebookIntegration.test.ts b/extensions/notebook/src/integrationTest/notebookIntegration.test.ts index b8c831eebb..8b43bb67ba 100644 --- a/extensions/notebook/src/integrationTest/notebookIntegration.test.ts +++ b/extensions/notebook/src/integrationTest/notebookIntegration.test.ts @@ -10,9 +10,10 @@ import * as path from 'path'; import 'mocha'; import { JupyterController } from '../jupyter/jupyterController'; -import JupyterServerInstallation, { PythonPkgDetails } from '../jupyter/jupyterServerInstallation'; +import { JupyterServerInstallation, PythonPkgDetails } from '../jupyter/jupyterServerInstallation'; import { pythonBundleVersion } from '../common/constants'; import { executeStreamedCommand } from '../common/utils'; +import { AddNewPackageTab } from '../dialog/managePackages/addNewPackageTab'; describe('Notebook Extension Python Installation', function () { this.timeout(600000); @@ -20,6 +21,7 @@ describe('Notebook Extension Python Installation', function () { let installComplete = false; let pythonInstallDir = process.env.PYTHON_TEST_PATH; let jupyterController: JupyterController; + before(async function () { assert.ok(pythonInstallDir, 'Python install directory was not defined.'); @@ -81,13 +83,9 @@ describe('Notebook Extension Python Installation', function () { console.log('Existing Python Installation is done'); }); - it('Python Install Utilities Test', async function () { + it('Pip 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 }; @@ -104,4 +102,26 @@ describe('Notebook Extension Python Installation', function () { packages = await install.getInstalledPipPackages(); should(packages).containEql(expectedPkg); }); + + it('Conda Install Utilities Test', async function () { + let install = jupyterController.jupyterInstallation; + + // Anaconda is not included in our Python package, so all + // the conda utilities should fail. + should(install.usingConda).be.false(); + + should(install.getInstalledCondaPackages()).be.rejected(); + + should(install.installCondaPackage('pandas', '0.24.2')).be.rejected(); + + should(install.uninstallCondaPackages([{ name: 'pandas', version: '0.24.2' }])).be.rejected(); + }); + + it('Manage Packages Dialog: New Package Test', async function () { + let testVersions = ['1.0.0', '1.1', '0.0.0.9', '0.0.5', '100', '0.3', '3']; + let expectedVerions = ['100', '3', '1.1', '1.0.0', '0.3', '0.0.5', '0.0.0.9']; + + let actualVersions = AddNewPackageTab.sortPackageVersions(testVersions); + should(actualVersions).be.deepEqual(expectedVerions); + }); }); diff --git a/extensions/notebook/src/jupyter/jupyterController.ts b/extensions/notebook/src/jupyter/jupyterController.ts index 4222a1fdff..9bbf3b8363 100644 --- a/extensions/notebook/src/jupyter/jupyterController.ts +++ b/extensions/notebook/src/jupyter/jupyterController.ts @@ -12,7 +12,7 @@ const localize = nls.loadMessageBundle(); import * as constants from '../common/constants'; import * as localizedConstants from '../common/localizedConstants'; -import JupyterServerInstallation from './jupyterServerInstallation'; +import { JupyterServerInstallation } from './jupyterServerInstallation'; import { IServerInstance } from './common'; import * as utils from '../common/utils'; import { IPrompter, QuestionTypes, IQuestion } from '../prompts/question'; @@ -210,20 +210,6 @@ export class JupyterController implements vscode.Disposable { }); } - public getTextToSendToTerminal(shellType: any): string { - if (utils.getOSPlatform() === utils.Platform.Windows && typeof shellType === 'string') { - if (shellType.endsWith('powershell.exe')) { - return localizedConstants.msgManagePackagesPowershell; - } else if (shellType.endsWith('cmd.exe')) { - return localizedConstants.msgManagePackagesCmd; - } else { - return localizedConstants.msgManagePackagesBash; - } - } else { - return localizedConstants.msgManagePackagesBash; - } - } - public get jupyterInstallation() { return this._jupyterInstallation; } diff --git a/extensions/notebook/src/jupyter/jupyterServerInstallation.ts b/extensions/notebook/src/jupyter/jupyterServerInstallation.ts index dcd6201c5c..4a78733991 100644 --- a/extensions/notebook/src/jupyter/jupyterServerInstallation.ts +++ b/extensions/notebook/src/jupyter/jupyterServerInstallation.ts @@ -35,7 +35,7 @@ const msgSkipPythonInstall = localize('msgSkipPythonInstall', "Python already ex 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); } -export default class JupyterServerInstallation { +export class JupyterServerInstallation { public apiWrapper: ApiWrapper; public extensionPath: string; public pythonBinPath: string; @@ -86,11 +86,11 @@ export default class JupyterServerInstallation { backgroundOperation.updateStatus(azdata.TaskStatus.InProgress, msgPythonDownloadComplete); if (this._usingConda) { - await this.installCondaPackages(); + await this.installCondaDependencies(); } else if (this._usingExistingPython) { - await this.installPipPackages(); + await this.installPipDependencies(); } else { - await this.installOfflinePipPackages(); + await this.installOfflinePipDependencies(); } let doOnlineInstall = this._usingExistingPython; await this.installSparkMagic(doOnlineInstall); @@ -369,7 +369,36 @@ export default class JupyterServerInstallation { return this.executeStreamedCommand(cmd); } - private async installOfflinePipPackages(): Promise { + public async getInstalledCondaPackages(): Promise { + let condaExe = this.getCondaExePath(); + let cmd = `"${condaExe}" list --json`; + let packagesInfo = await this.executeBufferedCommand(cmd); + + if (packagesInfo) { + let packagesResult = JSON.parse(packagesInfo); + if (Array.isArray(packagesResult)) { + return packagesResult + .filter(pkg => pkg && pkg.channel && pkg.channel !== 'pypi') + .map(pkg => { name: pkg.name, version: pkg.version }); + } + } + return []; + } + + public installCondaPackage(packageName: string, version: string): Promise { + let condaExe = this.getCondaExePath(); + let cmd = `"${condaExe}" install -y ${packageName}==${version}`; + return this.executeStreamedCommand(cmd); + } + + public uninstallCondaPackages(packages: PythonPkgDetails[]): Promise { + let condaExe = this.getCondaExePath(); + let packagesStr = packages.map(pkg => `${pkg.name}==${pkg.version}`).join(' '); + let cmd = `"${condaExe}" uninstall -y ${packagesStr}`; + return this.executeStreamedCommand(cmd); + } + + private async installOfflinePipDependencies(): Promise { let installJupyterCommand: string; if (process.platform === constants.winPlatform) { let requirements = path.join(this._pythonPackageDir, 'requirements.txt'); @@ -404,7 +433,7 @@ export default class JupyterServerInstallation { } } - private async installPipPackages(): Promise { + private async installPipDependencies(): Promise { this.outputChannel.show(true); this.outputChannel.appendLine(localize('msgInstallStart', "Installing required packages to run Notebooks...")); @@ -417,7 +446,7 @@ export default class JupyterServerInstallation { this.outputChannel.appendLine(localize('msgJupyterInstallDone', "... Jupyter installation complete.")); } - private async installCondaPackages(): Promise { + private async installCondaDependencies(): Promise { this.outputChannel.show(true); this.outputChannel.appendLine(localize('msgInstallStart', "Installing required packages to run Notebooks...")); @@ -437,7 +466,7 @@ export default class JupyterServerInstallation { await utils.executeStreamedCommand(command, { env: this.execOptions.env }, this.outputChannel); } - private async executeBufferedCommand(command: string): Promise { + public async executeBufferedCommand(command: string): Promise { return await utils.executeBufferedCommand(command, { env: this.execOptions.env }); } @@ -445,11 +474,15 @@ export default class JupyterServerInstallation { return this._pythonExecutable; } - private getCondaExePath(): string { + public getCondaExePath(): string { return path.join(this._pythonInstallationPath, process.platform === constants.winPlatform ? 'Scripts\\conda.exe' : 'bin/conda'); } + public get usingConda(): boolean { + return this._usingConda; + } + private checkCondaExists(): boolean { if (!this._usingExistingPython) { return false; @@ -526,11 +559,6 @@ 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, @@ -542,4 +570,10 @@ export default class JupyterServerInstallation { export interface PythonPkgDetails { name: string; version: string; +} + +export interface PipPackageOverview { + name: string; + versions: string[]; + summary: string; } \ No newline at end of file diff --git a/extensions/notebook/src/jupyter/jupyterServerManager.ts b/extensions/notebook/src/jupyter/jupyterServerManager.ts index b1c1953b15..1084f0e8a5 100644 --- a/extensions/notebook/src/jupyter/jupyterServerManager.ts +++ b/extensions/notebook/src/jupyter/jupyterServerManager.ts @@ -13,7 +13,7 @@ import * as nls from 'vscode-nls'; const localize = nls.loadMessageBundle(); import { ApiWrapper } from '../common/apiWrapper'; -import JupyterServerInstallation from './jupyterServerInstallation'; +import { JupyterServerInstallation } from './jupyterServerInstallation'; import * as utils from '../common/utils'; import { IServerInstance } from './common'; import { PerNotebookServerInstance, IInstanceOptions } from './serverInstance'; diff --git a/extensions/notebook/src/jupyter/serverInstance.ts b/extensions/notebook/src/jupyter/serverInstance.ts index f40dc46690..00309257ee 100644 --- a/extensions/notebook/src/jupyter/serverInstance.ts +++ b/extensions/notebook/src/jupyter/serverInstance.ts @@ -13,7 +13,7 @@ import * as nls from 'vscode-nls'; const localize = nls.loadMessageBundle(); import { IServerInstance } from './common'; -import JupyterServerInstallation from './jupyterServerInstallation'; +import { JupyterServerInstallation } from './jupyterServerInstallation'; import * as utils from '../common/utils'; import * as constants from '../common/constants'; import * as notebookUtils from '../common/notebookUtils'; diff --git a/extensions/notebook/src/test/model/serverInstance.test.ts b/extensions/notebook/src/test/model/serverInstance.test.ts index 60bf21f6f9..37d655c684 100644 --- a/extensions/notebook/src/test/model/serverInstance.test.ts +++ b/extensions/notebook/src/test/model/serverInstance.test.ts @@ -9,7 +9,7 @@ import * as stream from 'stream'; import { ChildProcess } from 'child_process'; import 'mocha'; -import JupyterServerInstallation from '../../jupyter/jupyterServerInstallation'; +import { JupyterServerInstallation } from '../../jupyter/jupyterServerInstallation'; import { ApiWrapper } from '../..//common/apiWrapper'; import { PerNotebookServerInstance, ServerInstanceUtils } from '../../jupyter/serverInstance'; import { MockOutputChannel } from '../common/stubs'; diff --git a/extensions/notebook/src/test/model/serverManager.test.ts b/extensions/notebook/src/test/model/serverManager.test.ts index ec17f93095..ba8c0ff040 100644 --- a/extensions/notebook/src/test/model/serverManager.test.ts +++ b/extensions/notebook/src/test/model/serverManager.test.ts @@ -10,7 +10,7 @@ import 'mocha'; import { JupyterServerInstanceStub } from '../common'; import { LocalJupyterServerManager, ServerInstanceFactory } from '../../jupyter/jupyterServerManager'; -import JupyterServerInstallation from '../../jupyter/jupyterServerInstallation'; +import { JupyterServerInstallation } from '../../jupyter/jupyterServerInstallation'; import { Deferred } from '../../common/promise'; import { ApiWrapper } from '../../common/apiWrapper'; import * as testUtils from '../common/testUtils';