diff --git a/extensions/notebook/src/jupyter/jupyterServerInstallation.ts b/extensions/notebook/src/jupyter/jupyterServerInstallation.ts index b92460ef54..47d9dd06a7 100644 --- a/extensions/notebook/src/jupyter/jupyterServerInstallation.ts +++ b/extensions/notebook/src/jupyter/jupyterServerInstallation.ts @@ -63,7 +63,6 @@ export interface IJupyterServerInstallation { getInstalledCondaPackages(): Promise; uninstallCondaPackages(packages: PythonPkgDetails[]): Promise; usingConda: boolean; - getCondaExePath(): string; executeBufferedCommand(command: string): Promise; executeStreamedCommand(command: string): Promise; /** @@ -74,6 +73,7 @@ export interface IJupyterServerInstallation { installPipPackages(packages: PythonPkgDetails[], useMinVersionDefault: boolean): Promise; uninstallPipPackages(packages: PythonPkgDetails[]): Promise; pythonExecutable: string; + condaExecutable: string | undefined; pythonInstallationPath: string; installedPythonVersion: string; } @@ -113,8 +113,8 @@ export class JupyterServerInstallation implements IJupyterServerInstallation { private _pythonInstallationPath: string; private _pythonExecutable: string; + private _condaExecutable: string | undefined; private _usingExistingPython: boolean; - private _usingConda: boolean; private _installedPythonVersion: string; private _upgradeInProcess: boolean = false; @@ -154,7 +154,6 @@ export class JupyterServerInstallation implements IJupyterServerInstallation { this._pythonInstallationPath = JupyterServerInstallation.getPythonInstallPath(); this._usingExistingPython = JupyterServerInstallation.getExistingPythonSetting(); } - this._usingConda = false; this._installInProgress = false; this._kernelSetupCache = new Map(); @@ -360,10 +359,9 @@ export class JupyterServerInstallation implements IJupyterServerInstallation { let pythonBinPathSuffix = process.platform === constants.winPlatform ? '' : 'bin'; this._pythonExecutable = JupyterServerInstallation.getPythonExePath(this._pythonInstallationPath); + this._condaExecutable = await JupyterServerInstallation.getCondaExePath(this._pythonInstallationPath); this.pythonBinPath = path.join(this._pythonInstallationPath, pythonBinPathSuffix); - this._usingConda = this.isCondaInstalled(); - // Store paths to python libraries required to run jupyter. this.pythonEnvVarPath = process.env['PATH']; @@ -373,7 +371,7 @@ export class JupyterServerInstallation implements IJupyterServerInstallation { let pythonScriptsPath = path.join(this._pythonInstallationPath, 'Scripts'); this.pythonEnvVarPath = pythonScriptsPath + delimiter + this.pythonEnvVarPath; - if (this._usingConda) { + if (this.usingConda) { this.pythonEnvVarPath = [ path.join(this._pythonInstallationPath, 'Library', 'mingw-w64', 'bin'), path.join(this._pythonInstallationPath, 'Library', 'usr', 'bin'), @@ -650,12 +648,10 @@ export class JupyterServerInstallation implements IJupyterServerInstallation { public async getInstalledCondaPackages(): Promise { try { - if (!this.isCondaInstalled()) { + if (!this.condaExecutable) { return []; } - - let condaExe = this.getCondaExePath(); - let cmd = `"${condaExe}" list --json`; + let cmd = `"${this.condaExecutable}" list --json`; let packagesInfo = await this.executeBufferedCommand(cmd); let packages: PythonPkgDetails[] = []; @@ -673,8 +669,8 @@ export class JupyterServerInstallation implements IJupyterServerInstallation { } } - public installCondaPackages(packages: PythonPkgDetails[], useMinVersionDefault: boolean): Promise { - if (!packages || packages.length === 0) { + public async installCondaPackages(packages: PythonPkgDetails[], useMinVersionDefault: boolean): Promise { + if (!this.condaExecutable || !packages || packages.length === 0) { return Promise.resolve(); } @@ -683,23 +679,24 @@ export class JupyterServerInstallation implements IJupyterServerInstallation { const pkgVersionSpecifier = pkg.installExactVersion ? '==' : versionSpecifierDefault; return `"${pkg.name}${pkgVersionSpecifier}${pkg.version}"`; }).join(' '); - let condaExe = this.getCondaExePath(); - let cmd = `"${condaExe}" install -c conda-forge -y ${packagesStr}`; - return this.executeStreamedCommand(cmd); + + let cmd = `"${this.condaExecutable}" install -c conda-forge -y ${packagesStr}`; + await this.executeStreamedCommand(cmd); } - public uninstallCondaPackages(packages: PythonPkgDetails[]): Promise { - for (let pkg of packages) { - if (this._requiredPackagesSet.has(pkg.name)) { - this._kernelSetupCache.clear(); - break; + public async uninstallCondaPackages(packages: PythonPkgDetails[]): Promise { + if (this.condaExecutable) { + for (let pkg of packages) { + if (this._requiredPackagesSet.has(pkg.name)) { + this._kernelSetupCache.clear(); + break; + } } - } - let condaExe = this.getCondaExePath(); - let packagesStr = packages.map(pkg => `"${pkg.name}==${pkg.version}"`).join(' '); - let cmd = `"${condaExe}" uninstall -y ${packagesStr}`; - return this.executeStreamedCommand(cmd); + let packagesStr = packages.map(pkg => `"${pkg.name}==${pkg.version}"`).join(' '); + let cmd = `"${this.condaExecutable}" uninstall -y ${packagesStr}`; + await this.executeStreamedCommand(cmd); + } } public async executeStreamedCommand(command: string): Promise { @@ -714,9 +711,29 @@ export class JupyterServerInstallation implements IJupyterServerInstallation { return this._pythonExecutable; } - public getCondaExePath(): string { - return path.join(this._pythonInstallationPath, - process.platform === constants.winPlatform ? 'Scripts\\conda.exe' : 'bin/conda'); + public get condaExecutable(): string | undefined { + return this._condaExecutable; + } + + public static async getCondaExePath(pythonInstallationPath: string): Promise { + let exeName = process.platform === constants.winPlatform ? 'Scripts\\conda.exe' : 'bin/conda'; + let condaPath = path.join(pythonInstallationPath, exeName); + let condaExists = await fs.pathExists(condaPath); + // If conda was not found, then check if we're using a virtual environment + if (!condaExists) { + let pathParts = pythonInstallationPath.split(path.sep); + if (pathParts.length > 1 && pathParts[pathParts.length - 2] === 'envs') { + // The root Anaconda folder is 2 folders above the virtual environment's folder + // Example: Anaconda3\envs\myEnv\python.exe -> Anaconda3\conda.exe + // Docs: https://conda.io/projects/conda/en/latest/user-guide/tasks/manage-environments.html#creating-an-environment-with-commands + condaPath = path.join(pythonInstallationPath, '..', '..', exeName); + condaExists = await fs.pathExists(condaPath); + } + if (!condaExists) { + condaPath = undefined; + } + } + return condaPath; } /** @@ -727,19 +744,13 @@ export class JupyterServerInstallation implements IJupyterServerInstallation { } public get usingConda(): boolean { - return this._usingConda; + return !!this._condaExecutable; } public get installedPythonVersion(): string { return this._installedPythonVersion; } - private isCondaInstalled(): boolean { - let condaExePath = this.getCondaExePath(); - // eslint-disable-next-line no-sync - return fs.existsSync(condaExePath); - } - /** * Checks if a python executable exists at the "notebook.pythonPath" defined in the user's settings. */ diff --git a/extensions/notebook/src/jupyter/localCondaPackageManageProvider.ts b/extensions/notebook/src/jupyter/localCondaPackageManageProvider.ts index 7c34342029..12997df4b8 100644 --- a/extensions/notebook/src/jupyter/localCondaPackageManageProvider.ts +++ b/extensions/notebook/src/jupyter/localCondaPackageManageProvider.ts @@ -86,7 +86,7 @@ export class LocalCondaPackageManageProvider implements IPackageManageProvider { } private async fetchCondaPackage(packageName: string): Promise { - let condaExe = this.jupyterInstallation.getCondaExePath(); + let condaExe = this.jupyterInstallation.condaExecutable; let cmd = `"${condaExe}" search --json ${packageName}`; let packageResult: string; try { diff --git a/extensions/notebook/src/test/managePackages/localPackageManageProvider.test.ts b/extensions/notebook/src/test/managePackages/localPackageManageProvider.test.ts index f7cd890fbb..ab4728067e 100644 --- a/extensions/notebook/src/test/managePackages/localPackageManageProvider.test.ts +++ b/extensions/notebook/src/test/managePackages/localPackageManageProvider.test.ts @@ -212,8 +212,8 @@ describe('Manage Package Providers', () => { uninstallCondaPackages: (packages: PythonPkgDetails[]) => { return Promise.resolve(); }, executeBufferedCommand: (command: string) => { return Promise.resolve(''); }, executeStreamedCommand: (command: string) => { return Promise.resolve(); }, - getCondaExePath: () => { return ''; }, pythonExecutable: '', + condaExecutable: undefined, pythonInstallationPath: '', usingConda: false, installedPythonVersion: '', diff --git a/extensions/notebook/src/test/python/jupyterInstallation.test.ts b/extensions/notebook/src/test/python/jupyterInstallation.test.ts index 6a351f33aa..c50e7606e0 100644 --- a/extensions/notebook/src/test/python/jupyterInstallation.test.ts +++ b/extensions/notebook/src/test/python/jupyterInstallation.test.ts @@ -31,12 +31,12 @@ describe('Jupyter Server Installation', function () { sinon.restore(); }); - it('Getters and setters', async function() { + it('Getters and setters', async function () { let pythonPath = installation.pythonInstallationPath; should(pythonPath).be.equal(JupyterServerInstallation.getPythonInstallPath()); }); - it('Get pip packages', async function() { + it('Get pip packages', async function () { // Should return nothing if passed an invalid python path let fakePath = uuid.v4(); let pkgResult = await installation.getInstalledPipPackages(fakePath); @@ -73,7 +73,7 @@ describe('Jupyter Server Installation', function () { should(pkgResult).be.deepEqual(testPackages); }); - it('Install pip package', async function() { + it('Install pip package', async function () { let commandStub = sinon.stub(utils, 'executeStreamedCommand').resolves(); // Should not execute any commands when passed an empty package list @@ -103,7 +103,7 @@ describe('Jupyter Server Installation', function () { should(commandStr.includes('"TestPkg1>=1.2.3" "TestPkg2>=4.5.6"')).be.true(); }); - it('Uninstall pip package', async function() { + it('Uninstall pip package', async function () { let commandStub = sinon.stub(utils, 'executeStreamedCommand').resolves(); let testPackages = [{ @@ -119,16 +119,16 @@ describe('Jupyter Server Installation', function () { should(commandStr.includes('"jupyter==1.0.0" "TestPkg2==4.5.6"')).be.true(); }); - it('Get conda packages', async function() { + it('Get conda packages', async function () { // Should return nothing if conda is not installed - sinon.stub(fs, 'existsSync').returns(false); + sinon.stub(installation, 'condaExecutable').get(() => undefined); let pkgResult = await installation.getInstalledCondaPackages(); should(pkgResult).not.be.undefined(); should(pkgResult.length).be.equal(0); // Should return nothing on error sinon.restore(); - sinon.stub(fs, 'existsSync').returns(true); + sinon.stub(installation, 'condaExecutable').get(() => 'TestCondaPath'); sinon.stub(utils, 'executeBufferedCommand').rejects(new Error('Expected test failure.')); pkgResult = await installation.getInstalledCondaPackages(); should(pkgResult).not.be.undefined(); @@ -150,16 +150,31 @@ describe('Jupyter Server Installation', function () { version: '7.8.9', channel: 'conda' }]; - sinon.stub(fs, 'existsSync').returns(true); + sinon.stub(installation, 'condaExecutable').get(() => 'TestCondaPath'); sinon.stub(utils, 'executeBufferedCommand').resolves(JSON.stringify(testPackages)); pkgResult = await installation.getInstalledCondaPackages(); let filteredPackages = testPackages.filter(pkg => pkg.channel !== 'pypi'); should(pkgResult).be.deepEqual(filteredPackages); }); - it('Install conda package', async function() { + it('Install conda package', async function () { + const testPackages = [{ + name: 'TestPkg1', + version: '1.2.3' + }, { + name: 'TestPkg2', + version: '4.5.6' + }]; let commandStub = sinon.stub(utils, 'executeStreamedCommand').resolves(); + // Should not execute any commands if conda is not installed + let condaStub = sinon.stub(installation, 'condaExecutable').get(() => undefined); + await installation.installCondaPackages(testPackages, false); + should(commandStub.called).be.false(); + + condaStub.restore(); + sinon.stub(installation, 'condaExecutable').get(() => 'TestCondaPath'); + // Should not execute any commands when passed an empty package list await installation.installCondaPackages(undefined, false); should(commandStub.called).be.false(); @@ -168,13 +183,6 @@ describe('Jupyter Server Installation', function () { should(commandStub.called).be.false(); // Install package using exact version - let testPackages = [{ - name: 'TestPkg1', - version: '1.2.3' - }, { - name: 'TestPkg2', - version: '4.5.6' - }]; await installation.installCondaPackages(testPackages, false); should(commandStub.calledOnce).be.true(); let commandStr = commandStub.args[0][0] as string; @@ -187,7 +195,8 @@ describe('Jupyter Server Installation', function () { should(commandStr.includes('"TestPkg1>=1.2.3" "TestPkg2>=4.5.6"')).be.true(); }); - it('Uninstall conda package', async function() { + it('Uninstall conda package', async function () { + sinon.stub(installation, 'condaExecutable').get(() => 'TestCondaPath'); let commandStub = sinon.stub(utils, 'executeStreamedCommand').resolves(); let testPackages = [{ @@ -203,29 +212,29 @@ describe('Jupyter Server Installation', function () { should(commandStr.includes('"jupyter==1.0.0" "TestPkg2==4.5.6"')).be.true(); }); - it('Get required packages test - Undefined argument', async function() { + it('Get required packages test - Undefined argument', async function () { let packages = installation.getRequiredPackagesForKernel(undefined); should(packages).not.be.undefined(); should(packages.length).be.equal(0); }); - it('Get required packages test - Fake kernel', async function() { + it('Get required packages test - Fake kernel', async function () { let packages = installation.getRequiredPackagesForKernel('NotARealKernel'); should(packages).not.be.undefined(); should(packages.length).be.equal(0); }); - it('Get required packages test - Python 3 kernel', async function() { + it('Get required packages test - Python 3 kernel', async function () { let packages = installation.getRequiredPackagesForKernel(python3DisplayName); should(packages).be.deepEqual([requiredJupyterPkg]); }); - it('Get required packages test - Powershell kernel', async function() { + it('Get required packages test - Powershell kernel', async function () { let packages = installation.getRequiredPackagesForKernel(powershellDisplayName); should(packages).be.deepEqual([requiredJupyterPkg, requiredPowershellPkg]); }); - it('Get required packages test - Spark kernels', async function() { + it('Get required packages test - Spark kernels', async function () { let packages = installation.getRequiredPackagesForKernel(pysparkDisplayName); should(packages).be.deepEqual(requiredSparkPackages, 'Unexpected packages for PySpark kernel.'); @@ -236,7 +245,7 @@ describe('Jupyter Server Installation', function () { should(packages).be.deepEqual(requiredSparkPackages, 'Unexpected packages for Spark R kernel.'); }); - it('Install python test - Run install while Python is already running', async function() { + it('Install python test - Run install while Python is already running', async function () { // Should reject overwriting an existing python install if running on Windows and python is currently running. if (process.platform === winPlatform) { sinon.stub(utils, 'executeBufferedCommand').resolves('python.exe'); @@ -250,7 +259,7 @@ describe('Jupyter Server Installation', function () { } }); - it('Install python test - Run install with existing Python instance', async function() { + it('Install python test - Run install with existing Python instance', async function () { let installSettings: PythonInstallSettings = { installPath: `${process.env['USERPROFILE']}\\ads-python`, existingPython: true, @@ -258,7 +267,7 @@ describe('Jupyter Server Installation', function () { }; sinon.stub(utils, 'exists').resolves(true); - sinon.stub(fs, 'existsSync').returns(false); + sinon.stub(fs, 'pathExists').resolves(false); sinon.stub(utils, 'executeBufferedCommand').resolves(`${installSettings.installPath}\\site-packages`); // Both of these are called from upgradePythonPackages