From 919677c065a36d8789ff934d6430f9f0617c4c86 Mon Sep 17 00:00:00 2001 From: David Kutugata Date: Tue, 2 Jul 2019 14:12:28 -0700 Subject: [PATCH 01/19] fixed issue 5682, promt user to install jupyter if its not found --- news/2 Fixes/5682.md | 1 + package.nls.json | 2 + src/client/common/installer/productNames.ts | 1 + src/client/common/types.ts | 3 +- src/client/common/utils/localize.ts | 2 + .../interactive-window/interactiveWindow.ts | 125 +++++++++++------- 6 files changed, 84 insertions(+), 50 deletions(-) create mode 100644 news/2 Fixes/5682.md diff --git a/news/2 Fixes/5682.md b/news/2 Fixes/5682.md new file mode 100644 index 000000000000..ca11f285eda2 --- /dev/null +++ b/news/2 Fixes/5682.md @@ -0,0 +1 @@ +The extension will now prompt to auto install jupyter in case its not found. diff --git a/package.nls.json b/package.nls.json index 7111cabb75b8..d220976a026d 100644 --- a/package.nls.json +++ b/package.nls.json @@ -141,6 +141,8 @@ "Linter.enableLinter": "Enable {0}", "Linter.enablePylint": "You have a pylintrc file in your workspace. Do you want to enable pylint?", "Linter.replaceWithSelectedLinter": "Multiple linters are enabled in settings. Replace with '{0}'?", + "DataScience.jupyterInstall": "Install", + "DataScience.jupyterInstallError": "There was an error installing Jupyter.", "DataScience.jupyterSelectURILaunchLocal": "Launch a local Jupyter server when needed", "DataScience.jupyterSelectURISpecifyURI": "Type in the URI to connect to a running Jupyter server", "DataScience.jupyterSelectURIPrompt": "Enter the URI of a Jupyter server", diff --git a/src/client/common/installer/productNames.ts b/src/client/common/installer/productNames.ts index f8800d347d73..43004aa82e1b 100644 --- a/src/client/common/installer/productNames.ts +++ b/src/client/common/installer/productNames.ts @@ -19,3 +19,4 @@ ProductNames.set(Product.pylint, 'pylint'); ProductNames.set(Product.pytest, 'pytest'); ProductNames.set(Product.yapf, 'yapf'); ProductNames.set(Product.rope, 'rope'); +ProductNames.set(Product.jupyter, 'jupyter'); diff --git a/src/client/common/types.ts b/src/client/common/types.ts index 541568cfec37..12b30a52ebab 100644 --- a/src/client/common/types.ts +++ b/src/client/common/types.ts @@ -95,7 +95,8 @@ export enum Product { rope = 14, isort = 15, black = 16, - bandit = 17 + bandit = 17, + jupyter = 18 } export enum ModuleNamePurpose { diff --git a/src/client/common/utils/localize.ts b/src/client/common/utils/localize.ts index 62f6c5f67443..e8cb1f164544 100644 --- a/src/client/common/utils/localize.ts +++ b/src/client/common/utils/localize.ts @@ -100,6 +100,8 @@ export namespace DataScience { export const notebookCheckForImportYes = localize('DataScience.notebookCheckForImportYes', 'Import'); export const notebookCheckForImportNo = localize('DataScience.notebookCheckForImportNo', 'Later'); export const notebookCheckForImportDontAskAgain = localize('DataScience.notebookCheckForImportDontAskAgain', 'Don\'t Ask Again'); + export const jupyterInstall = localize('DataScience.jupyterAskToInstall', 'Install'); + export const jupyterInstallError = localize('DataScience.jupyterInstallError', 'There was an error installing Jupyter.'); export const jupyterNotSupported = localize('DataScience.jupyterNotSupported', 'Jupyter is not installed'); export const jupyterNotSupportedBecauseOfEnvironment = localize('DataScience.jupyterNotSupportedBecauseOfEnvironment', 'Activating {0} to run Jupyter failed with {1}'); export const jupyterNbConvertNotSupported = localize('DataScience.jupyterNbConvertNotSupported', 'Jupyter nbconvert is not installed'); diff --git a/src/client/datascience/interactive-window/interactiveWindow.ts b/src/client/datascience/interactive-window/interactiveWindow.ts index 5ebe5a9d0679..91bd32613cec 100644 --- a/src/client/datascience/interactive-window/interactiveWindow.ts +++ b/src/client/datascience/interactive-window/interactiveWindow.ts @@ -24,13 +24,15 @@ import { import { CancellationError } from '../../common/cancellation'; import { EXTENSION_ROOT_DIR, PYTHON_LANGUAGE } from '../../common/constants'; import { ContextKey } from '../../common/contextKey'; +import { IInstallationChannelManager } from '../../common/installer/types'; import { traceInfo, traceWarning } from '../../common/logger'; import { IFileSystem } from '../../common/platform/types'; -import { IConfigurationService, IDisposableRegistry, ILogger } from '../../common/types'; +import { IConfigurationService, IDisposableRegistry, ILogger, Product } from '../../common/types'; import { createDeferred, Deferred } from '../../common/utils/async'; import * as localize from '../../common/utils/localize'; import { StopWatch } from '../../common/utils/stopWatch'; import { IInterpreterService, PythonInterpreter } from '../../interpreter/contracts'; +import { IServiceContainer } from '../../ioc/types'; import { captureTelemetry, sendTelemetryEvent } from '../../telemetry'; import { generateCellRanges } from '../cellFactory'; import { EditorContexts, Identifiers, Telemetry } from '../constants'; @@ -77,8 +79,8 @@ import { } from './interactiveWindowTypes'; @injectable() -export class InteractiveWindow extends WebViewHost implements IInteractiveWindow { - private static sentExecuteCellTelemetry : boolean = false; +export class InteractiveWindow extends WebViewHost implements IInteractiveWindow { + private static sentExecuteCellTelemetry: boolean = false; private disposed: boolean = false; private loadPromise: Promise; private interpreterChangedDisposable: Disposable; @@ -89,14 +91,14 @@ export class InteractiveWindow extends WebViewHost im private addSysInfoPromise: Deferred | undefined; private waitingForExportCells: boolean = false; private jupyterServer: INotebookServer | undefined; - private id : string; + private id: string; private executeEvent: EventEmitter = new EventEmitter(); private variableRequestStopWatch: StopWatch | undefined; private variableRequestPendingCount: number = 0; constructor( @multiInject(IInteractiveWindowListener) private readonly listeners: IInteractiveWindowListener[], - @inject(ILiveShareApi) private liveShare : ILiveShareApi, + @inject(ILiveShareApi) private liveShare: ILiveShareApi, @inject(IApplicationShell) private applicationShell: IApplicationShell, @inject(IDocumentManager) private documentManager: IDocumentManager, @inject(IInterpreterService) private interpreterService: IInterpreterService, @@ -116,8 +118,9 @@ export class InteractiveWindow extends WebViewHost im @inject(IDataViewerProvider) private dataExplorerProvider: IDataViewerProvider, @inject(IJupyterVariables) private jupyterVariables: IJupyterVariables, @inject(INotebookImporter) private jupyterImporter: INotebookImporter, - @inject(IJupyterDebugger) private jupyterDebugger: IJupyterDebugger - ) { + @inject(IJupyterDebugger) private jupyterDebugger: IJupyterDebugger, + @inject(IServiceContainer) protected serviceContainer: IServiceContainer + ) { super( configuration, provider, @@ -153,7 +156,7 @@ export class InteractiveWindow extends WebViewHost im this.listeners.forEach(l => l.postMessage((e) => this.postMessageInternal(e.message, e.payload))); } - public get ready() : Promise { + public get ready(): Promise { // We need this to ensure the interactive window is up and ready to receive messages. return this.loadPromise; } @@ -175,16 +178,16 @@ export class InteractiveWindow extends WebViewHost im return this.closedEvent.event; } - public get onExecutedCode() : Event { + public get onExecutedCode(): Event { return this.executeEvent.event; } - public addCode(code: string, file: string, line: number, editor?: TextEditor, runningStopWatch?: StopWatch) : Promise { + public addCode(code: string, file: string, line: number, editor?: TextEditor, runningStopWatch?: StopWatch): Promise { // Call the internal method. return this.submitCode(code, file, line, undefined, editor, runningStopWatch, false); } - public debugCode(code: string, file: string, line: number, editor?: TextEditor, runningStopWatch?: StopWatch) : Promise { + public debugCode(code: string, file: string, line: number, editor?: TextEditor, runningStopWatch?: StopWatch): Promise { // Call the internal method. return this.submitCode(code, file, line, undefined, editor, runningStopWatch, true); } @@ -364,7 +367,7 @@ export class InteractiveWindow extends WebViewHost im } @captureTelemetry(Telemetry.RestartKernel) - public async restartKernel() : Promise { + public async restartKernel(): Promise { if (this.jupyterServer && !this.restartingKernel) { if (this.shouldAskForRestart()) { // Ask the user if they want us to restart or not. @@ -389,7 +392,7 @@ export class InteractiveWindow extends WebViewHost im } @captureTelemetry(Telemetry.Interrupt) - public async interruptKernel() : Promise { + public async interruptKernel(): Promise { if (this.jupyterServer && !this.restartingKernel) { const status = this.statusProvider.set(localize.DataScience.interruptKernelStatus()); @@ -421,7 +424,7 @@ export class InteractiveWindow extends WebViewHost im } } - public async previewNotebook(file: string) : Promise { + public async previewNotebook(file: string): Promise { try { // First convert to a python file to verify this file is valid. This is // an easy way to have something else verify the validity of the file. @@ -503,8 +506,8 @@ export class InteractiveWindow extends WebViewHost im } } - private addMessage(message: string, type: 'preview' | 'execute') : void { - const cell : ICell = { + private addMessage(message: string, type: 'preview' | 'execute'): void { + const cell: ICell = { id: uuid(), file: Identifiers.EmptyFileName, line: 0, @@ -522,17 +525,17 @@ export class InteractiveWindow extends WebViewHost im this.onAddCodeEvent([cell]); } - private addPreviewHeader(file: string) : void { + private addPreviewHeader(file: string): void { const message = localize.DataScience.previewHeader().format(file); this.addMessage(message, 'preview'); } - private addPreviewFooter(file: string) : void { + private addPreviewFooter(file: string): void { const message = localize.DataScience.previewFooter().format(file); this.addMessage(message, 'preview'); } - private async checkPandas() : Promise { + private async checkPandas(): Promise { const pandasVersion = await this.dataExplorerProvider.getPandasVersion(); if (!pandasVersion) { sendTelemetryEvent(Telemetry.PandasNotInstalled); @@ -562,7 +565,7 @@ export class InteractiveWindow extends WebViewHost im } } - private async checkColumnSize(columnSize: number) : Promise { + private async checkColumnSize(columnSize: number): Promise { if (columnSize > ColumnWarningSize && this.shouldAskForLargeData()) { const message = localize.DataScience.tooManyColumnsMessage(); const yes = localize.DataScience.tooManyColumnsYes(); @@ -578,7 +581,7 @@ export class InteractiveWindow extends WebViewHost im return true; } - private async showDataViewer(request: IShowDataViewer) : Promise { + private async showDataViewer(request: IShowDataViewer): Promise { try { if (await this.checkPandas() && await this.checkColumnSize(request.columnSize)) { await this.dataExplorerProvider.create(request.variableName); @@ -589,13 +592,13 @@ export class InteractiveWindow extends WebViewHost im } // tslint:disable-next-line:no-any - private dispatchMessage(_message: T, payload: any, handler: (args : M[T]) => void) { + private dispatchMessage(_message: T, payload: any, handler: (args: M[T]) => void) { const args = payload as M[T]; handler.bind(this)(args); } // tslint:disable-next-line:no-any - private onAddedSysInfo(sysInfo : IAddedSysInfo) { + private onAddedSysInfo(sysInfo: IAddedSysInfo) { // See if this is from us or not. if (sysInfo.id !== this.id) { @@ -701,12 +704,12 @@ export class InteractiveWindow extends WebViewHost im // Activate the other side, and send as if came from a file this.interactiveWindowProvider.getOrCreateActive().then(_v => { - this.shareMessage(InteractiveWindowMessages.RemoteAddCode, {code: info.code, file: Identifiers.EmptyFileName, line: 0, id: info.id, originator: this.id}); + this.shareMessage(InteractiveWindowMessages.RemoteAddCode, { code: info.code, file: Identifiers.EmptyFileName, line: 0, id: info.id, originator: this.id }); }).ignoreErrors(); } } - private async submitCode(code: string, file: string, line: number, id?: string, _editor?: TextEditor, runningStopWatch?: StopWatch, debug?: boolean) : Promise { + private async submitCode(code: string, file: string, line: number, id?: string, _editor?: TextEditor, runningStopWatch?: StopWatch, debug?: boolean): Promise { this.logger.logInformation(`Submitting code for ${this.id}`); // Start a status item @@ -715,7 +718,7 @@ export class InteractiveWindow extends WebViewHost im // Transmit this submission to all other listeners (in a live share session) if (!id) { id = uuid(); - this.shareMessage(InteractiveWindowMessages.RemoteAddCode, {code, file, line, id, originator: this.id}); + this.shareMessage(InteractiveWindowMessages.RemoteAddCode, { code, file, line, id, originator: this.id }); } // Create a deferred object that will wait until the status is disposed @@ -790,11 +793,11 @@ export class InteractiveWindow extends WebViewHost im traceInfo(`Finished execution for ${id}`); } } catch (err) { - status.dispose(); - const message = localize.DataScience.executingCodeFailure().format(err); this.applicationShell.showErrorMessage(message); } finally { + status.dispose(); + if (debug) { if (this.jupyterServer) { await this.jupyterDebugger.stopDebugging(this.jupyterServer); @@ -820,7 +823,7 @@ export class InteractiveWindow extends WebViewHost im return result; } - private logTelemetry = (event : Telemetry) => { + private logTelemetry = (event: Telemetry) => { sendTelemetryEvent(event); } @@ -871,7 +874,7 @@ export class InteractiveWindow extends WebViewHost im this.loadPromise = this.reloadWithNew(); } - private async reloadWithNew() : Promise { + private async reloadWithNew(): Promise { const status = this.setStatus(localize.DataScience.startingJupyter()); try { // Not the same as reload, we need to actually dispose the server. @@ -890,7 +893,7 @@ export class InteractiveWindow extends WebViewHost im } } - private async reloadAfterShutdown() : Promise { + private async reloadAfterShutdown(): Promise { try { if (this.loadPromise) { await this.loadPromise; @@ -1003,7 +1006,7 @@ export class InteractiveWindow extends WebViewHost im } } - private showInformationMessage(message: string, question?: string) : Thenable { + private showInformationMessage(message: string, question?: string): Thenable { if (question) { return this.applicationShell.showInformationMessage(message, question); } else { @@ -1161,8 +1164,8 @@ export class InteractiveWindow extends WebViewHost im } } - private async checkUsable() : Promise { - let activeInterpreter : PythonInterpreter | undefined; + private async checkUsable(): Promise { + let activeInterpreter: PythonInterpreter | undefined; try { activeInterpreter = await this.interpreterService.getActiveInterpreter(); const usableInterpreter = await this.jupyterExecution.getUsableJupyterPython(); @@ -1196,24 +1199,50 @@ export class InteractiveWindow extends WebViewHost im } private load = async (): Promise => { - // Status depends upon if we're about to connect to existing server or not. - const status = (await this.jupyterExecution.getServer(await this.interactiveWindowProvider.getNotebookOptions())) ? - this.setStatus(localize.DataScience.connectingToJupyter()) : this.setStatus(localize.DataScience.startingJupyter()); - // Check to see if we support ipykernel or not try { const usable = await this.checkUsable(); if (!usable) { // Not loading anymore - status.dispose(); + this.dispose(); - // Indicate failing. - throw new JupyterInstallError(localize.DataScience.jupyterNotSupported(), localize.DataScience.pythonInteractiveHelpLink()); - } + const response = await this.applicationShell.showInformationMessage( + localize.DataScience.jupyterNotSupported(), + localize.DataScience.jupyterInstall(), + localize.DataScience.notebookCheckForImportNo()); - // Then load the jupyter server - await this.loadJupyterServer(); + if (response === localize.DataScience.jupyterInstall()) { + const channels = this.serviceContainer.get(IInstallationChannelManager); + const installer = await channels.getInstallationChannel(Product.jupyter); + if (!installer) { + // Indicate failing. + throw new JupyterInstallError(localize.DataScience.jupyterInstallError(), localize.DataScience.pythonInteractiveHelpLink()); + } + + const moduleName = 'jupyter'; + const logger = this.serviceContainer.get(ILogger); + await installer.installModule(moduleName) + .catch(logger.logError.bind(logger, `Error in installing the module '${moduleName}'`)); + + await this.loadJupyterServer(); + } else { + throw new JupyterInstallError(localize.DataScience.jupyterNotSupported(), localize.DataScience.pythonInteractiveHelpLink()); + } + } else { + // Status depends upon if we're about to connect to existing server or not. + const status = (await this.jupyterExecution.getServer(await this.interactiveWindowProvider.getNotebookOptions())) ? + this.setStatus(localize.DataScience.connectingToJupyter()) : this.setStatus(localize.DataScience.startingJupyter()); + + try { + // Then load the jupyter server + await this.loadJupyterServer(); + } catch (e) { + throw e; + } finally { + status.dispose(); + } + } } catch (e) { if (e instanceof JupyterSelfCertsError) { // On a self cert error, warn the user and ask if they want to change the setting @@ -1233,8 +1262,6 @@ export class InteractiveWindow extends WebViewHost im } else { throw e; } - } finally { - status.dispose(); } } @@ -1243,7 +1270,7 @@ export class InteractiveWindow extends WebViewHost im // Request our new list of variables const vars: IJupyterVariable[] = await this.jupyterVariables.getVariables(); - const variablesResponse: IJupyterVariablesResponse = {executionCount: requestExecutionCount, variables: vars }; + const variablesResponse: IJupyterVariablesResponse = { executionCount: requestExecutionCount, variables: vars }; // Tag all of our jupyter variables with the execution count of the request variablesResponse.variables.forEach((value: IJupyterVariable) => { @@ -1256,7 +1283,7 @@ export class InteractiveWindow extends WebViewHost im if (excludeString) { const excludeArray = excludeString.split(';'); variablesResponse.variables = variablesResponse.variables.filter((value) => { - return excludeArray.indexOf(value.type) === -1; + return excludeArray.indexOf(value.type) === -1; }); } this.variableRequestPendingCount = variablesResponse.variables.length; @@ -1305,7 +1332,7 @@ export class InteractiveWindow extends WebViewHost im }); } - private async requestOnigasm() : Promise { + private async requestOnigasm(): Promise { // Look for the file next or our current file (this is where it's installed in the vsix) let filePath = path.join(__dirname, 'node_modules', 'onigasm', 'lib', 'onigasm.wasm'); traceInfo(`Request for onigasm file at ${filePath}`); From 951c71f24ca40511123026e729c98bb6962aeb16 Mon Sep 17 00:00:00 2001 From: David Kutugata Date: Tue, 2 Jul 2019 17:22:16 -0700 Subject: [PATCH 02/19] moved the jupyter installation to codewatcher --- .../editor-integration/codewatcher.ts | 75 ++++--- .../interactive-window/interactiveWindow.ts | 54 ++--- .../codewatcher.unit.test.ts | 202 +++++++++--------- 3 files changed, 162 insertions(+), 169 deletions(-) diff --git a/src/client/datascience/editor-integration/codewatcher.ts b/src/client/datascience/editor-integration/codewatcher.ts index 0ff6d8012b3c..d2cd2d2b9fe6 100644 --- a/src/client/datascience/editor-integration/codewatcher.ts +++ b/src/client/datascience/editor-integration/codewatcher.ts @@ -5,11 +5,13 @@ import { inject, injectable } from 'inversify'; import { CodeLens, Command, Position, Range, Selection, TextDocument, TextEditor, TextEditorRevealType } from 'vscode'; import { IApplicationShell, IDocumentManager } from '../../common/application/types'; +import { IInstallationChannelManager } from '../../common/installer/types'; import { IFileSystem } from '../../common/platform/types'; -import { IConfigurationService, IDataScienceSettings, ILogger } from '../../common/types'; +import { IConfigurationService, IDataScienceSettings, ILogger, Product } from '../../common/types'; import * as localize from '../../common/utils/localize'; import { noop } from '../../common/utils/misc'; import { StopWatch } from '../../common/utils/stopWatch'; +import { IServiceContainer } from '../../ioc/types'; import { captureTelemetry } from '../../telemetry'; import { ICodeExecutionHelper } from '../../terminals/types'; import { generateCellRanges } from '../cellFactory'; @@ -27,13 +29,14 @@ export class CodeWatcher implements ICodeWatcher { private cachedSettings: IDataScienceSettings | undefined; constructor(@inject(IApplicationShell) private applicationShell: IApplicationShell, - @inject(ILogger) private logger: ILogger, - @inject(IInteractiveWindowProvider) private interactiveWindowProvider : IInteractiveWindowProvider, - @inject(IFileSystem) private fileSystem: IFileSystem, - @inject(IConfigurationService) private configService: IConfigurationService, - @inject(IDocumentManager) private documentManager : IDocumentManager, - @inject(ICodeExecutionHelper) private executionHelper: ICodeExecutionHelper - ) { + @inject(ILogger) private logger: ILogger, + @inject(IInteractiveWindowProvider) private interactiveWindowProvider: IInteractiveWindowProvider, + @inject(IFileSystem) private fileSystem: IFileSystem, + @inject(IConfigurationService) private configService: IConfigurationService, + @inject(IDocumentManager) private documentManager: IDocumentManager, + @inject(ICodeExecutionHelper) private executionHelper: ICodeExecutionHelper, + @inject(IServiceContainer) protected serviceContainer: IServiceContainer + ) { } public setDocument(document: TextDocument) { @@ -86,7 +89,7 @@ export class CodeWatcher implements ICodeWatcher { return this.version; } - public getCachedSettings() : IDataScienceSettings | undefined { + public getCachedSettings(): IDataScienceSettings | undefined { return this.cachedSettings; } @@ -172,13 +175,13 @@ export class CodeWatcher implements ICodeWatcher { } @captureTelemetry(Telemetry.RunSelectionOrLine) - public async runSelectionOrLine(activeEditor : TextEditor | undefined) { + public async runSelectionOrLine(activeEditor: TextEditor | undefined) { if (this.document && activeEditor && this.fileSystem.arePathsSame(activeEditor.document.fileName, this.document.fileName)) { // Get just the text of the selection or the current line if none const codeToExecute = await this.executionHelper.getSelectedTextToExecute(activeEditor); if (!codeToExecute) { - return ; + return; } const normalizedCode = await this.executionHelper.normalizeLines(codeToExecute!); if (!normalizedCode || normalizedCode.trim().length === 0) { @@ -213,7 +216,7 @@ export class CodeWatcher implements ICodeWatcher { } @captureTelemetry(Telemetry.RunCell) - public runCell(range: Range) : Promise { + public runCell(range: Range): Promise { if (!this.documentManager.activeTextEditor || !this.documentManager.activeTextEditor.document) { return Promise.resolve(); } @@ -224,7 +227,7 @@ export class CodeWatcher implements ICodeWatcher { } @captureTelemetry(Telemetry.RunCurrentCell) - public runCurrentCell() : Promise { + public runCurrentCell(): Promise { if (!this.documentManager.activeTextEditor || !this.documentManager.activeTextEditor.document) { return Promise.resolve(); } @@ -243,7 +246,7 @@ export class CodeWatcher implements ICodeWatcher { return this.runMatchingCell(this.documentManager.activeTextEditor.selection, true); } - public async addEmptyCellToBottom() : Promise { + public async addEmptyCellToBottom(): Promise { const editor = this.documentManager.activeTextEditor; if (editor) { editor.edit((editBuilder) => { @@ -255,7 +258,7 @@ export class CodeWatcher implements ICodeWatcher { } } - private async addCode(code: string, file: string, line: number, editor?: TextEditor, debug?: boolean) : Promise { + private async addCode(code: string, file: string, line: number, editor?: TextEditor, debug?: boolean): Promise { try { const stopWatch = new StopWatch(); const activeInteractiveWindow = await this.interactiveWindowProvider.getOrCreateActive(); @@ -298,11 +301,11 @@ export class CodeWatcher implements ICodeWatcher { } } - private getCurrentCellLens(pos: Position) : CodeLens | undefined { + private getCurrentCellLens(pos: Position): CodeLens | undefined { return this.codeLenses.find(l => l.range.contains(pos) && l.command !== undefined && l.command.command === Commands.RunCell); } - private getNextCellLens(pos: Position) : CodeLens | undefined { + private getNextCellLens(pos: Position): CodeLens | undefined { const currentIndex = this.codeLenses.findIndex(l => l.range.contains(pos) && l.command !== undefined && l.command.command === Commands.RunCell); if (currentIndex >= 0) { return this.codeLenses.find((l: CodeLens, i: number) => l.command !== undefined && l.command.command === Commands.RunCell && i > currentIndex); @@ -318,17 +321,35 @@ export class CodeWatcher implements ICodeWatcher { } // tslint:disable-next-line:no-any - private handleError = (err : any) => { + private handleError = (err: any) => { if (err instanceof JupyterInstallError) { - const jupyterError = err as JupyterInstallError; - - // This is a special error that shows a link to open for more help - this.applicationShell.showErrorMessage(jupyterError.message, jupyterError.actionTitle).then(v => { - // User clicked on the link, open it. - if (v === jupyterError.actionTitle) { - this.applicationShell.openUrl(jupyterError.action); - } - }); + this.applicationShell.showInformationMessage( + localize.DataScience.jupyterNotSupported(), + localize.DataScience.jupyterInstall(), + localize.DataScience.notebookCheckForImportNo()) + .then(response => { + if (response === localize.DataScience.jupyterInstall()) { + const channels = this.serviceContainer.get(IInstallationChannelManager); + return channels.getInstallationChannel(Product.jupyter); + } else { + const jupyterError = err as JupyterInstallError; + + // This is a special error that shows a link to open for more help + this.applicationShell.showErrorMessage(jupyterError.message, jupyterError.actionTitle).then(v => { + // User clicked on the link, open it. + if (v === jupyterError.actionTitle) { + this.applicationShell.openUrl(jupyterError.action); + } + }); + } + }).then(inst => { + if (inst) { + inst.installModule('jupyter') + .catch(() => this.applicationShell.showErrorMessage( + localize.DataScience.jupyterInstallError(), + localize.DataScience.pythonInteractiveHelpLink())); + } + }); } else if (err instanceof JupyterSelfCertsError) { // Don't show the message for self cert errors noop(); diff --git a/src/client/datascience/interactive-window/interactiveWindow.ts b/src/client/datascience/interactive-window/interactiveWindow.ts index 91bd32613cec..903fd5876b0c 100644 --- a/src/client/datascience/interactive-window/interactiveWindow.ts +++ b/src/client/datascience/interactive-window/interactiveWindow.ts @@ -24,15 +24,13 @@ import { import { CancellationError } from '../../common/cancellation'; import { EXTENSION_ROOT_DIR, PYTHON_LANGUAGE } from '../../common/constants'; import { ContextKey } from '../../common/contextKey'; -import { IInstallationChannelManager } from '../../common/installer/types'; import { traceInfo, traceWarning } from '../../common/logger'; import { IFileSystem } from '../../common/platform/types'; -import { IConfigurationService, IDisposableRegistry, ILogger, Product } from '../../common/types'; +import { IConfigurationService, IDisposableRegistry, ILogger } from '../../common/types'; import { createDeferred, Deferred } from '../../common/utils/async'; import * as localize from '../../common/utils/localize'; import { StopWatch } from '../../common/utils/stopWatch'; import { IInterpreterService, PythonInterpreter } from '../../interpreter/contracts'; -import { IServiceContainer } from '../../ioc/types'; import { captureTelemetry, sendTelemetryEvent } from '../../telemetry'; import { generateCellRanges } from '../cellFactory'; import { EditorContexts, Identifiers, Telemetry } from '../constants'; @@ -118,8 +116,7 @@ export class InteractiveWindow extends WebViewHost im @inject(IDataViewerProvider) private dataExplorerProvider: IDataViewerProvider, @inject(IJupyterVariables) private jupyterVariables: IJupyterVariables, @inject(INotebookImporter) private jupyterImporter: INotebookImporter, - @inject(IJupyterDebugger) private jupyterDebugger: IJupyterDebugger, - @inject(IServiceContainer) protected serviceContainer: IServiceContainer + @inject(IJupyterDebugger) private jupyterDebugger: IJupyterDebugger ) { super( configuration, @@ -1199,50 +1196,23 @@ export class InteractiveWindow extends WebViewHost im } private load = async (): Promise => { + // Status depends upon if we're about to connect to existing server or not. + const status = (await this.jupyterExecution.getServer(await this.interactiveWindowProvider.getNotebookOptions())) ? + this.setStatus(localize.DataScience.connectingToJupyter()) : this.setStatus(localize.DataScience.startingJupyter()); + // Check to see if we support ipykernel or not try { const usable = await this.checkUsable(); if (!usable) { // Not loading anymore + status.dispose(); this.dispose(); - const response = await this.applicationShell.showInformationMessage( - localize.DataScience.jupyterNotSupported(), - localize.DataScience.jupyterInstall(), - localize.DataScience.notebookCheckForImportNo()); - - if (response === localize.DataScience.jupyterInstall()) { - const channels = this.serviceContainer.get(IInstallationChannelManager); - const installer = await channels.getInstallationChannel(Product.jupyter); - - if (!installer) { - // Indicate failing. - throw new JupyterInstallError(localize.DataScience.jupyterInstallError(), localize.DataScience.pythonInteractiveHelpLink()); - } - - const moduleName = 'jupyter'; - const logger = this.serviceContainer.get(ILogger); - await installer.installModule(moduleName) - .catch(logger.logError.bind(logger, `Error in installing the module '${moduleName}'`)); - - await this.loadJupyterServer(); - } else { - throw new JupyterInstallError(localize.DataScience.jupyterNotSupported(), localize.DataScience.pythonInteractiveHelpLink()); - } - } else { - // Status depends upon if we're about to connect to existing server or not. - const status = (await this.jupyterExecution.getServer(await this.interactiveWindowProvider.getNotebookOptions())) ? - this.setStatus(localize.DataScience.connectingToJupyter()) : this.setStatus(localize.DataScience.startingJupyter()); - - try { - // Then load the jupyter server - await this.loadJupyterServer(); - } catch (e) { - throw e; - } finally { - status.dispose(); - } + // Indicate failing. + throw new JupyterInstallError(localize.DataScience.jupyterNotSupported(), localize.DataScience.pythonInteractiveHelpLink()); } + // Then load the jupyter server + await this.loadJupyterServer(); } catch (e) { if (e instanceof JupyterSelfCertsError) { // On a self cert error, warn the user and ask if they want to change the setting @@ -1262,6 +1232,8 @@ export class InteractiveWindow extends WebViewHost im } else { throw e; } + } finally { + status.dispose(); } } diff --git a/src/test/datascience/editor-integration/codewatcher.unit.test.ts b/src/test/datascience/editor-integration/codewatcher.unit.test.ts index c7c89d3b76e2..1636f65ed4d6 100644 --- a/src/test/datascience/editor-integration/codewatcher.unit.test.ts +++ b/src/test/datascience/editor-integration/codewatcher.unit.test.ts @@ -33,10 +33,10 @@ suite('DataScience Code Watcher Unit Tests', () => { let textEditor: TypeMoq.IMock; let fileSystem: TypeMoq.IMock; let configService: TypeMoq.IMock; - let serviceContainer : TypeMoq.IMock; + let serviceContainer: TypeMoq.IMock; let helper: TypeMoq.IMock; - let tokenSource : CancellationTokenSource; - const contexts : Map = new Map(); + let tokenSource: CancellationTokenSource; + const contexts: Map = new Map(); const pythonSettings = new class extends PythonSettings { public fireChangeEvent() { this.changed.fire(); @@ -85,7 +85,7 @@ suite('DataScience Code Watcher Unit Tests', () => { // Setup the service container to return code watchers serviceContainer = TypeMoq.Mock.ofType(); - serviceContainer.setup(c => c.get(TypeMoq.It.isValue(ICodeWatcher))).returns(() => new CodeWatcher(appShell.object, logger.object, interactiveWindowProvider.object, fileSystem.object, configService.object, documentManager.object, helper.object)); + serviceContainer.setup(c => c.get(TypeMoq.It.isValue(ICodeWatcher))).returns(() => new CodeWatcher(appShell.object, logger.object, interactiveWindowProvider.object, fileSystem.object, configService.object, documentManager.object, helper.object, serviceContainer.object)); // Setup our active history instance interactiveWindowProvider.setup(h => h.getOrCreateActive()).returns(() => Promise.resolve(activeInteractiveWindow.object)); @@ -106,7 +106,7 @@ suite('DataScience Code Watcher Unit Tests', () => { return Promise.resolve(); }); - codeWatcher = new CodeWatcher(appShell.object, logger.object, interactiveWindowProvider.object, fileSystem.object, configService.object, documentManager.object, helper.object); + codeWatcher = new CodeWatcher(appShell.object, logger.object, interactiveWindowProvider.object, fileSystem.object, configService.object, documentManager.object, helper.object, serviceContainer.object); }); function createTypeMoq(tag: string): TypeMoq.IMock { @@ -183,7 +183,7 @@ suite('DataScience Code Watcher Unit Tests', () => { const fileName = 'test.py'; const version = 1; const inputText = -`first line + `first line second line #%% @@ -214,7 +214,7 @@ fourth line`; const fileName = 'test.py'; const version = 1; const inputText = -`first line + `first line second line # @@ -252,7 +252,7 @@ fourth line const fileName = 'test.py'; const version = 1; const inputText = -`first line + `first line second line # @@ -297,12 +297,12 @@ fourth line // Set up our expected call to add code activeInteractiveWindow.setup(h => h.addCode(TypeMoq.It.isValue(testString), - TypeMoq.It.isValue(fileName), - TypeMoq.It.isValue(0), - TypeMoq.It.is((ed: TextEditor) => { - return textEditor.object === ed; - }), - TypeMoq.It.isAny())).verifiable(TypeMoq.Times.once()); + TypeMoq.It.isValue(fileName), + TypeMoq.It.isValue(0), + TypeMoq.It.is((ed: TextEditor) => { + return textEditor.object === ed; + }), + TypeMoq.It.isAny())).verifiable(TypeMoq.Times.once()); // Try our RunCell command await codeWatcher.runCell(testRange); @@ -316,7 +316,7 @@ fourth line const fileName = 'test.py'; const version = 1; const inputText = -`#%% + `#%% testing1 #%% testing2`; // Command tests override getText, so just need the ranges here @@ -327,11 +327,11 @@ testing2`; // Command tests override getText, so just need the ranges here // Set up our expected calls to add code // RunFileInteractive should run the entire file in one block, not cell by cell like RunAllCells activeInteractiveWindow.setup(h => h.addCode(TypeMoq.It.isValue(inputText), - TypeMoq.It.isValue('test.py'), - TypeMoq.It.isValue(0), - TypeMoq.It.isAny(), - TypeMoq.It.isAny() - )).verifiable(TypeMoq.Times.once()); + TypeMoq.It.isValue('test.py'), + TypeMoq.It.isValue(0), + TypeMoq.It.isAny(), + TypeMoq.It.isAny() + )).verifiable(TypeMoq.Times.once()); await codeWatcher.runFileInteractive(); @@ -344,7 +344,7 @@ testing2`; // Command tests override getText, so just need the ranges here const fileName = 'test.py'; const version = 1; const inputText = -`#%% + `#%% testing1 #%% testing2`; // Command tests override getText, so just need the ranges here @@ -362,18 +362,18 @@ testing2`; // Command tests override getText, so just need the ranges here // Set up our expected calls to add code activeInteractiveWindow.setup(h => h.addCode(TypeMoq.It.isValue(testString1), - TypeMoq.It.isValue('test.py'), - TypeMoq.It.isValue(0), - TypeMoq.It.isAny(), - TypeMoq.It.isAny() - )).verifiable(TypeMoq.Times.once()); + TypeMoq.It.isValue('test.py'), + TypeMoq.It.isValue(0), + TypeMoq.It.isAny(), + TypeMoq.It.isAny() + )).verifiable(TypeMoq.Times.once()); activeInteractiveWindow.setup(h => h.addCode(TypeMoq.It.isValue(testString2), - TypeMoq.It.isValue('test.py'), - TypeMoq.It.isValue(2), - TypeMoq.It.isAny(), - TypeMoq.It.isAny() - )).verifiable(TypeMoq.Times.once()); + TypeMoq.It.isValue('test.py'), + TypeMoq.It.isValue(2), + TypeMoq.It.isAny(), + TypeMoq.It.isAny() + )).verifiable(TypeMoq.Times.once()); await codeWatcher.runAllCells(); @@ -386,7 +386,7 @@ testing2`; // Command tests override getText, so just need the ranges here const fileName = 'test.py'; const version = 1; const inputText = -`#%% + `#%% testing1 #%% testing2`; @@ -397,13 +397,13 @@ testing2`; // Set up our expected calls to add code activeInteractiveWindow.setup(h => h.addCode(TypeMoq.It.isValue('testing2'), - TypeMoq.It.isValue(fileName), - TypeMoq.It.isValue(2), - TypeMoq.It.is((ed: TextEditor) => { - return textEditor.object === ed; - }), - TypeMoq.It.isAny() - )).verifiable(TypeMoq.Times.once()); + TypeMoq.It.isValue(fileName), + TypeMoq.It.isValue(2), + TypeMoq.It.is((ed: TextEditor) => { + return textEditor.object === ed; + }), + TypeMoq.It.isAny() + )).verifiable(TypeMoq.Times.once()); // For this test we need to set up a document selection point textEditor.setup(te => te.selection).returns(() => new Selection(2, 0, 2, 0)); @@ -419,18 +419,18 @@ testing2`; const fileName = 'test.py'; const version = 1; const inputText = -`#%% + `#%% testing1 #%% testing2 #%% testing3`; const targetText1 = -`#%% + `#%% testing2`; const targetText2 = -`#%% + `#%% testing3`; const document = createDocument(inputText, fileName, version, TypeMoq.Times.atLeastOnce(), true); @@ -439,18 +439,18 @@ testing3`; // Set up our expected calls to add code activeInteractiveWindow.setup(h => h.addCode(TypeMoq.It.isValue(targetText1), - TypeMoq.It.isValue(fileName), - TypeMoq.It.isValue(2), - TypeMoq.It.isAny(), - TypeMoq.It.isAny() - )).verifiable(TypeMoq.Times.once()); + TypeMoq.It.isValue(fileName), + TypeMoq.It.isValue(2), + TypeMoq.It.isAny(), + TypeMoq.It.isAny() + )).verifiable(TypeMoq.Times.once()); activeInteractiveWindow.setup(h => h.addCode(TypeMoq.It.isValue(targetText2), - TypeMoq.It.isValue(fileName), - TypeMoq.It.isValue(4), - TypeMoq.It.isAny(), - TypeMoq.It.isAny() - )).verifiable(TypeMoq.Times.once()); + TypeMoq.It.isValue(fileName), + TypeMoq.It.isValue(4), + TypeMoq.It.isAny(), + TypeMoq.It.isAny() + )).verifiable(TypeMoq.Times.once()); await codeWatcher.runCellAndAllBelow(2, 0); @@ -463,18 +463,18 @@ testing3`; const fileName = 'test.py'; const version = 1; const inputText = -`#%% + `#%% testing1 #%% testing2 #%% testing3`; const targetText1 = -`#%% + `#%% testing1`; const targetText2 = -`#%% + `#%% testing2`; const document = createDocument(inputText, fileName, version, TypeMoq.Times.atLeastOnce(), true); @@ -483,18 +483,18 @@ testing2`; // Set up our expected calls to add code activeInteractiveWindow.setup(h => h.addCode(TypeMoq.It.isValue(targetText1), - TypeMoq.It.isValue(fileName), - TypeMoq.It.isValue(0), - TypeMoq.It.isAny(), - TypeMoq.It.isAny() - )).verifiable(TypeMoq.Times.once()); + TypeMoq.It.isValue(fileName), + TypeMoq.It.isValue(0), + TypeMoq.It.isAny(), + TypeMoq.It.isAny() + )).verifiable(TypeMoq.Times.once()); activeInteractiveWindow.setup(h => h.addCode(TypeMoq.It.isValue(targetText2), - TypeMoq.It.isValue(fileName), - TypeMoq.It.isValue(2), - TypeMoq.It.isAny(), - TypeMoq.It.isAny() - )).verifiable(TypeMoq.Times.once()); + TypeMoq.It.isValue(fileName), + TypeMoq.It.isValue(2), + TypeMoq.It.isAny(), + TypeMoq.It.isAny() + )).verifiable(TypeMoq.Times.once()); await codeWatcher.runAllCellsAbove(4, 0); @@ -507,14 +507,14 @@ testing2`; const fileName = 'test.py'; const version = 1; const inputText = -`#%% + `#%% testing1 #%% testing2 #%% testing3`; const targetText = -`#%% + `#%% testing1`; const document = createDocument(inputText, fileName, version, TypeMoq.Times.atLeastOnce(), true); @@ -523,11 +523,11 @@ testing1`; // Set up our expected calls to add code activeInteractiveWindow.setup(h => h.addCode(TypeMoq.It.isValue(targetText), - TypeMoq.It.isValue(fileName), - TypeMoq.It.isValue(0), - TypeMoq.It.isAny(), - TypeMoq.It.isAny() - )).verifiable(TypeMoq.Times.once()); + TypeMoq.It.isValue(fileName), + TypeMoq.It.isValue(0), + TypeMoq.It.isAny(), + TypeMoq.It.isAny() + )).verifiable(TypeMoq.Times.once()); await codeWatcher.runToLine(2); @@ -540,7 +540,7 @@ testing1`; const fileName = 'test.py'; const version = 1; const inputText = -` + ` print('testing')`; @@ -551,11 +551,11 @@ print('testing')`; // If adding empty lines nothing should be added and history should not be started interactiveWindowProvider.setup(h => h.getOrCreateActive()).returns(() => Promise.resolve(activeInteractiveWindow.object)).verifiable(TypeMoq.Times.never()); activeInteractiveWindow.setup(h => h.addCode(TypeMoq.It.isAny(), - TypeMoq.It.isValue(fileName), - TypeMoq.It.isAnyNumber(), - TypeMoq.It.isAny(), - TypeMoq.It.isAny() - )).verifiable(TypeMoq.Times.never()); + TypeMoq.It.isValue(fileName), + TypeMoq.It.isAnyNumber(), + TypeMoq.It.isAny(), + TypeMoq.It.isAny() + )).verifiable(TypeMoq.Times.never()); await codeWatcher.runToLine(2); @@ -569,14 +569,14 @@ print('testing')`; const fileName = 'test.py'; const version = 1; const inputText = -`#%% + `#%% testing1 #%% testing2 #%% testing3`; const targetText = -`#%% + `#%% testing2 #%% testing3`; @@ -587,11 +587,11 @@ testing3`; // Set up our expected calls to add code activeInteractiveWindow.setup(h => h.addCode(TypeMoq.It.isValue(targetText), - TypeMoq.It.isValue(fileName), - TypeMoq.It.isValue(2), - TypeMoq.It.isAny(), - TypeMoq.It.isAny() - )).verifiable(TypeMoq.Times.once()); + TypeMoq.It.isValue(fileName), + TypeMoq.It.isValue(2), + TypeMoq.It.isAny(), + TypeMoq.It.isAny() + )).verifiable(TypeMoq.Times.once()); // Try our RunCell command with the first selection point await codeWatcher.runFromLine(2); @@ -605,7 +605,7 @@ testing3`; const fileName = 'test.py'; const version = 1; const inputText = -`#%% + `#%% testing1 #%% testing2`; @@ -619,13 +619,13 @@ testing2`; // Set up our expected calls to add code activeInteractiveWindow.setup(h => h.addCode(TypeMoq.It.isValue('testing2'), - TypeMoq.It.isValue(fileName), - TypeMoq.It.isValue(3), - TypeMoq.It.is((ed: TextEditor) => { - return textEditor.object === ed; - }), - TypeMoq.It.isAny() - )).verifiable(TypeMoq.Times.once()); + TypeMoq.It.isValue(fileName), + TypeMoq.It.isValue(3), + TypeMoq.It.is((ed: TextEditor) => { + return textEditor.object === ed; + }), + TypeMoq.It.isAny() + )).verifiable(TypeMoq.Times.once()); // For this test we need to set up a document selection point textEditor.setup(te => te.document).returns(() => document.object); @@ -643,7 +643,7 @@ testing2`; const fileName = 'test.py'; const version = 1; const inputText = -`#%% + `#%% testing1 #%% testing2`; // Command tests override getText, so just need the ranges here @@ -656,13 +656,13 @@ testing2`; // Command tests override getText, so just need the ranges here // Set up our expected calls to add code activeInteractiveWindow.setup(h => h.addCode(TypeMoq.It.isValue(testString), - TypeMoq.It.isValue('test.py'), - TypeMoq.It.isValue(0), - TypeMoq.It.is((ed: TextEditor) => { - return textEditor.object === ed; - }), - TypeMoq.It.isAny() - )).verifiable(TypeMoq.Times.once()); + TypeMoq.It.isValue('test.py'), + TypeMoq.It.isValue(0), + TypeMoq.It.is((ed: TextEditor) => { + return textEditor.object === ed; + }), + TypeMoq.It.isAny() + )).verifiable(TypeMoq.Times.once()); // For this test we need to set up a document selection point const selection = new Selection(0, 0, 0, 0); From 3ad16452707553d1f775de5a808bc980df9d83d3 Mon Sep 17 00:00:00 2001 From: David Kutugata Date: Mon, 8 Jul 2019 13:22:18 -0700 Subject: [PATCH 03/19] changed the error handler to be its own separate object --- package.nls.json | 1 - src/client/common/utils/localize.ts | 1 - .../editor-integration/codewatcher.ts | 60 +-------- .../datascience/errorHandler/errorHandler.ts | 58 +++++++++ .../interactiveWindowCommandListener.ts | 50 ++++---- .../datascience/jupyter/jupyterExecution.ts | 7 +- src/client/datascience/serviceRegistry.ts | 5 +- src/client/datascience/types.ts | 119 +++++++++--------- .../codewatcher.unit.test.ts | 17 ++- ...eractiveWindowCommandListener.unit.test.ts | 5 +- 10 files changed, 173 insertions(+), 150 deletions(-) create mode 100644 src/client/datascience/errorHandler/errorHandler.ts diff --git a/package.nls.json b/package.nls.json index d220976a026d..f04c3d6e7686 100644 --- a/package.nls.json +++ b/package.nls.json @@ -142,7 +142,6 @@ "Linter.enablePylint": "You have a pylintrc file in your workspace. Do you want to enable pylint?", "Linter.replaceWithSelectedLinter": "Multiple linters are enabled in settings. Replace with '{0}'?", "DataScience.jupyterInstall": "Install", - "DataScience.jupyterInstallError": "There was an error installing Jupyter.", "DataScience.jupyterSelectURILaunchLocal": "Launch a local Jupyter server when needed", "DataScience.jupyterSelectURISpecifyURI": "Type in the URI to connect to a running Jupyter server", "DataScience.jupyterSelectURIPrompt": "Enter the URI of a Jupyter server", diff --git a/src/client/common/utils/localize.ts b/src/client/common/utils/localize.ts index e8cb1f164544..893319d217de 100644 --- a/src/client/common/utils/localize.ts +++ b/src/client/common/utils/localize.ts @@ -101,7 +101,6 @@ export namespace DataScience { export const notebookCheckForImportNo = localize('DataScience.notebookCheckForImportNo', 'Later'); export const notebookCheckForImportDontAskAgain = localize('DataScience.notebookCheckForImportDontAskAgain', 'Don\'t Ask Again'); export const jupyterInstall = localize('DataScience.jupyterAskToInstall', 'Install'); - export const jupyterInstallError = localize('DataScience.jupyterInstallError', 'There was an error installing Jupyter.'); export const jupyterNotSupported = localize('DataScience.jupyterNotSupported', 'Jupyter is not installed'); export const jupyterNotSupportedBecauseOfEnvironment = localize('DataScience.jupyterNotSupportedBecauseOfEnvironment', 'Activating {0} to run Jupyter failed with {1}'); export const jupyterNbConvertNotSupported = localize('DataScience.jupyterNbConvertNotSupported', 'Jupyter nbconvert is not installed'); diff --git a/src/client/datascience/editor-integration/codewatcher.ts b/src/client/datascience/editor-integration/codewatcher.ts index d2cd2d2b9fe6..6b9ca31dd3dc 100644 --- a/src/client/datascience/editor-integration/codewatcher.ts +++ b/src/client/datascience/editor-integration/codewatcher.ts @@ -4,21 +4,16 @@ import { inject, injectable } from 'inversify'; import { CodeLens, Command, Position, Range, Selection, TextDocument, TextEditor, TextEditorRevealType } from 'vscode'; -import { IApplicationShell, IDocumentManager } from '../../common/application/types'; -import { IInstallationChannelManager } from '../../common/installer/types'; +import { IDocumentManager } from '../../common/application/types'; import { IFileSystem } from '../../common/platform/types'; -import { IConfigurationService, IDataScienceSettings, ILogger, Product } from '../../common/types'; +import { IConfigurationService, IDataScienceSettings } from '../../common/types'; import * as localize from '../../common/utils/localize'; -import { noop } from '../../common/utils/misc'; import { StopWatch } from '../../common/utils/stopWatch'; -import { IServiceContainer } from '../../ioc/types'; import { captureTelemetry } from '../../telemetry'; import { ICodeExecutionHelper } from '../../terminals/types'; import { generateCellRanges } from '../cellFactory'; import { Commands, Telemetry } from '../constants'; -import { JupyterInstallError } from '../jupyter/jupyterInstallError'; -import { JupyterSelfCertsError } from '../jupyter/jupyterSelfCertsError'; -import { ICodeWatcher, IInteractiveWindowProvider } from '../types'; +import { ICodeWatcher, IDataScienceErrorHandler, IInteractiveWindowProvider } from '../types'; @injectable() export class CodeWatcher implements ICodeWatcher { @@ -28,14 +23,12 @@ export class CodeWatcher implements ICodeWatcher { private codeLenses: CodeLens[] = []; private cachedSettings: IDataScienceSettings | undefined; - constructor(@inject(IApplicationShell) private applicationShell: IApplicationShell, - @inject(ILogger) private logger: ILogger, - @inject(IInteractiveWindowProvider) private interactiveWindowProvider: IInteractiveWindowProvider, + constructor(@inject(IInteractiveWindowProvider) private interactiveWindowProvider: IInteractiveWindowProvider, @inject(IFileSystem) private fileSystem: IFileSystem, @inject(IConfigurationService) private configService: IConfigurationService, @inject(IDocumentManager) private documentManager: IDocumentManager, @inject(ICodeExecutionHelper) private executionHelper: ICodeExecutionHelper, - @inject(IServiceContainer) protected serviceContainer: IServiceContainer + @inject(IDataScienceErrorHandler) protected dataScienceErrorHandler: IDataScienceErrorHandler ) { } @@ -268,7 +261,7 @@ export class CodeWatcher implements ICodeWatcher { await activeInteractiveWindow.addCode(code, file, line, editor, stopWatch); } } catch (err) { - this.handleError(err); + this.dataScienceErrorHandler.handleError(err); } } @@ -320,47 +313,6 @@ export class CodeWatcher implements ICodeWatcher { } } - // tslint:disable-next-line:no-any - private handleError = (err: any) => { - if (err instanceof JupyterInstallError) { - this.applicationShell.showInformationMessage( - localize.DataScience.jupyterNotSupported(), - localize.DataScience.jupyterInstall(), - localize.DataScience.notebookCheckForImportNo()) - .then(response => { - if (response === localize.DataScience.jupyterInstall()) { - const channels = this.serviceContainer.get(IInstallationChannelManager); - return channels.getInstallationChannel(Product.jupyter); - } else { - const jupyterError = err as JupyterInstallError; - - // This is a special error that shows a link to open for more help - this.applicationShell.showErrorMessage(jupyterError.message, jupyterError.actionTitle).then(v => { - // User clicked on the link, open it. - if (v === jupyterError.actionTitle) { - this.applicationShell.openUrl(jupyterError.action); - } - }); - } - }).then(inst => { - if (inst) { - inst.installModule('jupyter') - .catch(() => this.applicationShell.showErrorMessage( - localize.DataScience.jupyterInstallError(), - localize.DataScience.pythonInteractiveHelpLink())); - } - }); - } else if (err instanceof JupyterSelfCertsError) { - // Don't show the message for self cert errors - noop(); - } else if (err.message) { - this.applicationShell.showErrorMessage(err.message); - } else { - this.applicationShell.showErrorMessage(err.toString()); - } - this.logger.logError(err); - } - // User has picked run and advance on the last cell of a document // Create a new cell at the bottom and put their selection there, ready to type private createNewCell(currentRange: Range): Range { diff --git a/src/client/datascience/errorHandler/errorHandler.ts b/src/client/datascience/errorHandler/errorHandler.ts new file mode 100644 index 000000000000..e6d65c29c513 --- /dev/null +++ b/src/client/datascience/errorHandler/errorHandler.ts @@ -0,0 +1,58 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +import { inject, injectable } from 'inversify'; +import { IApplicationShell } from '../../common/application/types'; +import { IInstallationChannelManager } from '../../common/installer/types'; +import { ILogger, Product } from '../../common/types'; +import * as localize from '../../common/utils/localize'; +import { noop } from '../../common/utils/misc'; +import { IServiceContainer } from '../../ioc/types'; +import { JupyterInstallError } from '../jupyter/jupyterInstallError'; +import { JupyterSelfCertsError } from '../jupyter/jupyterSelfCertsError'; +import { IDataScienceErrorHandler } from '../types'; + +@injectable() +export class DataScienceErrorHandler implements IDataScienceErrorHandler { + constructor(@inject(IApplicationShell) private applicationShell: IApplicationShell, + @inject(ILogger) private logger: ILogger, + @inject(IServiceContainer) protected serviceContainer: IServiceContainer) { + } + + public handleError(err: Error) { + if (err instanceof JupyterInstallError) { + this.applicationShell.showInformationMessage( + localize.DataScience.jupyterNotSupported(), + localize.DataScience.jupyterInstall(), + localize.DataScience.notebookCheckForImportNo()) + .then(response => { + if (response === localize.DataScience.jupyterInstall()) { + const channels = this.serviceContainer.get(IInstallationChannelManager); + return channels.getInstallationChannel(Product.jupyter); + } else { + const jupyterError = err as JupyterInstallError; + + // This is a special error that shows a link to open for more help + this.applicationShell.showErrorMessage(jupyterError.message, jupyterError.actionTitle).then(v => { + // User clicked on the link, open it. + if (v === jupyterError.actionTitle) { + this.applicationShell.openUrl(jupyterError.action); + } + }); + } + }).then(installer => { + if (installer) { + installer.installModule('jupyter') + .catch(e => this.applicationShell.showErrorMessage(e.message, localize.DataScience.pythonInteractiveHelpLink())); + } + }); + } else if (err instanceof JupyterSelfCertsError) { + // Don't show the message for self cert errors + noop(); + } else if (err.message) { + this.applicationShell.showErrorMessage(err.message); + } else { + this.applicationShell.showErrorMessage(err.toString()); + } + this.logger.logError(err); + } +} diff --git a/src/client/datascience/interactive-window/interactiveWindowCommandListener.ts b/src/client/datascience/interactive-window/interactiveWindowCommandListener.ts index 723b19930ab7..b808cff6640a 100644 --- a/src/client/datascience/interactive-window/interactiveWindowCommandListener.ts +++ b/src/client/datascience/interactive-window/interactiveWindowCommandListener.ts @@ -19,8 +19,10 @@ import { captureTelemetry } from '../../telemetry'; import { CommandSource } from '../../testing/common/constants'; import { generateCellRanges, generateCellsFromDocument } from '../cellFactory'; import { Commands, Telemetry } from '../constants'; +import { JupyterInstallError } from '../jupyter/jupyterInstallError'; import { IDataScienceCommandListener, + IDataScienceErrorHandler, IInteractiveWindowProvider, IJupyterExecution, INotebookExporter, @@ -41,9 +43,10 @@ export class InteractiveWindowCommandListener implements IDataScienceCommandList @inject(IFileSystem) private fileSystem: IFileSystem, @inject(ILogger) private logger: ILogger, @inject(IConfigurationService) private configuration: IConfigurationService, - @inject(IStatusProvider) private statusProvider : IStatusProvider, - @inject(INotebookImporter) private jupyterImporter : INotebookImporter - ) { + @inject(IStatusProvider) private statusProvider: IStatusProvider, + @inject(INotebookImporter) private jupyterImporter: INotebookImporter, + @inject(IDataScienceErrorHandler) protected dataScienceErrorHandler: IDataScienceErrorHandler + ) { // Listen to document open commands. We want to ask the user if they want to import. const disposable = this.documentManager.onDidOpenTextDocument(this.onOpenedDocument); this.disposableRegistry.push(disposable); @@ -102,7 +105,7 @@ export class InteractiveWindowCommandListener implements IDataScienceCommandList } // tslint:disable:no-any - private async listenForErrors(promise: () => Promise) : Promise { + private async listenForErrors(promise: () => Promise): Promise { let result: any; try { result = await promise(); @@ -123,7 +126,7 @@ export class InteractiveWindowCommandListener implements IDataScienceCommandList return result; } - private showInformationMessage(message: string, question?: string) : Thenable { + private showInformationMessage(message: string, question?: string): Thenable { if (question) { return this.applicationShell.showInformationMessage(message, question); } else { @@ -222,25 +225,26 @@ export class InteractiveWindowCommandListener implements IDataScienceCommandList } } } else { - this.applicationShell.showErrorMessage(localize.DataScience.jupyterNotSupported()); + this.dataScienceErrorHandler.handleError( + new JupyterInstallError(localize.DataScience.jupyterNotSupported(), localize.DataScience.pythonInteractiveHelpLink())); } } - private async exportCellsWithOutput(ranges: {range: Range; title: string}[], document: TextDocument, file: string, cancelToken: CancellationToken) : Promise { + private async exportCellsWithOutput(ranges: { range: Range; title: string }[], document: TextDocument, file: string, cancelToken: CancellationToken): Promise { let server: INotebookServer | undefined; try { const settings = this.configuration.getSettings(); - const useDefaultConfig : boolean | undefined = settings.datascience.useDefaultConfigForJupyter; + const useDefaultConfig: boolean | undefined = settings.datascience.useDefaultConfigForJupyter; // Try starting a server. Purpose should be unique so we // create a brand new one. - server = await this.jupyterExecution.connectToNotebookServer({ useDefaultConfig, purpose: uuid()}, cancelToken); + server = await this.jupyterExecution.connectToNotebookServer({ useDefaultConfig, purpose: uuid() }, cancelToken); // If that works, then execute all of the cells. const cells = Array.prototype.concat(... await Promise.all(ranges.map(r => { - const code = document.getText(r.range); - return server ? server.execute(code, document.fileName, r.range.start.line, uuid(), cancelToken) : []; - }))); + const code = document.getText(r.range); + return server ? server.execute(code, document.fileName, r.range.start.line, uuid(), cancelToken) : []; + }))); // Then save them to the file let directoryChange; @@ -258,7 +262,7 @@ export class InteractiveWindowCommandListener implements IDataScienceCommandList } } - private async showExportDialog() : Promise { + private async showExportDialog(): Promise { const filtersKey = localize.DataScience.exportDialogFilter(); const filtersObject: { [name: string]: string[] } = {}; filtersObject[filtersKey] = ['ipynb']; @@ -347,7 +351,7 @@ export class InteractiveWindowCommandListener implements IDataScienceCommandList private onOpenedDocument = async (document: TextDocument) => { // Preview and import the document if necessary. - const results = await Promise.all([this.previewNotebook(document.fileName), this.askForImportDocument(document)]); + const results = await Promise.all([this.previewNotebook(document.fileName), this.askForImportDocument(document)]); // When done, make sure the current document is still the active editor if we did // not do an import. Otherwise subsequent opens will cover up the interactive pane. @@ -356,7 +360,7 @@ export class InteractiveWindowCommandListener implements IDataScienceCommandList } } - private async previewNotebook(fileName: string) : Promise { + private async previewNotebook(fileName: string): Promise { if (fileName && fileName.endsWith('.ipynb') && this.autoPreviewNotebooks()) { // Get history before putting up status so that we show a busy message when we // start the preview. @@ -373,7 +377,7 @@ export class InteractiveWindowCommandListener implements IDataScienceCommandList return false; } - private async askForImportDocument(document: TextDocument) : Promise { + private async askForImportDocument(document: TextDocument): Promise { if (document.fileName.endsWith('.ipynb') && this.canImportFromOpenedFile()) { const yes = localize.DataScience.notebookCheckForImportYes(); const no = localize.DataScience.notebookCheckForImportNo(); @@ -399,18 +403,18 @@ export class InteractiveWindowCommandListener implements IDataScienceCommandList } @captureTelemetry(Telemetry.ShowHistoryPane, undefined, false) - private async showInteractiveWindow() : Promise{ + private async showInteractiveWindow(): Promise { const active = await this.interactiveWindowProvider.getOrCreateActive(); return active.show(); } - private waitForStatus(promise: () => Promise, format: string, file?: string, canceled?: () => void, skipHistory?: boolean) : Promise { + private waitForStatus(promise: () => Promise, format: string, file?: string, canceled?: () => void, skipHistory?: boolean): Promise { const message = file ? format.format(file) : format; return this.statusProvider.waitWithStatus(promise, message, undefined, canceled, skipHistory); } @captureTelemetry(Telemetry.ImportNotebook, { scope: 'command' }, false) - private async importNotebook() : Promise { + private async importNotebook(): Promise { const filtersKey = localize.DataScience.importDialogFilter(); const filtersObject: { [name: string]: string[] } = {}; filtersObject[filtersKey] = ['ipynb']; @@ -434,7 +438,7 @@ export class InteractiveWindowCommandListener implements IDataScienceCommandList } @captureTelemetry(Telemetry.ImportNotebook, { scope: 'file' }, false) - private async importNotebookOnFile(file: string, preview: boolean) : Promise { + private async importNotebookOnFile(file: string, preview: boolean): Promise { if (file && file.length > 0) { // Preview a file whenever we import if not already previewed if (preview) { @@ -442,14 +446,14 @@ export class InteractiveWindowCommandListener implements IDataScienceCommandList } await this.waitForStatus(async () => { - const contents = await this.jupyterImporter.importFromFile(file); + const contents = await this.jupyterImporter.importFromFile(file); await this.viewDocument(contents); }, localize.DataScience.importingFormat(), file); } } - private viewDocument = async (contents: string) : Promise => { - const doc = await this.documentManager.openTextDocument({language: 'python', content: contents}); + private viewDocument = async (contents: string): Promise => { + const doc = await this.documentManager.openTextDocument({ language: 'python', content: contents }); const editor = await this.documentManager.showTextDocument(doc, ViewColumn.One); // Edit the document so that it is dirty (add a space at the end) diff --git a/src/client/datascience/jupyter/jupyterExecution.ts b/src/client/datascience/jupyter/jupyterExecution.ts index c5eb82e1a1ac..316b8bc614bd 100644 --- a/src/client/datascience/jupyter/jupyterExecution.ts +++ b/src/client/datascience/jupyter/jupyterExecution.ts @@ -36,6 +36,7 @@ import { INotebookServerOptions } from '../types'; import { JupyterConnection, JupyterServerInfo } from './jupyterConnection'; +import { JupyterInstallError } from './jupyterInstallError'; import { JupyterKernelSpec } from './jupyterKernelSpec'; import { JupyterSelfCertsError } from './jupyterSelfCertsError'; import { JupyterWaitForIdleError } from './jupyterWaitForIdleError'; @@ -204,7 +205,7 @@ export class JupyterExecutionBase implements IJupyterExecution { // First we find a way to start a notebook server const notebookCommand = await this.findBestCommandTimed(JupyterCommands.NotebookCommand); if (!notebookCommand) { - throw new Error(localize.DataScience.jupyterNotSupported()); + throw new JupyterInstallError(localize.DataScience.jupyterNotSupported(), localize.DataScience.pythonInteractiveHelpLink()); } const args: string[] = [`--NotebookApp.file_to_run=${file}`]; @@ -337,7 +338,7 @@ export class JupyterExecutionBase implements IJupyterExecution { // First we find a way to start a notebook server const notebookCommand = await this.findBestCommandTimed(JupyterCommands.NotebookCommand, cancelToken); if (!notebookCommand) { - throw new Error(localize.DataScience.jupyterNotSupported()); + throw new JupyterInstallError(localize.DataScience.jupyterNotSupported(), localize.DataScience.pythonInteractiveHelpLink()); } // Now actually launch it @@ -775,7 +776,7 @@ export class JupyterExecutionBase implements IJupyterExecution { return true; } - private async findBestCommandTimed(command: string, cancelToken?: CancellationToken) : Promise { + private async findBestCommandTimed(command: string, cancelToken?: CancellationToken): Promise { // Only log telemetry if not already found (meaning the first time) let timer: StopWatch | undefined; if (!this.commands.hasOwnProperty(command)) { diff --git a/src/client/datascience/serviceRegistry.ts b/src/client/datascience/serviceRegistry.ts index 66c1ac4aaf1a..f28f6a030ecf 100644 --- a/src/client/datascience/serviceRegistry.ts +++ b/src/client/datascience/serviceRegistry.ts @@ -14,6 +14,7 @@ import { DataScience } from './datascience'; import { DataScienceCodeLensProvider } from './editor-integration/codelensprovider'; import { CodeWatcher } from './editor-integration/codewatcher'; import { Decorator } from './editor-integration/decorator'; +import { DataScienceErrorHandler } from './errorHandler/errorHandler'; import { DotNetIntellisenseProvider } from './interactive-window/intellisense/dotNetIntellisenseProvider'; import { JediIntellisenseProvider } from './interactive-window/intellisense/jediIntellisenseProvider'; import { InteractiveWindow } from './interactive-window/interactiveWindow'; @@ -40,6 +41,7 @@ import { IDataScience, IDataScienceCodeLensProvider, IDataScienceCommandListener, + IDataScienceErrorHandler, IDataViewer, IDataViewerProvider, IInteractiveWindow, @@ -61,7 +63,7 @@ import { } from './types'; // tslint:disable:no-any -function wrapType(ctor: ClassType) : ClassType { +function wrapType(ctor: ClassType): ClassType { return class extends ctor { constructor(...args: any[]) { const stopWatch = new StopWatch(); @@ -105,4 +107,5 @@ export function registerTypes(serviceManager: IServiceManager) { serviceManager.addSingleton(IPlotViewerProvider, wrapType(PlotViewerProvider)); serviceManager.add(IPlotViewer, wrapType(PlotViewer)); serviceManager.addSingleton(IJupyterDebugger, wrapType(JupyterDebugger)); + serviceManager.add(IDataScienceErrorHandler, wrapType(DataScienceErrorHandler)); } diff --git a/src/client/datascience/types.ts b/src/client/datascience/types.ts index 75fc5177e079..58f381fec23a 100644 --- a/src/client/datascience/types.ts +++ b/src/client/datascience/types.ts @@ -70,19 +70,19 @@ export interface INotebookCompletion { // Talks to a jupyter ipython kernel to retrieve data for cells export const INotebookServer = Symbol('INotebookServer'); export interface INotebookServer extends IAsyncDisposable { - connect(launchInfo: INotebookServerLaunchInfo, cancelToken?: CancellationToken) : Promise; - executeObservable(code: string, file: string, line: number, id: string, silent: boolean) : Observable; - execute(code: string, file: string, line: number, id: string, cancelToken?: CancellationToken, silent?: boolean) : Promise; - getCompletion(cellCode: string, offsetInCode: number, cancelToken?: CancellationToken) : Promise; - restartKernel(timeoutInMs: number) : Promise; - waitForIdle(timeoutInMs: number) : Promise; - shutdown() : Promise; - interruptKernel(timeoutInMs: number) : Promise; + connect(launchInfo: INotebookServerLaunchInfo, cancelToken?: CancellationToken): Promise; + executeObservable(code: string, file: string, line: number, id: string, silent: boolean): Observable; + execute(code: string, file: string, line: number, id: string, cancelToken?: CancellationToken, silent?: boolean): Promise; + getCompletion(cellCode: string, offsetInCode: number, cancelToken?: CancellationToken): Promise; + restartKernel(timeoutInMs: number): Promise; + waitForIdle(timeoutInMs: number): Promise; + shutdown(): Promise; + interruptKernel(timeoutInMs: number): Promise; setInitialDirectory(directory: string): Promise; waitForConnect(): Promise; getConnectionInfo(): IConnection | undefined; - getSysInfo() : Promise; - setMatplotLibStyle(useDark: boolean) : Promise; + getSysInfo(): Promise; + setMatplotLibStyle(useDark: boolean): Promise; } export interface INotebookServerOptions { @@ -96,17 +96,17 @@ export interface INotebookServerOptions { export const IJupyterExecution = Symbol('IJupyterExecution'); export interface IJupyterExecution extends IAsyncDisposable { - sessionChanged: Event ; - isNotebookSupported(cancelToken?: CancellationToken) : Promise; - isImportSupported(cancelToken?: CancellationToken) : Promise; + sessionChanged: Event; + isNotebookSupported(cancelToken?: CancellationToken): Promise; + isImportSupported(cancelToken?: CancellationToken): Promise; isKernelCreateSupported(cancelToken?: CancellationToken): Promise; isKernelSpecSupported(cancelToken?: CancellationToken): Promise; isSpawnSupported(cancelToken?: CancellationToken): Promise; - connectToNotebookServer(options?: INotebookServerOptions, cancelToken?: CancellationToken) : Promise; - spawnNotebook(file: string) : Promise; - importNotebook(file: string, template: string | undefined) : Promise; - getUsableJupyterPython(cancelToken?: CancellationToken) : Promise; - getServer(options?: INotebookServerOptions) : Promise; + connectToNotebookServer(options?: INotebookServerOptions, cancelToken?: CancellationToken): Promise; + spawnNotebook(file: string): Promise; + importNotebook(file: string, template: string | undefined): Promise; + getUsableJupyterPython(cancelToken?: CancellationToken): Promise; + getServer(options?: INotebookServerOptions): Promise; } export const IJupyterDebugger = Symbol('IJupyterDebugger'); @@ -130,16 +130,16 @@ export interface IJupyterPasswordConnect { export const IJupyterSession = Symbol('IJupyterSession'); export interface IJupyterSession extends IAsyncDisposable { onRestarted: Event; - restart(timeout: number) : Promise; - interrupt(timeout: number) : Promise; - waitForIdle(timeout: number) : Promise; - requestExecute(content: KernelMessage.IExecuteRequest, disposeOnDone?: boolean, metadata?: JSONObject) : Kernel.IFuture | undefined; + restart(timeout: number): Promise; + interrupt(timeout: number): Promise; + waitForIdle(timeout: number): Promise; + requestExecute(content: KernelMessage.IExecuteRequest, disposeOnDone?: boolean, metadata?: JSONObject): Kernel.IFuture | undefined; requestComplete(content: KernelMessage.ICompleteRequest): Promise; } export const IJupyterSessionManager = Symbol('IJupyterSessionManager'); export interface IJupyterSessionManager { - startNew(connInfo: IConnection, kernelSpec: IJupyterKernelSpec | undefined, cancelToken?: CancellationToken) : Promise; - getActiveKernelSpecs(connInfo: IConnection) : Promise; + startNew(connInfo: IConnection, kernelSpec: IJupyterKernelSpec | undefined, cancelToken?: CancellationToken): Promise; + getActiveKernelSpecs(connInfo: IConnection): Promise; } export interface IJupyterKernelSpec extends IAsyncDisposable { @@ -150,20 +150,25 @@ export interface IJupyterKernelSpec extends IAsyncDisposable { export const INotebookImporter = Symbol('INotebookImporter'); export interface INotebookImporter extends Disposable { - importFromFile(file: string) : Promise; + importFromFile(file: string): Promise; } export const INotebookExporter = Symbol('INotebookExporter'); export interface INotebookExporter extends Disposable { - translateToNotebook(cells: ICell[], directoryChange?: string) : Promise; + translateToNotebook(cells: ICell[], directoryChange?: string): Promise; } export const IInteractiveWindowProvider = Symbol('IInteractiveWindowProvider'); export interface IInteractiveWindowProvider { onExecutedCode: Event; - getActive() : IInteractiveWindow | undefined; + getActive(): IInteractiveWindow | undefined; getOrCreateActive(): Promise; - getNotebookOptions() : Promise; + getNotebookOptions(): Promise; +} + +export const IDataScienceErrorHandler = Symbol('IDataScienceErrorHandler'); +export interface IDataScienceErrorHandler { + handleError(err: Error): void; } export const IInteractiveWindow = Symbol('IInteractiveWindow'); @@ -171,9 +176,9 @@ export interface IInteractiveWindow extends Disposable { closed: Event; ready: Promise; onExecutedCode: Event; - show() : Promise; - addCode(code: string, file: string, line: number, editor?: TextEditor, runningStopWatch?: StopWatch) : Promise; - debugCode(code: string, file: string, line: number, editor?: TextEditor, runningStopWatch?: StopWatch) : Promise; + show(): Promise; + addCode(code: string, file: string, line: number, editor?: TextEditor, runningStopWatch?: StopWatch): Promise; + debugCode(code: string, file: string, line: number, editor?: TextEditor, runningStopWatch?: StopWatch): Promise; startProgress(): void; stopProgress(): void; undoCells(): void; @@ -184,7 +189,7 @@ export interface IInteractiveWindow extends Disposable { expandAllCells(): void; collapseAllCells(): void; exportCells(): void; - previewNotebook(notebookFile: string) : Promise; + previewNotebook(notebookFile: string): Promise; } export const IInteractiveWindowListener = Symbol('IInteractiveWindowListener'); @@ -197,7 +202,7 @@ export interface IInteractiveWindowListener extends IDisposable { * Fires this event when posting a response message */ // tslint:disable-next-line: no-any - postMessage: Event<{message: string; payload: any}>; + postMessage: Event<{ message: string; payload: any }>; /** * Handles messages that the interactive window receives * @param message message type @@ -213,23 +218,23 @@ export interface IPostOffice { // tslint:disable-next-line:no-any post(message: string, params: any[] | undefined): void; // tslint:disable-next-line:no-any - listen(message: string, listener: (args: any[] | undefined) => void) : void; + listen(message: string, listener: (args: any[] | undefined) => void): void; } // Wraps the vscode CodeLensProvider base class export const IDataScienceCodeLensProvider = Symbol('IDataScienceCodeLensProvider'); export interface IDataScienceCodeLensProvider extends CodeLensProvider { - getCodeWatcher(document: TextDocument) : ICodeWatcher | undefined; + getCodeWatcher(document: TextDocument): ICodeWatcher | undefined; } // Wraps the Code Watcher API export const ICodeWatcher = Symbol('ICodeWatcher'); export interface ICodeWatcher { setDocument(document: TextDocument): void; - getFileName() : string; - getVersion() : number; - getCodeLenses() : CodeLens[]; - getCachedSettings() : IDataScienceSettings | undefined; + getFileName(): string; + getVersion(): number; + getCodeLenses(): CodeLens[]; + getCachedSettings(): IDataScienceSettings | undefined; runAllCells(): Promise; runCell(range: Range): Promise; runCurrentCell(): Promise; @@ -275,37 +280,37 @@ export interface IMessageCell extends nbformat.IBaseCell { export const ICodeCssGenerator = Symbol('ICodeCssGenerator'); export interface ICodeCssGenerator { - generateThemeCss(isDark: boolean, theme: string) : Promise; - generateMonacoTheme(isDark: boolean, theme: string) : Promise; + generateThemeCss(isDark: boolean, theme: string): Promise; + generateMonacoTheme(isDark: boolean, theme: string): Promise; } export const IThemeFinder = Symbol('IThemeFinder'); export interface IThemeFinder { - findThemeRootJson(themeName: string) : Promise; - findTmLanguage(language: string) : Promise; - isThemeDark(themeName: string) : Promise; + findThemeRootJson(themeName: string): Promise; + findTmLanguage(language: string): Promise; + isThemeDark(themeName: string): Promise; } export const IStatusProvider = Symbol('IStatusProvider'); export interface IStatusProvider { // call this function to set the new status on the active // interactive window. Dispose of the returned object when done. - set(message: string, timeout?: number) : Disposable; + set(message: string, timeout?: number): Disposable; // call this function to wait for a promise while displaying status - waitWithStatus(promise: () => Promise, message: string, timeout?: number, canceled?: () => void, skipHistory?: boolean) : Promise; + waitWithStatus(promise: () => Promise, message: string, timeout?: number, canceled?: () => void, skipHistory?: boolean): Promise; } export interface IJupyterCommand { - interpreter() : Promise; + interpreter(): Promise; execObservable(args: string[], options: SpawnOptions): Promise>; exec(args: string[], options: SpawnOptions): Promise>; } export const IJupyterCommandFactory = Symbol('IJupyterCommandFactory'); export interface IJupyterCommandFactory { - createInterpreterCommand(args: string[], interpreter: PythonInterpreter) : IJupyterCommand; - createProcessCommand(exe: string, args: string[]) : IJupyterCommand; + createInterpreterCommand(args: string[], interpreter: PythonInterpreter): IJupyterCommand; + createProcessCommand(exe: string, args: string[]): IJupyterCommand; } // Config settings we pass to our react code @@ -354,8 +359,8 @@ export const IJupyterVariables = Symbol('IJupyterVariables'); export interface IJupyterVariables { getVariables(): Promise; getValue(targetVariable: IJupyterVariable): Promise; - getDataFrameInfo(targetVariable: IJupyterVariable) : Promise; - getDataFrameRows(targetVariable: IJupyterVariable, start: number, end: number) : Promise; + getDataFrameInfo(targetVariable: IJupyterVariable): Promise; + getDataFrameRows(targetVariable: IJupyterVariable, start: number, end: number): Promise; } // Wrapper to hold an execution count for our variable requests @@ -366,25 +371,25 @@ export interface IJupyterVariablesResponse { export const IDataViewerProvider = Symbol('IDataViewerProvider'); export interface IDataViewerProvider { - create(variable: string) : Promise; - getPandasVersion() : Promise<{major: number; minor: number; build: number} | undefined>; + create(variable: string): Promise; + getPandasVersion(): Promise<{ major: number; minor: number; build: number } | undefined>; } export const IDataViewer = Symbol('IDataViewer'); export interface IDataViewer extends IDisposable { - showVariable(variable: IJupyterVariable) : Promise; + showVariable(variable: IJupyterVariable): Promise; } export const IPlotViewerProvider = Symbol('IPlotViewerProvider'); export interface IPlotViewerProvider { - showPlot(imageHtml: string) : Promise; + showPlot(imageHtml: string): Promise; } export const IPlotViewer = Symbol('IPlotViewer'); export interface IPlotViewer extends IDisposable { closed: Event; removed: Event; - addPlot(imageHtml: string) : Promise; + addPlot(imageHtml: string): Promise; show(): Promise; } @@ -401,5 +406,5 @@ export interface IFileHashes { } export interface ICellHashProvider { - getHashes() : IFileHashes[]; + getHashes(): IFileHashes[]; } diff --git a/src/test/datascience/editor-integration/codewatcher.unit.test.ts b/src/test/datascience/editor-integration/codewatcher.unit.test.ts index 1636f65ed4d6..010a19ac9cbc 100644 --- a/src/test/datascience/editor-integration/codewatcher.unit.test.ts +++ b/src/test/datascience/editor-integration/codewatcher.unit.test.ts @@ -7,14 +7,14 @@ import { expect } from 'chai'; import * as TypeMoq from 'typemoq'; import { CancellationTokenSource, CodeLens, Disposable, Range, Selection, TextEditor } from 'vscode'; -import { IApplicationShell, ICommandManager, IDocumentManager } from '../../../client/common/application/types'; +import { ICommandManager, IDocumentManager } from '../../../client/common/application/types'; import { PythonSettings } from '../../../client/common/configSettings'; import { IFileSystem } from '../../../client/common/platform/types'; -import { IConfigurationService, ILogger } from '../../../client/common/types'; +import { IConfigurationService } from '../../../client/common/types'; import { Commands, EditorContexts } from '../../../client/datascience/constants'; import { DataScienceCodeLensProvider } from '../../../client/datascience/editor-integration/codelensprovider'; import { CodeWatcher } from '../../../client/datascience/editor-integration/codewatcher'; -import { ICodeWatcher, IInteractiveWindow, IInteractiveWindowProvider } from '../../../client/datascience/types'; +import { IDataScienceErrorHandler, IInteractiveWindow, IInteractiveWindowProvider } from '../../../client/datascience/types'; import { IServiceContainer } from '../../../client/ioc/types'; import { ICodeExecutionHelper } from '../../../client/terminals/types'; import { MockAutoSelectionService } from '../../mocks/autoSelector'; @@ -24,8 +24,6 @@ import { createDocument } from './helpers'; suite('DataScience Code Watcher Unit Tests', () => { let codeWatcher: CodeWatcher; - let appShell: TypeMoq.IMock; - let logger: TypeMoq.IMock; let interactiveWindowProvider: TypeMoq.IMock; let activeInteractiveWindow: TypeMoq.IMock; let documentManager: TypeMoq.IMock; @@ -33,6 +31,7 @@ suite('DataScience Code Watcher Unit Tests', () => { let textEditor: TypeMoq.IMock; let fileSystem: TypeMoq.IMock; let configService: TypeMoq.IMock; + let dataScienceErrorHandler: TypeMoq.IMock; let serviceContainer: TypeMoq.IMock; let helper: TypeMoq.IMock; let tokenSource: CancellationTokenSource; @@ -46,8 +45,6 @@ suite('DataScience Code Watcher Unit Tests', () => { setup(() => { tokenSource = new CancellationTokenSource(); - appShell = TypeMoq.Mock.ofType(); - logger = TypeMoq.Mock.ofType(); interactiveWindowProvider = TypeMoq.Mock.ofType(); activeInteractiveWindow = createTypeMoq('history'); documentManager = TypeMoq.Mock.ofType(); @@ -85,7 +82,9 @@ suite('DataScience Code Watcher Unit Tests', () => { // Setup the service container to return code watchers serviceContainer = TypeMoq.Mock.ofType(); - serviceContainer.setup(c => c.get(TypeMoq.It.isValue(ICodeWatcher))).returns(() => new CodeWatcher(appShell.object, logger.object, interactiveWindowProvider.object, fileSystem.object, configService.object, documentManager.object, helper.object, serviceContainer.object)); + + // Setup our error handler + dataScienceErrorHandler = TypeMoq.Mock.ofType(); // Setup our active history instance interactiveWindowProvider.setup(h => h.getOrCreateActive()).returns(() => Promise.resolve(activeInteractiveWindow.object)); @@ -106,7 +105,7 @@ suite('DataScience Code Watcher Unit Tests', () => { return Promise.resolve(); }); - codeWatcher = new CodeWatcher(appShell.object, logger.object, interactiveWindowProvider.object, fileSystem.object, configService.object, documentManager.object, helper.object, serviceContainer.object); + codeWatcher = new CodeWatcher(interactiveWindowProvider.object, fileSystem.object, configService.object, documentManager.object, helper.object, dataScienceErrorHandler.object); }); function createTypeMoq(tag: string): TypeMoq.IMock { diff --git a/src/test/datascience/interactiveWindowCommandListener.unit.test.ts b/src/test/datascience/interactiveWindowCommandListener.unit.test.ts index 0f9eef3ab9b4..0bf51769050e 100644 --- a/src/test/datascience/interactiveWindowCommandListener.unit.test.ts +++ b/src/test/datascience/interactiveWindowCommandListener.unit.test.ts @@ -18,6 +18,7 @@ import { IFileSystem } from '../../client/common/platform/types'; import { IConfigurationService, IDisposable, ILogger } from '../../client/common/types'; import { generateCells } from '../../client/datascience/cellFactory'; import { Commands } from '../../client/datascience/constants'; +import { DataScienceErrorHandler } from '../../client/datascience/errorHandler/errorHandler'; import { InteractiveWindowCommandListener } from '../../client/datascience/interactive-window/interactiveWindowCommandListener'; import { InteractiveWindowProvider } from '../../client/datascience/interactive-window/interactiveWindowProvider'; import { JupyterExecutionFactory } from '../../client/datascience/jupyter/jupyterExecutionFactory'; @@ -69,6 +70,7 @@ suite('Interactive window command listener', async () => { const pythonSettings = new PythonSettings(undefined, new MockAutoSelectionService()); const disposableRegistry: IDisposable[] = []; const interactiveWindowProvider = mock(InteractiveWindowProvider); + const dataScienceErrorHandler = mock(DataScienceErrorHandler); const notebookImporter = mock(JupyterImporter); const notebookExporter = mock(JupyterExporter); const applicationShell = mock(ApplicationShell); @@ -216,7 +218,8 @@ suite('Interactive window command listener', async () => { instance(logger), instance(configService), statusProvider, - instance(notebookImporter)); + instance(notebookImporter), + instance(dataScienceErrorHandler)); result.register(commandManager); return result; From fa675646b1f26f6e2fe52ddc4be10ee3d78e6294 Mon Sep 17 00:00:00 2001 From: David Kutugata Date: Mon, 8 Jul 2019 13:53:49 -0700 Subject: [PATCH 04/19] removed serviceContainer from the data science error handler --- src/client/datascience/errorHandler/errorHandler.ts | 6 ++---- .../interactive-window/interactiveWindowCommandListener.ts | 2 +- 2 files changed, 3 insertions(+), 5 deletions(-) diff --git a/src/client/datascience/errorHandler/errorHandler.ts b/src/client/datascience/errorHandler/errorHandler.ts index e6d65c29c513..fe68d314092a 100644 --- a/src/client/datascience/errorHandler/errorHandler.ts +++ b/src/client/datascience/errorHandler/errorHandler.ts @@ -6,7 +6,6 @@ import { IInstallationChannelManager } from '../../common/installer/types'; import { ILogger, Product } from '../../common/types'; import * as localize from '../../common/utils/localize'; import { noop } from '../../common/utils/misc'; -import { IServiceContainer } from '../../ioc/types'; import { JupyterInstallError } from '../jupyter/jupyterInstallError'; import { JupyterSelfCertsError } from '../jupyter/jupyterSelfCertsError'; import { IDataScienceErrorHandler } from '../types'; @@ -15,7 +14,7 @@ import { IDataScienceErrorHandler } from '../types'; export class DataScienceErrorHandler implements IDataScienceErrorHandler { constructor(@inject(IApplicationShell) private applicationShell: IApplicationShell, @inject(ILogger) private logger: ILogger, - @inject(IServiceContainer) protected serviceContainer: IServiceContainer) { + @inject(IInstallationChannelManager) protected channels: IInstallationChannelManager) { } public handleError(err: Error) { @@ -26,8 +25,7 @@ export class DataScienceErrorHandler implements IDataScienceErrorHandler { localize.DataScience.notebookCheckForImportNo()) .then(response => { if (response === localize.DataScience.jupyterInstall()) { - const channels = this.serviceContainer.get(IInstallationChannelManager); - return channels.getInstallationChannel(Product.jupyter); + return this.channels.getInstallationChannel(Product.jupyter); } else { const jupyterError = err as JupyterInstallError; diff --git a/src/client/datascience/interactive-window/interactiveWindowCommandListener.ts b/src/client/datascience/interactive-window/interactiveWindowCommandListener.ts index b808cff6640a..709e7a65200b 100644 --- a/src/client/datascience/interactive-window/interactiveWindowCommandListener.ts +++ b/src/client/datascience/interactive-window/interactiveWindowCommandListener.ts @@ -45,7 +45,7 @@ export class InteractiveWindowCommandListener implements IDataScienceCommandList @inject(IConfigurationService) private configuration: IConfigurationService, @inject(IStatusProvider) private statusProvider: IStatusProvider, @inject(INotebookImporter) private jupyterImporter: INotebookImporter, - @inject(IDataScienceErrorHandler) protected dataScienceErrorHandler: IDataScienceErrorHandler + @inject(IDataScienceErrorHandler) private dataScienceErrorHandler: IDataScienceErrorHandler ) { // Listen to document open commands. We want to ask the user if they want to import. const disposable = this.documentManager.onDidOpenTextDocument(this.onOpenedDocument); From aa437f190d3ca488271a59f9e8afa95d163bb419 Mon Sep 17 00:00:00 2001 From: David Kutugata Date: Mon, 8 Jul 2019 15:20:35 -0700 Subject: [PATCH 05/19] added jupyter to the productService map --- src/client/common/installer/productService.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/src/client/common/installer/productService.ts b/src/client/common/installer/productService.ts index a6e0c7a53c0d..bb95dc91fbcb 100644 --- a/src/client/common/installer/productService.ts +++ b/src/client/common/installer/productService.ts @@ -28,6 +28,7 @@ export class ProductService implements IProductService { this.ProductTypes.set(Product.black, ProductType.Formatter); this.ProductTypes.set(Product.yapf, ProductType.Formatter); this.ProductTypes.set(Product.rope, ProductType.RefactoringLibrary); + this.ProductTypes.set(Product.jupyter, ProductType.Linter); } public getProductType(product: Product): ProductType { return this.ProductTypes.get(product)!; From 5ef1551a513727be3ff51048a25a9dc2a08f7c03 Mon Sep 17 00:00:00 2001 From: David Kutugata Date: Mon, 8 Jul 2019 16:44:25 -0700 Subject: [PATCH 06/19] added jupyter to a function in the productInstaller --- src/client/common/installer/productInstaller.ts | 1 + src/client/common/utils/localize.ts | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/src/client/common/installer/productInstaller.ts b/src/client/common/installer/productInstaller.ts index 02dd60ee3393..318f3883dd5f 100644 --- a/src/client/common/installer/productInstaller.ts +++ b/src/client/common/installer/productInstaller.ts @@ -333,6 +333,7 @@ function translateProductToModule(product: Product, purpose: ModuleNamePurpose): case Product.unittest: return 'unittest'; case Product.rope: return 'rope'; case Product.bandit: return 'bandit'; + case Product.jupyter: return 'jupyter'; default: { throw new Error(`Product ${product} cannot be installed as a Python Module.`); } diff --git a/src/client/common/utils/localize.ts b/src/client/common/utils/localize.ts index f3b0dfd2a505..123dd504d818 100644 --- a/src/client/common/utils/localize.ts +++ b/src/client/common/utils/localize.ts @@ -100,7 +100,7 @@ export namespace DataScience { export const notebookCheckForImportYes = localize('DataScience.notebookCheckForImportYes', 'Import'); export const notebookCheckForImportNo = localize('DataScience.notebookCheckForImportNo', 'Later'); export const notebookCheckForImportDontAskAgain = localize('DataScience.notebookCheckForImportDontAskAgain', 'Don\'t Ask Again'); - export const jupyterInstall = localize('DataScience.jupyterAskToInstall', 'Install'); + export const jupyterInstall = localize('DataScience.jupyterInstall', 'Install'); export const jupyterNotSupported = localize('DataScience.jupyterNotSupported', 'Jupyter is not installed'); export const jupyterNotSupportedBecauseOfEnvironment = localize('DataScience.jupyterNotSupportedBecauseOfEnvironment', 'Activating {0} to run Jupyter failed with {1}'); export const jupyterNbConvertNotSupported = localize('DataScience.jupyterNbConvertNotSupported', 'Jupyter nbconvert is not installed'); From 5e02817b54c4d23196790d1bf46183c659b359f0 Mon Sep 17 00:00:00 2001 From: David Kutugata Date: Mon, 8 Jul 2019 17:13:09 -0700 Subject: [PATCH 07/19] added the data science productType --- src/client/common/installer/productInstaller.ts | 10 ++++++++++ src/client/common/installer/productService.ts | 2 +- src/client/common/types.ts | 3 ++- 3 files changed, 13 insertions(+), 2 deletions(-) diff --git a/src/client/common/installer/productInstaller.ts b/src/client/common/installer/productInstaller.ts index 318f3883dd5f..0edd94414607 100644 --- a/src/client/common/installer/productInstaller.ts +++ b/src/client/common/installer/productInstaller.ts @@ -271,6 +271,14 @@ export class RefactoringLibraryInstaller extends BaseInstaller { } } +export class DataScienceInstaller extends BaseInstaller { + protected async promptToInstallImplementation(product: Product, resource?: Uri): Promise { + const productName = ProductNames.get(product)!; + const item = await this.appShell.showErrorMessage(`Data Science library ${productName} is not installed. Install?`, 'Yes', 'No'); + return item === 'Yes' ? this.install(product, resource) : InstallerResponse.Ignore; + } +} + @injectable() export class ProductInstaller implements IInstaller { private readonly productService: IProductService; @@ -307,6 +315,8 @@ export class ProductInstaller implements IInstaller { return new TestFrameworkInstaller(this.serviceContainer, this.outputChannel); case ProductType.RefactoringLibrary: return new RefactoringLibraryInstaller(this.serviceContainer, this.outputChannel); + case ProductType.DataScience: + return new DataScienceInstaller(this.serviceContainer, this.outputChannel); default: break; } diff --git a/src/client/common/installer/productService.ts b/src/client/common/installer/productService.ts index bb95dc91fbcb..98a2f190956d 100644 --- a/src/client/common/installer/productService.ts +++ b/src/client/common/installer/productService.ts @@ -28,7 +28,7 @@ export class ProductService implements IProductService { this.ProductTypes.set(Product.black, ProductType.Formatter); this.ProductTypes.set(Product.yapf, ProductType.Formatter); this.ProductTypes.set(Product.rope, ProductType.RefactoringLibrary); - this.ProductTypes.set(Product.jupyter, ProductType.Linter); + this.ProductTypes.set(Product.jupyter, ProductType.DataScience); } public getProductType(product: Product): ProductType { return this.ProductTypes.get(product)!; diff --git a/src/client/common/types.ts b/src/client/common/types.ts index e331cd21ed48..3d29874cb52e 100644 --- a/src/client/common/types.ts +++ b/src/client/common/types.ts @@ -75,7 +75,8 @@ export enum ProductType { Formatter = 'Formatter', TestFramework = 'TestFramework', RefactoringLibrary = 'RefactoringLibrary', - WorkspaceSymbols = 'WorkspaceSymbols' + WorkspaceSymbols = 'WorkspaceSymbols', + DataScience = 'DataScience' } export enum Product { From 6291348aca55635ab66f7d07687db2e6faed0e1e Mon Sep 17 00:00:00 2001 From: David Kutugata Date: Tue, 9 Jul 2019 10:30:35 -0700 Subject: [PATCH 08/19] excluded jupyter installation from a test --- src/test/common/installer/installer.invalidPath.unit.test.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/test/common/installer/installer.invalidPath.unit.test.ts b/src/test/common/installer/installer.invalidPath.unit.test.ts index a95b55d81421..51ea5007646a 100644 --- a/src/test/common/installer/installer.invalidPath.unit.test.ts +++ b/src/test/common/installer/installer.invalidPath.unit.test.ts @@ -55,7 +55,8 @@ suite('Module Installer - Invalid Paths', () => { case Product.isort: case Product.ctags: case Product.rope: - case Product.unittest: { + case Product.unittest: + case Product.jupyter: { return; } default: { From fe4b08cc87217e20975caae076444a72d8fd2dcb Mon Sep 17 00:00:00 2001 From: David Kutugata Date: Tue, 9 Jul 2019 10:55:59 -0700 Subject: [PATCH 09/19] removed jupyter from product installer --- src/client/common/installer/productInstaller.ts | 10 ---------- 1 file changed, 10 deletions(-) diff --git a/src/client/common/installer/productInstaller.ts b/src/client/common/installer/productInstaller.ts index 0edd94414607..318f3883dd5f 100644 --- a/src/client/common/installer/productInstaller.ts +++ b/src/client/common/installer/productInstaller.ts @@ -271,14 +271,6 @@ export class RefactoringLibraryInstaller extends BaseInstaller { } } -export class DataScienceInstaller extends BaseInstaller { - protected async promptToInstallImplementation(product: Product, resource?: Uri): Promise { - const productName = ProductNames.get(product)!; - const item = await this.appShell.showErrorMessage(`Data Science library ${productName} is not installed. Install?`, 'Yes', 'No'); - return item === 'Yes' ? this.install(product, resource) : InstallerResponse.Ignore; - } -} - @injectable() export class ProductInstaller implements IInstaller { private readonly productService: IProductService; @@ -315,8 +307,6 @@ export class ProductInstaller implements IInstaller { return new TestFrameworkInstaller(this.serviceContainer, this.outputChannel); case ProductType.RefactoringLibrary: return new RefactoringLibraryInstaller(this.serviceContainer, this.outputChannel); - case ProductType.DataScience: - return new DataScienceInstaller(this.serviceContainer, this.outputChannel); default: break; } From f7bd04ed38b6cba3f115d4b50274e0b8e6dc7b33 Mon Sep 17 00:00:00 2001 From: David Kutugata Date: Tue, 9 Jul 2019 12:39:24 -0700 Subject: [PATCH 10/19] added jupyter back to product installer --- src/client/common/installer/productInstaller.ts | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/src/client/common/installer/productInstaller.ts b/src/client/common/installer/productInstaller.ts index 318f3883dd5f..0edd94414607 100644 --- a/src/client/common/installer/productInstaller.ts +++ b/src/client/common/installer/productInstaller.ts @@ -271,6 +271,14 @@ export class RefactoringLibraryInstaller extends BaseInstaller { } } +export class DataScienceInstaller extends BaseInstaller { + protected async promptToInstallImplementation(product: Product, resource?: Uri): Promise { + const productName = ProductNames.get(product)!; + const item = await this.appShell.showErrorMessage(`Data Science library ${productName} is not installed. Install?`, 'Yes', 'No'); + return item === 'Yes' ? this.install(product, resource) : InstallerResponse.Ignore; + } +} + @injectable() export class ProductInstaller implements IInstaller { private readonly productService: IProductService; @@ -307,6 +315,8 @@ export class ProductInstaller implements IInstaller { return new TestFrameworkInstaller(this.serviceContainer, this.outputChannel); case ProductType.RefactoringLibrary: return new RefactoringLibraryInstaller(this.serviceContainer, this.outputChannel); + case ProductType.DataScience: + return new DataScienceInstaller(this.serviceContainer, this.outputChannel); default: break; } From 8c954a70b105089a736ea83c8ef7f7b05beaadf0 Mon Sep 17 00:00:00 2001 From: David Kutugata Date: Tue, 9 Jul 2019 13:03:21 -0700 Subject: [PATCH 11/19] added a productPath test for dataScience --- src/client/common/installer/productPath.ts | 10 ++++++++++ src/client/common/installer/serviceRegistry.ts | 3 ++- src/test/common/installer/productPath.unit.test.ts | 12 +++++++++++- 3 files changed, 23 insertions(+), 2 deletions(-) diff --git a/src/client/common/installer/productPath.ts b/src/client/common/installer/productPath.ts index 474c28c5e14f..43c7aac53d2e 100644 --- a/src/client/common/installer/productPath.ts +++ b/src/client/common/installer/productPath.ts @@ -99,3 +99,13 @@ export class RefactoringLibraryProductPathService extends BaseProductPathsServic return this.productInstaller.translateProductToModuleName(product, ModuleNamePurpose.run); } } + +@injectable() +export class DataScienceProductPathService extends BaseProductPathsService { + constructor(@inject(IServiceContainer) serviceContainer: IServiceContainer) { + super(serviceContainer); + } + public getExecutableNameFromSettings(product: Product, _?: Uri): string { + return this.productInstaller.translateProductToModuleName(product, ModuleNamePurpose.run); + } +} diff --git a/src/client/common/installer/serviceRegistry.ts b/src/client/common/installer/serviceRegistry.ts index 8ae4beffa549..0083fada0fd1 100644 --- a/src/client/common/installer/serviceRegistry.ts +++ b/src/client/common/installer/serviceRegistry.ts @@ -11,7 +11,7 @@ import { CondaInstaller } from './condaInstaller'; import { PipEnvInstaller } from './pipEnvInstaller'; import { PipInstaller } from './pipInstaller'; import { PoetryInstaller } from './poetryInstaller'; -import { CTagsProductPathService, FormatterProductPathService, LinterProductPathService, RefactoringLibraryProductPathService, TestFrameworkProductPathService } from './productPath'; +import { CTagsProductPathService, DataScienceProductPathService, FormatterProductPathService, LinterProductPathService, RefactoringLibraryProductPathService, TestFrameworkProductPathService } from './productPath'; import { ProductService } from './productService'; import { IInstallationChannelManager, IModuleInstaller, IProductPathService, IProductService } from './types'; @@ -28,5 +28,6 @@ export function registerTypes(serviceManager: IServiceManager) { serviceManager.addSingleton(IProductPathService, LinterProductPathService, ProductType.Linter); serviceManager.addSingleton(IProductPathService, TestFrameworkProductPathService, ProductType.TestFramework); serviceManager.addSingleton(IProductPathService, RefactoringLibraryProductPathService, ProductType.RefactoringLibrary); + serviceManager.addSingleton(IProductPathService, DataScienceProductPathService, ProductType.DataScience); serviceManager.addSingleton(IWebPanelProvider, WebPanelProvider); } diff --git a/src/test/common/installer/productPath.unit.test.ts b/src/test/common/installer/productPath.unit.test.ts index 4ba0017a7cb4..3c769f380d06 100644 --- a/src/test/common/installer/productPath.unit.test.ts +++ b/src/test/common/installer/productPath.unit.test.ts @@ -12,7 +12,7 @@ import * as TypeMoq from 'typemoq'; import { OutputChannel, Uri } from 'vscode'; import '../../../client/common/extensions'; import { ProductInstaller } from '../../../client/common/installer/productInstaller'; -import { CTagsProductPathService, FormatterProductPathService, LinterProductPathService, RefactoringLibraryProductPathService, TestFrameworkProductPathService } from '../../../client/common/installer/productPath'; +import { CTagsProductPathService, DataScienceProductPathService, FormatterProductPathService, LinterProductPathService, RefactoringLibraryProductPathService, TestFrameworkProductPathService } from '../../../client/common/installer/productPath'; import { ProductService } from '../../../client/common/installer/productService'; import { IProductService } from '../../../client/common/installer/types'; import { IConfigurationService, IFormattingSettings, IInstaller, IPythonSettings, ITestingSettings, IWorkspaceSymbolSettings, ModuleNamePurpose, Product, ProductType } from '../../../client/common/types'; @@ -179,6 +179,16 @@ suite('Product Path', () => { }); break; } + case ProductType.DataScience: { + test(`Ensure path is returned for ${product.name} (${resource ? 'With a resource' : 'without a resource'})`, async () => { + const productPathService = new DataScienceProductPathService(serviceContainer.object); + + const value = productPathService.getExecutableNameFromSettings(product.value, resource); + const moduleName = productInstaller.translateProductToModuleName(product.value, ModuleNamePurpose.run); + expect(value).to.be.equal(moduleName); + }); + break; + } default: { test(`No tests for Product Path of this Product Type ${product.name}`, () => { fail('No tests for Product Path of this Product Type'); From df377584446ecdc24717fd151cc59d6730389bff Mon Sep 17 00:00:00 2001 From: David Kutugata Date: Tue, 9 Jul 2019 13:37:00 -0700 Subject: [PATCH 12/19] excluded jupyter from checking if its installed on the product installer --- src/client/common/installer/productInstaller.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/client/common/installer/productInstaller.ts b/src/client/common/installer/productInstaller.ts index 0edd94414607..3bae41ee1255 100644 --- a/src/client/common/installer/productInstaller.ts +++ b/src/client/common/installer/productInstaller.ts @@ -76,7 +76,7 @@ export abstract class BaseInstaller { } public async isInstalled(product: Product, resource?: Uri): Promise { - if (product === Product.unittest) { + if (product === Product.unittest || product === Product.jupyter) { return true; } // User may have customized the module name or provided the fully qualified path. From 06748615dde00d55d12b970bc06c7a23feb5357e Mon Sep 17 00:00:00 2001 From: David Kutugata Date: Tue, 9 Jul 2019 14:14:22 -0700 Subject: [PATCH 13/19] Added DataScienceProductPathService to 3 tests --- src/test/common/installer.test.ts | 3 ++- src/test/linters/lint.multiroot.test.ts | 4 ++-- src/test/linters/lint.test.ts | 3 ++- 3 files changed, 6 insertions(+), 4 deletions(-) diff --git a/src/test/common/installer.test.ts b/src/test/common/installer.test.ts index 8eb649d073c3..e3bb38804fa0 100644 --- a/src/test/common/installer.test.ts +++ b/src/test/common/installer.test.ts @@ -7,7 +7,7 @@ import { WorkspaceService } from '../../client/common/application/workspace'; import { ConfigurationService } from '../../client/common/configuration/service'; import { InstallationChannelManager } from '../../client/common/installer/channelManager'; import { ProductInstaller } from '../../client/common/installer/productInstaller'; -import { CTagsProductPathService, FormatterProductPathService, LinterProductPathService, RefactoringLibraryProductPathService, TestFrameworkProductPathService } from '../../client/common/installer/productPath'; +import { CTagsProductPathService, DataScienceProductPathService, FormatterProductPathService, LinterProductPathService, RefactoringLibraryProductPathService, TestFrameworkProductPathService } from '../../client/common/installer/productPath'; import { ProductService } from '../../client/common/installer/productService'; import { IInstallationChannelManager, IModuleInstaller, IProductPathService, IProductService } from '../../client/common/installer/types'; import { Logger } from '../../client/common/logger'; @@ -74,6 +74,7 @@ suite('Installer', () => { ioc.serviceManager.addSingleton(IProductPathService, LinterProductPathService, ProductType.Linter); ioc.serviceManager.addSingleton(IProductPathService, TestFrameworkProductPathService, ProductType.TestFramework); ioc.serviceManager.addSingleton(IProductPathService, RefactoringLibraryProductPathService, ProductType.RefactoringLibrary); + ioc.serviceManager.addSingleton(IProductPathService, DataScienceProductPathService, ProductType.DataScience); } async function resetSettings() { await updateSetting('linting.pylintEnabled', true, rootWorkspaceUri, ConfigurationTarget.Workspace); diff --git a/src/test/linters/lint.multiroot.test.ts b/src/test/linters/lint.multiroot.test.ts index fb328c00b914..2fda48c4a7b6 100644 --- a/src/test/linters/lint.multiroot.test.ts +++ b/src/test/linters/lint.multiroot.test.ts @@ -2,7 +2,7 @@ import * as assert from 'assert'; import * as path from 'path'; import { CancellationTokenSource, ConfigurationTarget, OutputChannel, Uri, workspace } from 'vscode'; import { PythonSettings } from '../../client/common/configSettings'; -import { CTagsProductPathService, FormatterProductPathService, LinterProductPathService, RefactoringLibraryProductPathService, TestFrameworkProductPathService } from '../../client/common/installer/productPath'; +import { CTagsProductPathService, DataScienceProductPathService, FormatterProductPathService, LinterProductPathService, RefactoringLibraryProductPathService, TestFrameworkProductPathService } from '../../client/common/installer/productPath'; import { ProductService } from '../../client/common/installer/productService'; import { IProductPathService, IProductService } from '../../client/common/installer/types'; import { IConfigurationService, IOutputChannel, Product, ProductType } from '../../client/common/types'; @@ -50,7 +50,7 @@ suite('Multiroot Linting', () => { ioc.serviceManager.addSingleton(IProductPathService, LinterProductPathService, ProductType.Linter); ioc.serviceManager.addSingleton(IProductPathService, TestFrameworkProductPathService, ProductType.TestFramework); ioc.serviceManager.addSingleton(IProductPathService, RefactoringLibraryProductPathService, ProductType.RefactoringLibrary); - + ioc.serviceManager.addSingleton(IProductPathService, DataScienceProductPathService, ProductType.DataScience); } async function createLinter(product: Product, resource?: Uri): Promise { diff --git a/src/test/linters/lint.test.ts b/src/test/linters/lint.test.ts index 0e09b5604809..5f3ef1c1da8e 100644 --- a/src/test/linters/lint.test.ts +++ b/src/test/linters/lint.test.ts @@ -8,7 +8,7 @@ import { ConfigurationTarget, Uri } from 'vscode'; import { WorkspaceService } from '../../client/common/application/workspace'; import { Product } from '../../client/common/installer/productInstaller'; import { - CTagsProductPathService, FormatterProductPathService, LinterProductPathService, + CTagsProductPathService, DataScienceProductPathService, FormatterProductPathService, LinterProductPathService, RefactoringLibraryProductPathService, TestFrameworkProductPathService } from '../../client/common/installer/productPath'; import { ProductService } from '../../client/common/installer/productService'; @@ -64,6 +64,7 @@ suite('Linting Settings', () => { ioc.serviceManager.addSingleton(IProductPathService, LinterProductPathService, ProductType.Linter); ioc.serviceManager.addSingleton(IProductPathService, TestFrameworkProductPathService, ProductType.TestFramework); ioc.serviceManager.addSingleton(IProductPathService, RefactoringLibraryProductPathService, ProductType.RefactoringLibrary); + ioc.serviceManager.addSingleton(IProductPathService, DataScienceProductPathService, ProductType.DataScience); } async function resetSettings(lintingEnabled = true) { From 9d02f246aa336380f153d320a1b23b8ed0076df5 Mon Sep 17 00:00:00 2001 From: David Kutugata Date: Tue, 9 Jul 2019 14:45:25 -0700 Subject: [PATCH 14/19] added done(to two tests) --- src/test/common/installer.test.ts | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/test/common/installer.test.ts b/src/test/common/installer.test.ts index e3bb38804fa0..1dd2322150a1 100644 --- a/src/test/common/installer.test.ts +++ b/src/test/common/installer.test.ts @@ -95,7 +95,7 @@ suite('Installer', () => { await checkInstalledDef.promise; } getNamesAndValues(Product).forEach(prod => { - test(`Ensure isInstalled for Product: '${prod.name}' executes the right command`, async () => { + test(`Ensure isInstalled for Product: '${prod.name}' executes the right command`, async (done) => { ioc.serviceManager.addSingletonInstance(IModuleInstaller, new MockModuleInstaller('one', false)); ioc.serviceManager.addSingletonInstance(IModuleInstaller, new MockModuleInstaller('two', true)); ioc.serviceManager.addSingletonInstance(ITerminalHelper, instance(mock(TerminalHelper))); @@ -103,6 +103,7 @@ suite('Installer', () => { return; } await testCheckingIfProductIsInstalled(prod.value); + done(); }); }); @@ -122,7 +123,7 @@ suite('Installer', () => { await checkInstalledDef.promise; } getNamesAndValues(Product).forEach(prod => { - test(`Ensure install for Product: '${prod.name}' executes the right command in IModuleInstaller`, async () => { + test(`Ensure install for Product: '${prod.name}' executes the right command in IModuleInstaller`, async (done) => { ioc.serviceManager.addSingletonInstance(IModuleInstaller, new MockModuleInstaller('one', false)); ioc.serviceManager.addSingletonInstance(IModuleInstaller, new MockModuleInstaller('two', true)); ioc.serviceManager.addSingletonInstance(ITerminalHelper, instance(mock(TerminalHelper))); @@ -130,6 +131,7 @@ suite('Installer', () => { return; } await testInstallingProduct(prod.value); + done(); }); }); }); From a284cc50db2f39a0da4d167491abcc08e3ef3c1e Mon Sep 17 00:00:00 2001 From: David Kutugata Date: Tue, 9 Jul 2019 15:28:31 -0700 Subject: [PATCH 15/19] added a binding of IDataScienceErrorHandler --- .../datascience/dataScienceIocContainer.ts | 41 ++++++++++--------- 1 file changed, 22 insertions(+), 19 deletions(-) diff --git a/src/test/datascience/dataScienceIocContainer.ts b/src/test/datascience/dataScienceIocContainer.ts index 25d6d59d4ce7..3b0a7976dd38 100644 --- a/src/test/datascience/dataScienceIocContainer.ts +++ b/src/test/datascience/dataScienceIocContainer.ts @@ -91,6 +91,7 @@ import { DataViewerProvider } from '../../client/datascience/data-viewing/dataVi import { CellHashProvider } from '../../client/datascience/editor-integration/cellhashprovider'; import { CodeLensFactory } from '../../client/datascience/editor-integration/codeLensFactory'; import { CodeWatcher } from '../../client/datascience/editor-integration/codewatcher'; +import { DataScienceErrorHandler } from '../../client/datascience/errorHandler/errorHandler'; import { DotNetIntellisenseProvider } from '../../client/datascience/interactive-window/intellisense/dotNetIntellisenseProvider'; @@ -119,6 +120,7 @@ import { ICodeWatcher, IDataScience, IDataScienceCommandListener, + IDataScienceErrorHandler, IDataViewer, IDataViewerProvider, IInteractiveWindow, @@ -227,7 +229,7 @@ export class DataScienceIocContainer extends UnitTestIocContainer { public wrapperCreatedPromise: Deferred | undefined; public postMessage: ((ev: MessageEvent) => void) | undefined; // tslint:disable-next-line:no-any - private missedMessages : any[] = []; + private missedMessages: any[] = []; private pythonSettings = new class extends PythonSettings { public fireChangeEvent() { this.changed.fire(); @@ -325,6 +327,7 @@ export class DataScienceIocContainer extends UnitTestIocContainer { this.serviceManager.add(ICodeWatcher, CodeWatcher); this.serviceManager.add(ICodeExecutionHelper, CodeExecutionHelper); this.serviceManager.add(IDataScienceCommandListener, InteractiveWindowCommandListener); + this.serviceManager.add(IDataScienceErrorHandler, DataScienceErrorHandler); this.serviceManager.addSingleton(IJupyterVariables, JupyterVariables); this.serviceManager.addSingleton(IJupyterDebugger, JupyterDebugger); @@ -552,23 +555,23 @@ export class DataScienceIocContainer extends UnitTestIocContainer { // Setup the webpanel provider so that it returns our dummy web panel. It will have to talk to our global JSDOM window so that the react components can link into it webPanelProvider.setup(p => p.create(TypeMoq.It.isAny(), TypeMoq.It.isAny(), TypeMoq.It.isAnyString(), TypeMoq.It.isAnyString(), TypeMoq.It.isAnyString(), TypeMoq.It.isAny())).returns( (_viewColumn: ViewColumn, listener: IWebPanelMessageListener, _title: string, _script: string, _css: string) => { - // Keep track of the current listener. It listens to messages through the vscode api - this.webPanelListener = listener; - - // Send messages that were already posted but were missed. - // During normal operation, the react control will not be created before - // the webPanelListener - if (this.missedMessages.length && this.webPanelListener) { - this.missedMessages.forEach(m => this.webPanelListener ? this.webPanelListener.onMessage(m.type, m.payload) : noop()); - - // Note, you might think we should clean up the messages. However since the mount only occurs once, we might - // create multiple webpanels with the same mount. We need to resend these messages to - // other webpanels that get created with the same mount. - } + // Keep track of the current listener. It listens to messages through the vscode api + this.webPanelListener = listener; + + // Send messages that were already posted but were missed. + // During normal operation, the react control will not be created before + // the webPanelListener + if (this.missedMessages.length && this.webPanelListener) { + this.missedMessages.forEach(m => this.webPanelListener ? this.webPanelListener.onMessage(m.type, m.payload) : noop()); + + // Note, you might think we should clean up the messages. However since the mount only occurs once, we might + // create multiple webpanels with the same mount. We need to resend these messages to + // other webpanels that get created with the same mount. + } - // Return our dummy web panel - return webPanel.object; - }); + // Return our dummy web panel + return webPanel.object; + }); webPanel.setup(p => p.postMessage(TypeMoq.It.isAny())).callback((m: WebPanelMessage) => { const message = createMessageEvent(m); if (this.postMessage) { @@ -605,7 +608,7 @@ export class DataScienceIocContainer extends UnitTestIocContainer { this.pythonSettings.pythonPath = newPath; this.pythonSettings.fireChangeEvent(); this.configChangeEvent.fire({ - affectsConfiguration(_s: string, _r?: Uri) : boolean { + affectsConfiguration(_s: string, _r?: Uri): boolean { return true; } }); @@ -615,7 +618,7 @@ export class DataScienceIocContainer extends UnitTestIocContainer { return this.jupyterMock; } - public get(serviceIdentifier: interfaces.ServiceIdentifier, name?: string | number | symbol) : T { + public get(serviceIdentifier: interfaces.ServiceIdentifier, name?: string | number | symbol): T { return this.serviceManager.get(serviceIdentifier, name); } From ae9a56d47568f188de3e73962f02a77c98b8d273 Mon Sep 17 00:00:00 2001 From: David Kutugata Date: Tue, 9 Jul 2019 15:57:21 -0700 Subject: [PATCH 16/19] added IInstallationChannelManager binding --- src/test/datascience/dataScienceIocContainer.ts | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/test/datascience/dataScienceIocContainer.ts b/src/test/datascience/dataScienceIocContainer.ts index 3b0a7976dd38..86d32d250185 100644 --- a/src/test/datascience/dataScienceIocContainer.ts +++ b/src/test/datascience/dataScienceIocContainer.ts @@ -41,6 +41,8 @@ import { import { AsyncDisposableRegistry } from '../../client/common/asyncDisposableRegistry'; import { PythonSettings } from '../../client/common/configSettings'; import { EXTENSION_ROOT_DIR } from '../../client/common/constants'; +import { InstallationChannelManager } from '../../client/common/installer/channelManager'; +import { IInstallationChannelManager } from '../../client/common/installer/types'; import { Logger } from '../../client/common/logger'; import { PersistentStateFactory } from '../../client/common/persistentState'; import { IS_WINDOWS } from '../../client/common/platform/constants'; @@ -328,6 +330,7 @@ export class DataScienceIocContainer extends UnitTestIocContainer { this.serviceManager.add(ICodeExecutionHelper, CodeExecutionHelper); this.serviceManager.add(IDataScienceCommandListener, InteractiveWindowCommandListener); this.serviceManager.add(IDataScienceErrorHandler, DataScienceErrorHandler); + this.serviceManager.add(IInstallationChannelManager, InstallationChannelManager); this.serviceManager.addSingleton(IJupyterVariables, JupyterVariables); this.serviceManager.addSingleton(IJupyterDebugger, JupyterDebugger); From 83833efb72464cc77a84a564a369aef0c370e9aa Mon Sep 17 00:00:00 2001 From: David Kutugata Date: Tue, 9 Jul 2019 16:29:11 -0700 Subject: [PATCH 17/19] added jupyter as an exception to 'getNamesAndValues' test --- src/test/common/installer.test.ts | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/src/test/common/installer.test.ts b/src/test/common/installer.test.ts index 1dd2322150a1..f3217228af62 100644 --- a/src/test/common/installer.test.ts +++ b/src/test/common/installer.test.ts @@ -95,15 +95,14 @@ suite('Installer', () => { await checkInstalledDef.promise; } getNamesAndValues(Product).forEach(prod => { - test(`Ensure isInstalled for Product: '${prod.name}' executes the right command`, async (done) => { + test(`Ensure isInstalled for Product: '${prod.name}' executes the right command`, async () => { ioc.serviceManager.addSingletonInstance(IModuleInstaller, new MockModuleInstaller('one', false)); ioc.serviceManager.addSingletonInstance(IModuleInstaller, new MockModuleInstaller('two', true)); ioc.serviceManager.addSingletonInstance(ITerminalHelper, instance(mock(TerminalHelper))); - if (prod.value === Product.ctags || prod.value === Product.unittest || prod.value === Product.isort) { + if (prod.value === Product.ctags || prod.value === Product.unittest || prod.value === Product.isort || prod.value === Product.jupyter) { return; } await testCheckingIfProductIsInstalled(prod.value); - done(); }); }); From 0dd1e98f9a9c3b1b9dd34b3c667d424625873dcc Mon Sep 17 00:00:00 2001 From: David Kutugata Date: Tue, 9 Jul 2019 16:54:39 -0700 Subject: [PATCH 18/19] added jupyter as an exception to the 'getNamesAndValues' test --- src/test/common/installer.test.ts | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/src/test/common/installer.test.ts b/src/test/common/installer.test.ts index f3217228af62..04e008783ef6 100644 --- a/src/test/common/installer.test.ts +++ b/src/test/common/installer.test.ts @@ -122,15 +122,14 @@ suite('Installer', () => { await checkInstalledDef.promise; } getNamesAndValues(Product).forEach(prod => { - test(`Ensure install for Product: '${prod.name}' executes the right command in IModuleInstaller`, async (done) => { + test(`Ensure install for Product: '${prod.name}' executes the right command in IModuleInstaller`, async () => { ioc.serviceManager.addSingletonInstance(IModuleInstaller, new MockModuleInstaller('one', false)); ioc.serviceManager.addSingletonInstance(IModuleInstaller, new MockModuleInstaller('two', true)); ioc.serviceManager.addSingletonInstance(ITerminalHelper, instance(mock(TerminalHelper))); - if (prod.value === Product.unittest || prod.value === Product.ctags || prod.value === Product.isort) { + if (prod.value === Product.unittest || prod.value === Product.ctags || prod.value === Product.isort || prod.value === Product.jupyter) { return; } await testInstallingProduct(prod.value); - done(); }); }); }); From 963e12c6dcebff581e46aa306233e70aaf57214a Mon Sep 17 00:00:00 2001 From: David Kutugata Date: Tue, 9 Jul 2019 17:24:15 -0700 Subject: [PATCH 19/19] localized a new string from productInstaller.ts --- package.nls.json | 1 + src/client/common/installer/productInstaller.ts | 3 ++- src/client/common/utils/localize.ts | 1 + 3 files changed, 4 insertions(+), 1 deletion(-) diff --git a/package.nls.json b/package.nls.json index 276c6342855c..ecaef58aacf6 100644 --- a/package.nls.json +++ b/package.nls.json @@ -142,6 +142,7 @@ "Linter.enableLinter": "Enable {0}", "Linter.enablePylint": "You have a pylintrc file in your workspace. Do you want to enable pylint?", "Linter.replaceWithSelectedLinter": "Multiple linters are enabled in settings. Replace with '{0}'?", + "DataScience.libraryNotInstalled": "Data Science library {0} is not installed. Install?", "DataScience.jupyterInstall": "Install", "DataScience.jupyterSelectURILaunchLocal": "Launch a local Jupyter server when needed", "DataScience.jupyterSelectURISpecifyURI": "Type in the URI to connect to a running Jupyter server", diff --git a/src/client/common/installer/productInstaller.ts b/src/client/common/installer/productInstaller.ts index 3bae41ee1255..37e534903cd6 100644 --- a/src/client/common/installer/productInstaller.ts +++ b/src/client/common/installer/productInstaller.ts @@ -4,6 +4,7 @@ import { inject, injectable, named } from 'inversify'; import * as os from 'os'; import { OutputChannel, Uri } from 'vscode'; import '../../common/extensions'; +import * as localize from '../../common/utils/localize'; import { IServiceContainer } from '../../ioc/types'; import { LinterId } from '../../linters/types'; import { sendTelemetryEvent } from '../../telemetry'; @@ -274,7 +275,7 @@ export class RefactoringLibraryInstaller extends BaseInstaller { export class DataScienceInstaller extends BaseInstaller { protected async promptToInstallImplementation(product: Product, resource?: Uri): Promise { const productName = ProductNames.get(product)!; - const item = await this.appShell.showErrorMessage(`Data Science library ${productName} is not installed. Install?`, 'Yes', 'No'); + const item = await this.appShell.showErrorMessage(localize.DataScience.libraryNotInstalled().format(productName), 'Yes', 'No'); return item === 'Yes' ? this.install(product, resource) : InstallerResponse.Ignore; } } diff --git a/src/client/common/utils/localize.ts b/src/client/common/utils/localize.ts index 123dd504d818..4101ac6a4210 100644 --- a/src/client/common/utils/localize.ts +++ b/src/client/common/utils/localize.ts @@ -100,6 +100,7 @@ export namespace DataScience { export const notebookCheckForImportYes = localize('DataScience.notebookCheckForImportYes', 'Import'); export const notebookCheckForImportNo = localize('DataScience.notebookCheckForImportNo', 'Later'); export const notebookCheckForImportDontAskAgain = localize('DataScience.notebookCheckForImportDontAskAgain', 'Don\'t Ask Again'); + export const libraryNotInstalled = localize('DataScience.libraryNotInstalled', 'Data Science library {0} is not installed. Install?'); export const jupyterInstall = localize('DataScience.jupyterInstall', 'Install'); export const jupyterNotSupported = localize('DataScience.jupyterNotSupported', 'Jupyter is not installed'); export const jupyterNotSupportedBecauseOfEnvironment = localize('DataScience.jupyterNotSupportedBecauseOfEnvironment', 'Activating {0} to run Jupyter failed with {1}');