Skip to content

Commit

Permalink
Pip install DS packages if Python ext fails
Browse files Browse the repository at this point in the history
  • Loading branch information
DonJayamanne committed Nov 22, 2021
1 parent 0822bed commit c9dea22
Show file tree
Hide file tree
Showing 8 changed files with 230 additions and 17 deletions.
1 change: 1 addition & 0 deletions package.nls.json
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
{
"products.installingModule": "Installing {0}",
"jupyter.command.jupyter.exportAsPythonScript.title": "Export to Python Script",
"jupyter.command.jupyter.exportToHTML.title": "Export to HTML",
"jupyter.command.jupyter.exportToPDF.title": "Export to PDF",
Expand Down
156 changes: 156 additions & 0 deletions src/client/common/installer/backupPipInstaller.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,156 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License.

import { inject, injectable, named } from 'inversify';
import { CancellationToken, Disposable, OutputChannel, ProgressLocation, ProgressOptions } from 'vscode';
import { Telemetry } from '../../datascience/constants';
import { EnvironmentType, PythonEnvironment } from '../../pythonEnvironments/info';
import { sendTelemetryEvent } from '../../telemetry';
import { IApplicationShell, IWorkspaceService } from '../application/types';
import { wrapCancellationTokens } from '../cancellation';
import { STANDARD_OUTPUT_CHANNEL } from '../constants';
import { disposeAllDisposables } from '../helpers';
import { traceError, traceVerbose, traceWarning } from '../logger';
import { getDisplayPath } from '../platform/fs-paths';
import { IPythonExecutionFactory } from '../process/types';
import { IDisposable, IOutputChannel, Product, Resource } from '../types';
import { DataScience } from '../utils/localize';
import { translateProductToModule } from './productInstaller';
import { ProductNames } from './productNames';

/**
* Class used to install IPyKernel into global enviroments if Python extension fails to install it for what ever reason.
* We know ipykernel can be easily installed with `python -m pip install ipykernel` for Global Python Environments.
* Similarly for Jupyter & other python packages.
*
* Note: This is only a fallback, we know that sometimes Python fails to install these & Python installs them via the terminal.
* We don't really know why it fails to install these packages.
*/
@injectable()
export class BackupPipInstaller {
constructor(
@inject(IApplicationShell) private readonly appShell: IApplicationShell,
@inject(IWorkspaceService) private readonly workspaceService: IWorkspaceService,
@inject(IOutputChannel) @named(STANDARD_OUTPUT_CHANNEL) private readonly outputChannel: OutputChannel,
@inject(IPythonExecutionFactory) private readonly pythonExecFactory: IPythonExecutionFactory,
private readonly isInstalled: (product: Product, interpreter: PythonEnvironment) => Promise<boolean | undefined>
) {}
public async install(
product: Product,
interpreter: PythonEnvironment,
resource: Resource,
reInstallAndUpdate: boolean,
token: CancellationToken
): Promise<boolean> {
if (product === Product.pip) {
return false;
}
// We can only run this installer against global enviorments.
if (interpreter.envType !== EnvironmentType.Global) {
traceWarning(`We cannot pip install packages into non-Global Python environments.`);
return false;
}
// Check if pip is installed.
const isPipInstalled = await this.isInstalled(Product.pip, interpreter);
if (isPipInstalled === undefined || isPipInstalled === false) {
traceWarning(`We cannot pip install packages if Pip is unavailable.`);
return false;
}
try {
// Display progress indicator if we have ability to cancel this operation from calling code.
// This is required as its possible the installation can take a long time.
const productName = ProductNames.get(product)!;
const options: ProgressOptions = {
location: ProgressLocation.Notification,
cancellable: true,
title: DataScience.installingModule().format(productName)
};

let installationResult = false;
await this.appShell.withProgress(options, async (_, progressToken: CancellationToken) => {
installationResult = await this.installImplmentation(
product,
interpreter,
resource,
reInstallAndUpdate,
wrapCancellationTokens(token, progressToken)
);
});

if (!installationResult) {
return false;
}

// Check if the package is installed.
const isInstalled = await this.isInstalled(product, interpreter);
traceVerbose(
`After successfully running pip install product is ${
isInstalled ? '' : 'still not'
} installed (when checking via IInstaller.isInstalled).`
);

sendTelemetryEvent(Telemetry.PythonModuleInstal, undefined, {
action: isInstalled === true ? 'installedInJupyter' : 'failedToInstallInJupyter',
moduleName: productName,
pythonEnvType: interpreter.envType
});
return isInstalled === true;
} catch (ex) {
const productName = ProductNames.get(product)!;
traceError(`Failed to Pip install ${productName} into ${getDisplayPath(interpreter.path)}`, ex);
return false;
}
}
private async installImplmentation(
product: Product,
interpreter: PythonEnvironment,
resource: Resource,
reInstallAndUpdate: boolean,
token: CancellationToken
) {
const service = await this.pythonExecFactory.createActivatedEnvironment({
allowEnvironmentFetchExceptions: true,
interpreter,
resource
});
if (token.isCancellationRequested) {
return false;
}
const productName = ProductNames.get(product)!;
const args = this.getInstallerArgs({ product, reinstall: reInstallAndUpdate });
const cwd = resource ? this.workspaceService.getWorkspaceFolder(resource)?.uri.fsPath : undefined;
this.outputChannel.appendLine(`Pip Installing ${productName} into ${getDisplayPath(interpreter.path)}`);
this.outputChannel.appendLine('>>>>>>>>>>>>>');
const disposables: IDisposable[] = [];
try {
const result = await service.execModuleObservable('pip', args, { cwd });
token.onCancellationRequested(() => result.proc?.kill(), this, disposables);
const subscription = result.out.subscribe((output) => {
if (token.isCancellationRequested) {
return;
}
this.outputChannel.appendLine(output.out);
});
disposables.push(new Disposable(() => subscription.unsubscribe()));
traceVerbose(`Successfully ran pip installer for ${productName}`);
return true;
} finally {
disposeAllDisposables(disposables);
this.outputChannel.appendLine('<<<<<<<<<<<<<<');
}
}
private getInstallerArgs(options: { reinstall: boolean; product: Product }): string[] {
const args: string[] = [];
const proxy = this.workspaceService.getConfiguration('http').get('proxy', '');
if (proxy.length > 0) {
args.push('--proxy');
args.push(proxy);
}
args.push(...['install', '-U']);
if (options.reinstall) {
args.push('--force-reinstall');
}
const moduleName = translateProductToModule(options.product);
return [...args, moduleName];
}
}
53 changes: 47 additions & 6 deletions src/client/common/installer/productInstaller.ts
Original file line number Diff line number Diff line change
@@ -1,20 +1,22 @@
/* eslint-disable max-classes-per-file */

import { inject, injectable, named } from 'inversify';
import { CancellationToken, Memento, OutputChannel, Uri } from 'vscode';
import { CancellationToken, CancellationTokenSource, Memento, OutputChannel, Uri } from 'vscode';
import { IPythonInstaller } from '../../api/types';
import '../../common/extensions';
import { InterpreterPackages } from '../../datascience/telemetry/interpreterPackages';
import { IServiceContainer } from '../../ioc/types';
import { logValue } from '../../logging/trace';
import { PythonEnvironment } from '../../pythonEnvironments/info';
import { getInterpreterHash } from '../../pythonEnvironments/info/interpreter';
import { IApplicationShell } from '../application/types';
import { IApplicationShell, IWorkspaceService } from '../application/types';
import { STANDARD_OUTPUT_CHANNEL } from '../constants';
import { disposeAllDisposables } from '../helpers';
import { traceDecorators, traceError, traceInfo } from '../logger';
import { IProcessServiceFactory, IPythonExecutionFactory } from '../process/types';
import {
IConfigurationService,
IDisposable,
IInstaller,
InstallerResponse,
IOutputChannel,
Expand All @@ -23,6 +25,7 @@ import {
} from '../types';
import { sleep } from '../utils/async';
import { isResource } from '../utils/misc';
import { BackupPipInstaller } from './backupPipInstaller';
import { ProductNames } from './productNames';
import { InterpreterUri, IProductPathService } from './types';

Expand Down Expand Up @@ -131,6 +134,17 @@ export abstract class BaseInstaller {
}

export class DataScienceInstaller extends BaseInstaller {
private readonly backupPipInstaller: BackupPipInstaller;
constructor(serviceContainer: IServiceContainer, outputChannel: OutputChannel) {
super(serviceContainer, outputChannel);
this.backupPipInstaller = new BackupPipInstaller(
serviceContainer.get<IApplicationShell>(IApplicationShell),
serviceContainer.get<IWorkspaceService>(IWorkspaceService),
outputChannel,
serviceContainer.get<IPythonExecutionFactory>(IPythonExecutionFactory),
this.isInstalled.bind(this)
);
}
// Override base installer to support a more DS-friendly streamlined installation.
public async install(
product: Product,
Expand All @@ -152,10 +166,37 @@ export class DataScienceInstaller extends BaseInstaller {
if (result === InstallerResponse.Disabled || result === InstallerResponse.Ignore) {
return result;
}
if (cancel?.isCancellationRequested) {
return InstallerResponse.Ignore;
}

return this.isInstalled(product, interpreter).then((isInstalled) =>
isInstalled ? InstallerResponse.Installed : InstallerResponse.Ignore
);
return this.isInstalled(product, interpreter).then(async (isInstalled) => {
if (isInstalled === false) {
const disposables: IDisposable[] = [];
// Try installing this ourselves if Python extension fails to instll it.
if (!cancel) {
const token = new CancellationTokenSource();
disposables.push(token);
cancel = token.token;
}
try {
const result = await this.backupPipInstaller.install(
product,
interpreter,
undefined,
reInstallAndUpdate === true,
cancel!
);
if (result) {
return InstallerResponse.Installed;
}
} finally {
disposeAllDisposables(disposables);
}
}

return isInstalled ? InstallerResponse.Installed : InstallerResponse.Ignore;
});
}
}

Expand Down Expand Up @@ -189,7 +230,7 @@ export class ProductInstaller implements IInstaller {
}

// eslint-disable-next-line complexity
function translateProductToModule(product: Product): string {
export function translateProductToModule(product: Product): string {
switch (product) {
case Product.jupyter:
return 'jupyter';
Expand Down
1 change: 1 addition & 0 deletions src/client/common/utils/localize.ts
Original file line number Diff line number Diff line change
Expand Up @@ -145,6 +145,7 @@ export namespace ExtensionSurveyBanner {
}

export namespace DataScience {
export const installingModule = localize('products.installingModule', 'Installing {0}');
export const warnWhenSelectingKernelWithUnSupportedPythonVersion = localize(
'DataScience.warnWhenSelectingKernelWithUnSupportedPythonVersion',
'The version of Python associated with the selected kernel is no longer supported. Please consider selecting a different kernel.'
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -67,7 +67,8 @@ export class DataViewerDependencyService {
): Promise<void> {
sendTelemetryEvent(Telemetry.PythonModuleInstal, undefined, {
action: 'displayed',
moduleName: ProductNames.get(Product.pandas)!
moduleName: ProductNames.get(Product.pandas)!,
pythonEnvType: interpreter?.envType
});
const selection = this.isCodeSpace
? Common.install()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -152,7 +152,8 @@ export class JupyterInterpreterDependencyService {
);
sendTelemetryEvent(Telemetry.PythonModuleInstal, undefined, {
action: 'displayed',
moduleName: ProductNames.get(Product.jupyter)!
moduleName: ProductNames.get(Product.jupyter)!,
pythonEnvType: interpreter.envType
});
sendTelemetryEvent(Telemetry.JupyterNotInstalledErrorShown);
const selection = await this.applicationShell.showErrorMessage(
Expand Down
27 changes: 18 additions & 9 deletions src/client/datascience/jupyter/kernels/kernelDependencyService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -180,7 +180,8 @@ export class KernelDependencyService implements IKernelDependencyService {
action: 'displayed',
moduleName: productNameForTelemetry,
resourceType,
resourceHash
resourceHash,
pythonEnvType: interpreter.envType
});
const promptCancellationPromise = createPromiseFromCancellation({
cancelAction: 'resolve',
Expand All @@ -197,7 +198,8 @@ export class KernelDependencyService implements IKernelDependencyService {
action: 'prompted',
moduleName: productNameForTelemetry,
resourceType,
resourceHash
resourceHash,
pythonEnvType: interpreter.envType
});
}
const selection = this.isCodeSpace
Expand All @@ -211,7 +213,8 @@ export class KernelDependencyService implements IKernelDependencyService {
action: 'dismissed',
moduleName: productNameForTelemetry,
resourceType,
resourceHash
resourceHash,
pythonEnvType: interpreter.envType
});
return KernelInterpreterDependencyResponse.cancel;
}
Expand All @@ -221,15 +224,17 @@ export class KernelDependencyService implements IKernelDependencyService {
action: 'differentKernel',
moduleName: productNameForTelemetry,
resourceType,
resourceHash
resourceHash,
pythonEnvType: interpreter.envType
});
return KernelInterpreterDependencyResponse.selectDifferentKernel;
} else if (selection === Common.install()) {
sendTelemetryEvent(Telemetry.PythonModuleInstal, undefined, {
action: 'install',
moduleName: productNameForTelemetry,
resourceType,
resourceHash
resourceHash,
pythonEnvType: interpreter.envType
});
const cancellationPromise = createPromiseFromCancellation({
cancelAction: 'resolve',
Expand All @@ -252,15 +257,17 @@ export class KernelDependencyService implements IKernelDependencyService {
action: 'installed',
moduleName: productNameForTelemetry,
resourceType,
resourceHash
resourceHash,
pythonEnvType: interpreter.envType
});
return KernelInterpreterDependencyResponse.ok;
} else if (response === InstallerResponse.Ignore) {
sendTelemetryEvent(Telemetry.PythonModuleInstal, undefined, {
action: 'failed',
moduleName: productNameForTelemetry,
resourceType,
resourceHash
resourceHash,
pythonEnvType: interpreter.envType
});
return KernelInterpreterDependencyResponse.failed; // Happens when errors in pip or conda.
}
Expand All @@ -270,7 +277,8 @@ export class KernelDependencyService implements IKernelDependencyService {
action: 'dismissed',
moduleName: productNameForTelemetry,
resourceType,
resourceHash
resourceHash,
pythonEnvType: interpreter.envType
});
return KernelInterpreterDependencyResponse.cancel;
} catch (ex) {
Expand All @@ -279,7 +287,8 @@ export class KernelDependencyService implements IKernelDependencyService {
action: 'error',
moduleName: productNameForTelemetry,
resourceType,
resourceHash
resourceHash,
pythonEnvType: interpreter.envType
});
throw ex;
}
Expand Down
3 changes: 3 additions & 0 deletions src/client/telemetry/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -828,13 +828,16 @@ export interface IEventNamePropertyMapping {
| 'donotinstall' // User chose not to install from prompt.
| 'differentKernel' // User chose to select a different kernel.
| 'error' // Some other error.
| 'installedInJupyter' // The package was successfully installed in Jupyter whilst failed to install in Python ext.
| 'failedToInstallInJupyter' // Failed to install the package in Jupyter as well as Python ext.
| 'dismissed'; // User chose to dismiss the prompt.
resourceType?: 'notebook' | 'interactive';
/**
* Hash of the resource (notebook.uri or pythonfile.uri associated with this).
* If we run the same notebook tomorrow, the hash will be the same.
*/
resourceHash?: string;
pythonEnvType?: EnvironmentType;
};
/**
* This telemetry tracks the display of the Picker for Jupyter Remote servers.
Expand Down

0 comments on commit c9dea22

Please sign in to comment.