Skip to content
This repository has been archived by the owner on Sep 27, 2022. It is now read-only.

Refactoring #1

Merged
merged 2 commits into from
Jul 19, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions package.nls.json
Original file line number Diff line number Diff line change
Expand Up @@ -673,8 +673,8 @@
},
"DataScience.serverNotStarted": "Not Started",
"DataScience.localJupyterServer": "local",
"DataScience.pandasTooOldForViewingFormat": "Python package 'pandas' is version {0}. Version 0.20 or greater is required for viewing data.",
"DataScience.pandasRequiredForViewing": "Python package 'pandas' is required for viewing data. Please ensure you have installed 'pandas' version {0} or above.",
"DataScience.pandasTooOldForViewingFormat": "Python package 'pandas' is version {0}. Version {1} or greater is required for viewing data.",
"DataScience.pandasRequiredForViewing": "Python package 'pandas' version {0} (or above) is required for viewing data.",
"DataScience.failedToGetVersionOfPandas": "Failed to get version of Pandas to use the Data Viewer.",
"DataScience.valuesColumn": "values",
"DataScience.liveShareInvalid": "One or more guests in the session do not have the Jupyter [extension](https://marketplace.visualstudio.com/itemdetails?itemName=ms-toolsai.jupyter) installed.\r\nYour Live Share session cannot continue and will be closed.",
Expand Down
4 changes: 2 additions & 2 deletions src/platform/common/utils/localize.ts
Original file line number Diff line number Diff line change
Expand Up @@ -800,15 +800,15 @@ export namespace DataScience {
key: 'DataScience.pandasTooOldForViewingFormat',
comment: ["{Locked='pandas'", 'This is the name of the pandas package']
},
"Python package 'pandas' is version {0}. Version 0.20 or greater is required for viewing data."
"Python package 'pandas' is version {0}. Version {1} or greater is required for viewing data."
);
export const pandasRequiredForViewing = () =>
localize(
{
key: 'DataScience.pandasRequiredForViewing',
comment: ["{Locked='pandas'", 'This is the name of the pandas package']
},
"Python package 'pandas' is required for viewing data. Please ensure you have installed 'pandas' version {0} or above."
"Python package 'pandas' version {0} (or above) is required for viewing data."
);
export const valuesColumn = () => localize('DataScience.valuesColumn', 'values');
export const liveShareInvalid = () =>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -80,7 +80,7 @@ suite('DataScience - DataViewerDependencyService (IKernel, Web)', () => {
const resultPromise = dependencyService.checkAndInstallMissingDependencies(kernel);
await assert.isRejected(
resultPromise,
DataScience.pandasTooOldForViewingFormat().format('0.20.'),
DataScience.pandasTooOldForViewingFormat().format('0.20.', pandasMinimumVersionSupportedByVariableViewer),
'Failed to identify too old pandas'
);
assert.deepEqual(
Expand All @@ -98,7 +98,7 @@ suite('DataScience - DataViewerDependencyService (IKernel, Web)', () => {
const resultPromise = dependencyService.checkAndInstallMissingDependencies(kernel);
await assert.isRejected(
resultPromise,
DataScience.pandasTooOldForViewingFormat().format('0.10.'),
DataScience.pandasTooOldForViewingFormat().format('0.10.', pandasMinimumVersionSupportedByVariableViewer),
'Failed to identify too old pandas'
);
assert.deepEqual(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -72,7 +72,10 @@ suite('DataScience - DataViewerDependencyService (PythonEnvironment, Node)', ()

const promise = dependencyService.checkAndInstallMissingDependencies(interpreter);

await assert.isRejected(promise, DataScience.pandasTooOldForViewingFormat().format('0.20.'));
await assert.isRejected(
promise,
DataScience.pandasTooOldForViewingFormat().format('0.20.', pandasMinimumVersionSupportedByVariableViewer)
);
});
test('Throw exception if pandas is installed and version is < 0.20', async () => {
when(
Expand All @@ -81,7 +84,10 @@ suite('DataScience - DataViewerDependencyService (PythonEnvironment, Node)', ()

const promise = dependencyService.checkAndInstallMissingDependencies(interpreter);

await assert.isRejected(promise, DataScience.pandasTooOldForViewingFormat().format('0.10.'));
await assert.isRejected(
promise,
DataScience.pandasTooOldForViewingFormat().format('0.10.', pandasMinimumVersionSupportedByVariableViewer)
);
});
test('Prompt to install pandas and install pandas', async () => {
when(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -98,7 +98,7 @@ suite('DataScience - DataViewerDependencyService (IKernel, Node)', () => {
const resultPromise = dependencyService.checkAndInstallMissingDependencies(kernel);
await assert.isRejected(
resultPromise,
DataScience.pandasTooOldForViewingFormat().format('0.20.'),
DataScience.pandasTooOldForViewingFormat().format('0.20.', pandasMinimumVersionSupportedByVariableViewer),
'Failed to identify too old pandas'
);
assert.deepEqual(
Expand All @@ -116,7 +116,7 @@ suite('DataScience - DataViewerDependencyService (IKernel, Node)', () => {
const resultPromise = dependencyService.checkAndInstallMissingDependencies(kernel);
await assert.isRejected(
resultPromise,
DataScience.pandasTooOldForViewingFormat().format('0.10.'),
DataScience.pandasTooOldForViewingFormat().format('0.10.', pandasMinimumVersionSupportedByVariableViewer),
'Failed to identify too old pandas'
);
assert.deepEqual(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,24 +9,86 @@ import { IKernel } from '../../../kernels/types';
import { IDataViewerDependencyService } from './types';
import { pandasMinimumVersionSupportedByVariableViewer } from './constants';
import { PythonEnvironment } from '../../../platform/pythonEnvironments/info';
import { parseSemVer } from '../../../platform/common/utils';
import { SemVer } from 'semver';
import { captureTelemetry, sendTelemetryEvent, Telemetry } from '../../../telemetry';
import { ProductNames } from '../../../kernels/installer/productNames';
import { Product } from '../../../kernels/installer/types';
import { CancellationToken, CancellationTokenSource } from 'vscode';
import { Cancellation } from '../../../platform/common/cancellation';
import { traceWarning } from '../../../platform/logging';

/**
* base class of the data viewer dependency implementation.
*/
export abstract class BaseDataViewerDependencyImplementation implements IDataViewerDependencyService {
export abstract class BaseDataViewerDependencyImplementation<TExecuter> implements IDataViewerDependencyService {
constructor(private readonly applicationShell: IApplicationShell, private isCodeSpace: boolean) {}

abstract checkAndInstallMissingDependencies(executionEnvironment: IKernel | PythonEnvironment): Promise<void>;

protected async promptInstall(): Promise<boolean> {
protected abstract _getVersion(executer: TExecuter, token: CancellationToken): Promise<string | undefined>;
protected abstract _doInstall(executer: TExecuter, tokenSource: CancellationTokenSource): Promise<void>;

protected async getVersion(executer: TExecuter, token: CancellationToken): Promise<SemVer | undefined> {
try {
const version = await this._getVersion(executer, token);
return typeof version === 'string' ? parseSemVer(version) : version;
} catch (e) {
traceWarning(DataScience.failedToGetVersionOfPandas(), e.message);
return;
}
}

@captureTelemetry(Telemetry.PythonModuleInstall, {
action: 'displayed',
moduleName: ProductNames.get(Product.pandas)!
})
protected async promptInstall(
executer: TExecuter,
tokenSource: CancellationTokenSource,
version?: string
): Promise<void> {
let message = version
? DataScience.pandasTooOldForViewingFormat().format(version, pandasMinimumVersionSupportedByVariableViewer)
: DataScience.pandasRequiredForViewing().format(pandasMinimumVersionSupportedByVariableViewer);

let selection = this.isCodeSpace
? Common.install()
: await this.applicationShell.showErrorMessage(
DataScience.pandasRequiredForViewing().format(pandasMinimumVersionSupportedByVariableViewer),
{ modal: true },
Common.install()
);
: await this.applicationShell.showErrorMessage(message, { modal: true }, Common.install());

if (selection === Common.install()) {
await this._doInstall(executer, tokenSource);
} else {
sendTelemetryEvent(Telemetry.UserDidNotInstallPandas);
throw new Error(message);
}
}

protected async checkOrInstall(executer: TExecuter): Promise<void> {
const tokenSource = new CancellationTokenSource();

try {
const pandasVersion = await this.getVersion(executer, tokenSource.token);

if (Cancellation.isCanceled(tokenSource.token)) {
sendTelemetryEvent(Telemetry.PandasInstallCanceled);
return;
}

return selection === Common.install();
if (pandasVersion) {
if (pandasVersion.compare(pandasMinimumVersionSupportedByVariableViewer) > 0) {
sendTelemetryEvent(Telemetry.PandasOK);
return;
}
sendTelemetryEvent(Telemetry.PandasTooOld);
// Warn user that we cannot start because pandas is too old.
const versionStr = `${pandasVersion.major}.${pandasVersion.minor}.${pandasVersion.build}`;
await this.promptInstall(executer, tokenSource, versionStr);
}
sendTelemetryEvent(Telemetry.PandasNotInstalled);
await this.promptInstall(executer, tokenSource);
} finally {
tokenSource.dispose();
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,26 +3,20 @@

'use strict';

import { SemVer } from 'semver';
import { CancellationToken, CancellationTokenSource } from 'vscode';
import { ProductNames } from '../../../kernels/installer/productNames';
import { IInstaller, Product, InstallerResponse } from '../../../kernels/installer/types';
import { IApplicationShell } from '../../../platform/common/application/types';
import { Cancellation, createPromiseFromCancellation } from '../../../platform/common/cancellation';
import { traceWarning } from '../../../platform/logging';
import { IPythonExecutionFactory } from '../../../platform/common/process/types.node';
import { parseSemVer } from '../../../platform/common/utils';
import { DataScience } from '../../../platform/common/utils/localize';
import { IInterpreterService } from '../../../platform/interpreter/contracts';
import { PythonEnvironment } from '../../../platform/pythonEnvironments/info';
import { sendTelemetryEvent, Telemetry } from '../../../telemetry';
import { pandasMinimumVersionSupportedByVariableViewer } from './constants';
import { BaseDataViewerDependencyImplementation } from './baseDataViewerDependencyImplementation';

/**
* Uses the Python interpreter to manage dependencies of a Data Viewer.
*/
export class InterpreterDataViewerDependencyImplementation extends BaseDataViewerDependencyImplementation {
export class InterpreterDataViewerDependencyImplementation extends BaseDataViewerDependencyImplementation<PythonEnvironment> {
constructor(
private readonly installer: IInstaller,
private pythonFactory: IPythonExecutionFactory,
Expand All @@ -33,48 +27,23 @@ export class InterpreterDataViewerDependencyImplementation extends BaseDataViewe
super(applicationShell, isCodeSpace);
}

public async checkAndInstallMissingDependencies(interpreter: PythonEnvironment): Promise<void> {
sendTelemetryEvent(Telemetry.DataViewerUsingInterpreter);

const tokenSource = new CancellationTokenSource();
try {
const pandasVersion = await this.getVersion(interpreter, tokenSource.token);

if (Cancellation.isCanceled(tokenSource.token)) {
sendTelemetryEvent(Telemetry.PandasInstallCanceled);
return;
}

if (pandasVersion) {
if (pandasVersion.compare(pandasMinimumVersionSupportedByVariableViewer) > 0) {
sendTelemetryEvent(Telemetry.PandasOK);
return;
}
sendTelemetryEvent(Telemetry.PandasTooOld);
// Warn user that we cannot start because pandas is too old.
const versionStr = `${pandasVersion.major}.${pandasVersion.minor}.${pandasVersion.build}`;
throw new Error(DataScience.pandasTooOldForViewingFormat().format(versionStr));
}

sendTelemetryEvent(Telemetry.PandasNotInstalled);
await this.installMissingDependencies(interpreter, tokenSource);
} finally {
tokenSource.dispose();
}
}

private async installMissingDependencies(
protected async _getVersion(
interpreter: PythonEnvironment,
tokenSource: CancellationTokenSource
): Promise<void> {
sendTelemetryEvent(Telemetry.PythonModuleInstall, undefined, {
action: 'displayed',
moduleName: ProductNames.get(Product.pandas)!,
pythonEnvType: interpreter?.envType
token?: CancellationToken
): Promise<string | undefined> {
const launcher = await this.pythonFactory.createActivatedEnvironment({
resource: undefined,
interpreter,
allowEnvironmentFetchExceptions: true
});
const result = await launcher.exec(['-c', 'import pandas;print(pandas.__version__)'], {
throwOnStdErr: true,
token
});
return result.stdout;
}

const doInstall = await this.promptInstall();

protected async _doInstall(interpreter: PythonEnvironment, tokenSource: CancellationTokenSource): Promise<void> {
// All data science dependencies require an interpreter to be passed in
// Default to the active interpreter if no interpreter is available
const interpreterToInstallDependenciesInto =
Expand All @@ -84,44 +53,24 @@ export class InterpreterDataViewerDependencyImplementation extends BaseDataViewe
return;
}

if (doInstall) {
const cancellationPromise = createPromiseFromCancellation({
cancelAction: 'resolve',
defaultValue: InstallerResponse.Ignore,
token: tokenSource.token
});
// Always pass a cancellation token to `install`, to ensure it waits until the module is installed.
const response = await Promise.race([
this.installer.install(Product.pandas, interpreterToInstallDependenciesInto, tokenSource),
cancellationPromise
]);
if (response === InstallerResponse.Installed) {
sendTelemetryEvent(Telemetry.UserInstalledPandas);
}
} else {
sendTelemetryEvent(Telemetry.UserDidNotInstallPandas);
throw new Error(
DataScience.pandasRequiredForViewing().format(pandasMinimumVersionSupportedByVariableViewer)
);
const cancellationPromise = createPromiseFromCancellation({
cancelAction: 'resolve',
defaultValue: InstallerResponse.Ignore,
token: tokenSource.token
});
// Always pass a cancellation token to `install`, to ensure it waits until the module is installed.
const response = await Promise.race([
this.installer.install(Product.pandas, interpreterToInstallDependenciesInto, tokenSource),
cancellationPromise
]);
if (response === InstallerResponse.Installed) {
sendTelemetryEvent(Telemetry.UserInstalledPandas);
}
}

private async getVersion(interpreter: PythonEnvironment, token?: CancellationToken): Promise<SemVer | undefined> {
const launcher = await this.pythonFactory.createActivatedEnvironment({
resource: undefined,
interpreter,
allowEnvironmentFetchExceptions: true
});
try {
const result = await launcher.exec(['-c', 'import pandas;print(pandas.__version__)'], {
throwOnStdErr: true,
token
});
public async checkAndInstallMissingDependencies(interpreter: PythonEnvironment): Promise<void> {
sendTelemetryEvent(Telemetry.DataViewerUsingInterpreter);

return parseSemVer(result.stdout);
} catch (ex) {
traceWarning('Failed to get version of Pandas to use Data Viewer', ex);
return;
}
await this.checkOrInstall(interpreter);
}
}
Loading