Fix conda virtual environment paths in notebooks (#20438)

This commit is contained in:
Cory Rivera
2022-08-23 13:34:06 -07:00
committed by GitHub
parent fa6ffb6ce6
commit 52290a796d
4 changed files with 82 additions and 62 deletions

View File

@@ -63,7 +63,6 @@ export interface IJupyterServerInstallation {
getInstalledCondaPackages(): Promise<PythonPkgDetails[]>; getInstalledCondaPackages(): Promise<PythonPkgDetails[]>;
uninstallCondaPackages(packages: PythonPkgDetails[]): Promise<void>; uninstallCondaPackages(packages: PythonPkgDetails[]): Promise<void>;
usingConda: boolean; usingConda: boolean;
getCondaExePath(): string;
executeBufferedCommand(command: string): Promise<string>; executeBufferedCommand(command: string): Promise<string>;
executeStreamedCommand(command: string): Promise<void>; executeStreamedCommand(command: string): Promise<void>;
/** /**
@@ -74,6 +73,7 @@ export interface IJupyterServerInstallation {
installPipPackages(packages: PythonPkgDetails[], useMinVersionDefault: boolean): Promise<void>; installPipPackages(packages: PythonPkgDetails[], useMinVersionDefault: boolean): Promise<void>;
uninstallPipPackages(packages: PythonPkgDetails[]): Promise<void>; uninstallPipPackages(packages: PythonPkgDetails[]): Promise<void>;
pythonExecutable: string; pythonExecutable: string;
condaExecutable: string | undefined;
pythonInstallationPath: string; pythonInstallationPath: string;
installedPythonVersion: string; installedPythonVersion: string;
} }
@@ -113,8 +113,8 @@ export class JupyterServerInstallation implements IJupyterServerInstallation {
private _pythonInstallationPath: string; private _pythonInstallationPath: string;
private _pythonExecutable: string; private _pythonExecutable: string;
private _condaExecutable: string | undefined;
private _usingExistingPython: boolean; private _usingExistingPython: boolean;
private _usingConda: boolean;
private _installedPythonVersion: string; private _installedPythonVersion: string;
private _upgradeInProcess: boolean = false; private _upgradeInProcess: boolean = false;
@@ -154,7 +154,6 @@ export class JupyterServerInstallation implements IJupyterServerInstallation {
this._pythonInstallationPath = JupyterServerInstallation.getPythonInstallPath(); this._pythonInstallationPath = JupyterServerInstallation.getPythonInstallPath();
this._usingExistingPython = JupyterServerInstallation.getExistingPythonSetting(); this._usingExistingPython = JupyterServerInstallation.getExistingPythonSetting();
} }
this._usingConda = false;
this._installInProgress = false; this._installInProgress = false;
this._kernelSetupCache = new Map<string, boolean>(); this._kernelSetupCache = new Map<string, boolean>();
@@ -360,10 +359,9 @@ export class JupyterServerInstallation implements IJupyterServerInstallation {
let pythonBinPathSuffix = process.platform === constants.winPlatform ? '' : 'bin'; let pythonBinPathSuffix = process.platform === constants.winPlatform ? '' : 'bin';
this._pythonExecutable = JupyterServerInstallation.getPythonExePath(this._pythonInstallationPath); this._pythonExecutable = JupyterServerInstallation.getPythonExePath(this._pythonInstallationPath);
this._condaExecutable = await JupyterServerInstallation.getCondaExePath(this._pythonInstallationPath);
this.pythonBinPath = path.join(this._pythonInstallationPath, pythonBinPathSuffix); this.pythonBinPath = path.join(this._pythonInstallationPath, pythonBinPathSuffix);
this._usingConda = this.isCondaInstalled();
// Store paths to python libraries required to run jupyter. // Store paths to python libraries required to run jupyter.
this.pythonEnvVarPath = process.env['PATH']; this.pythonEnvVarPath = process.env['PATH'];
@@ -373,7 +371,7 @@ export class JupyterServerInstallation implements IJupyterServerInstallation {
let pythonScriptsPath = path.join(this._pythonInstallationPath, 'Scripts'); let pythonScriptsPath = path.join(this._pythonInstallationPath, 'Scripts');
this.pythonEnvVarPath = pythonScriptsPath + delimiter + this.pythonEnvVarPath; this.pythonEnvVarPath = pythonScriptsPath + delimiter + this.pythonEnvVarPath;
if (this._usingConda) { if (this.usingConda) {
this.pythonEnvVarPath = [ this.pythonEnvVarPath = [
path.join(this._pythonInstallationPath, 'Library', 'mingw-w64', 'bin'), path.join(this._pythonInstallationPath, 'Library', 'mingw-w64', 'bin'),
path.join(this._pythonInstallationPath, 'Library', 'usr', 'bin'), path.join(this._pythonInstallationPath, 'Library', 'usr', 'bin'),
@@ -650,12 +648,10 @@ export class JupyterServerInstallation implements IJupyterServerInstallation {
public async getInstalledCondaPackages(): Promise<PythonPkgDetails[]> { public async getInstalledCondaPackages(): Promise<PythonPkgDetails[]> {
try { try {
if (!this.isCondaInstalled()) { if (!this.condaExecutable) {
return []; return [];
} }
let cmd = `"${this.condaExecutable}" list --json`;
let condaExe = this.getCondaExePath();
let cmd = `"${condaExe}" list --json`;
let packagesInfo = await this.executeBufferedCommand(cmd); let packagesInfo = await this.executeBufferedCommand(cmd);
let packages: PythonPkgDetails[] = []; let packages: PythonPkgDetails[] = [];
@@ -673,8 +669,8 @@ export class JupyterServerInstallation implements IJupyterServerInstallation {
} }
} }
public installCondaPackages(packages: PythonPkgDetails[], useMinVersionDefault: boolean): Promise<void> { public async installCondaPackages(packages: PythonPkgDetails[], useMinVersionDefault: boolean): Promise<void> {
if (!packages || packages.length === 0) { if (!this.condaExecutable || !packages || packages.length === 0) {
return Promise.resolve(); return Promise.resolve();
} }
@@ -683,12 +679,13 @@ export class JupyterServerInstallation implements IJupyterServerInstallation {
const pkgVersionSpecifier = pkg.installExactVersion ? '==' : versionSpecifierDefault; const pkgVersionSpecifier = pkg.installExactVersion ? '==' : versionSpecifierDefault;
return `"${pkg.name}${pkgVersionSpecifier}${pkg.version}"`; return `"${pkg.name}${pkgVersionSpecifier}${pkg.version}"`;
}).join(' '); }).join(' ');
let condaExe = this.getCondaExePath();
let cmd = `"${condaExe}" install -c conda-forge -y ${packagesStr}`; let cmd = `"${this.condaExecutable}" install -c conda-forge -y ${packagesStr}`;
return this.executeStreamedCommand(cmd); await this.executeStreamedCommand(cmd);
} }
public uninstallCondaPackages(packages: PythonPkgDetails[]): Promise<void> { public async uninstallCondaPackages(packages: PythonPkgDetails[]): Promise<void> {
if (this.condaExecutable) {
for (let pkg of packages) { for (let pkg of packages) {
if (this._requiredPackagesSet.has(pkg.name)) { if (this._requiredPackagesSet.has(pkg.name)) {
this._kernelSetupCache.clear(); this._kernelSetupCache.clear();
@@ -696,10 +693,10 @@ export class JupyterServerInstallation implements IJupyterServerInstallation {
} }
} }
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 = `"${this.condaExecutable}" uninstall -y ${packagesStr}`;
return this.executeStreamedCommand(cmd); await this.executeStreamedCommand(cmd);
}
} }
public async executeStreamedCommand(command: string): Promise<void> { public async executeStreamedCommand(command: string): Promise<void> {
@@ -714,9 +711,29 @@ export class JupyterServerInstallation implements IJupyterServerInstallation {
return this._pythonExecutable; return this._pythonExecutable;
} }
public getCondaExePath(): string { public get condaExecutable(): string | undefined {
return path.join(this._pythonInstallationPath, return this._condaExecutable;
process.platform === constants.winPlatform ? 'Scripts\\conda.exe' : 'bin/conda'); }
public static async getCondaExePath(pythonInstallationPath: string): Promise<string> {
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 { public get usingConda(): boolean {
return this._usingConda; return !!this._condaExecutable;
} }
public get installedPythonVersion(): string { public get installedPythonVersion(): string {
return this._installedPythonVersion; 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. * Checks if a python executable exists at the "notebook.pythonPath" defined in the user's settings.
*/ */

View File

@@ -86,7 +86,7 @@ export class LocalCondaPackageManageProvider implements IPackageManageProvider {
} }
private async fetchCondaPackage(packageName: string): Promise<IPackageOverview> { private async fetchCondaPackage(packageName: string): Promise<IPackageOverview> {
let condaExe = this.jupyterInstallation.getCondaExePath(); let condaExe = this.jupyterInstallation.condaExecutable;
let cmd = `"${condaExe}" search --json ${packageName}`; let cmd = `"${condaExe}" search --json ${packageName}`;
let packageResult: string; let packageResult: string;
try { try {

View File

@@ -212,8 +212,8 @@ describe('Manage Package Providers', () => {
uninstallCondaPackages: (packages: PythonPkgDetails[]) => { return Promise.resolve(); }, uninstallCondaPackages: (packages: PythonPkgDetails[]) => { return Promise.resolve(); },
executeBufferedCommand: (command: string) => { return Promise.resolve(''); }, executeBufferedCommand: (command: string) => { return Promise.resolve(''); },
executeStreamedCommand: (command: string) => { return Promise.resolve(); }, executeStreamedCommand: (command: string) => { return Promise.resolve(); },
getCondaExePath: () => { return ''; },
pythonExecutable: '', pythonExecutable: '',
condaExecutable: undefined,
pythonInstallationPath: '', pythonInstallationPath: '',
usingConda: false, usingConda: false,
installedPythonVersion: '', installedPythonVersion: '',

View File

@@ -121,14 +121,14 @@ describe('Jupyter Server Installation', function () {
it('Get conda packages', async function () { it('Get conda packages', async function () {
// Should return nothing if conda is not installed // 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(); let pkgResult = await installation.getInstalledCondaPackages();
should(pkgResult).not.be.undefined(); should(pkgResult).not.be.undefined();
should(pkgResult.length).be.equal(0); should(pkgResult.length).be.equal(0);
// Should return nothing on error // Should return nothing on error
sinon.restore(); sinon.restore();
sinon.stub(fs, 'existsSync').returns(true); sinon.stub(installation, 'condaExecutable').get(() => 'TestCondaPath');
sinon.stub(utils, 'executeBufferedCommand').rejects(new Error('Expected test failure.')); sinon.stub(utils, 'executeBufferedCommand').rejects(new Error('Expected test failure.'));
pkgResult = await installation.getInstalledCondaPackages(); pkgResult = await installation.getInstalledCondaPackages();
should(pkgResult).not.be.undefined(); should(pkgResult).not.be.undefined();
@@ -150,7 +150,7 @@ describe('Jupyter Server Installation', function () {
version: '7.8.9', version: '7.8.9',
channel: 'conda' channel: 'conda'
}]; }];
sinon.stub(fs, 'existsSync').returns(true); sinon.stub(installation, 'condaExecutable').get(() => 'TestCondaPath');
sinon.stub(utils, 'executeBufferedCommand').resolves(JSON.stringify(testPackages)); sinon.stub(utils, 'executeBufferedCommand').resolves(JSON.stringify(testPackages));
pkgResult = await installation.getInstalledCondaPackages(); pkgResult = await installation.getInstalledCondaPackages();
let filteredPackages = testPackages.filter(pkg => pkg.channel !== 'pypi'); let filteredPackages = testPackages.filter(pkg => pkg.channel !== 'pypi');
@@ -158,8 +158,23 @@ describe('Jupyter Server Installation', function () {
}); });
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(); 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 // Should not execute any commands when passed an empty package list
await installation.installCondaPackages(undefined, false); await installation.installCondaPackages(undefined, false);
should(commandStub.called).be.false(); should(commandStub.called).be.false();
@@ -168,13 +183,6 @@ describe('Jupyter Server Installation', function () {
should(commandStub.called).be.false(); should(commandStub.called).be.false();
// Install package using exact version // Install package using exact version
let testPackages = [{
name: 'TestPkg1',
version: '1.2.3'
}, {
name: 'TestPkg2',
version: '4.5.6'
}];
await installation.installCondaPackages(testPackages, false); await installation.installCondaPackages(testPackages, false);
should(commandStub.calledOnce).be.true(); should(commandStub.calledOnce).be.true();
let commandStr = commandStub.args[0][0] as string; let commandStr = commandStub.args[0][0] as string;
@@ -188,6 +196,7 @@ describe('Jupyter Server Installation', function () {
}); });
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 commandStub = sinon.stub(utils, 'executeStreamedCommand').resolves();
let testPackages = [{ let testPackages = [{
@@ -258,7 +267,7 @@ describe('Jupyter Server Installation', function () {
}; };
sinon.stub(utils, 'exists').resolves(true); 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`); sinon.stub(utils, 'executeBufferedCommand').resolves(`${installSettings.installPath}\\site-packages`);
// Both of these are called from upgradePythonPackages // Both of these are called from upgradePythonPackages