From 70a3148605d69b86452fcfb6dd5083a4033720b0 Mon Sep 17 00:00:00 2001 From: rchiodo Date: Thu, 28 Mar 2019 17:08:50 -0700 Subject: [PATCH 01/10] Generalize lists into data frames --- .../getJupyterVariableDataFrameInfo.py | 27 ++++--- .../getJupyterVariableDataFrameRows.py | 13 +++- src/client/common/utils/localize.ts | 1 + .../datascience/jupyter/jupyterVariables.ts | 72 +++++++++---------- 4 files changed, 61 insertions(+), 52 deletions(-) diff --git a/pythonFiles/datascience/getJupyterVariableDataFrameInfo.py b/pythonFiles/datascience/getJupyterVariableDataFrameInfo.py index 59c8a2d6d79b..18a006686f3c 100644 --- a/pythonFiles/datascience/getJupyterVariableDataFrameInfo.py +++ b/pythonFiles/datascience/getJupyterVariableDataFrameInfo.py @@ -7,8 +7,14 @@ _VSCODE_evalResult = eval(_VSCODE_targetVariable['name']) # First list out the columns of the data frame (assuming it is one for now) -_VSCODE_columnTypes = list(_VSCODE_evalResult.dtypes) -_VSCODE_columnNames = list(_VSCODE_evalResult) +_VSCODE_columnTypes = [] +_VSCODE_columnNames = [] +if (hasattr(_VSCODE_evalResult, 'dtypes')): + _VSCODE_columnTypes = list(_VSCODE_evalResult.dtypes) + _VSCODE_columnNames = list(_VSCODE_evalResult) +elif _VSCODE_targetVariable['type'] == 'list': + _VSCODE_columnTypes = ['string'] # Might be able to be more specific here? + _VSCODE_columnNames = ['_VSCode_JupyterValuesColumn'] # Make sure we have an index column (see code in getJupyterVariableDataFrameRows.py) if 'index' not in _VSCODE_columnNames: @@ -33,20 +39,13 @@ _VSCODE_targetVariable['columns'] = _VSCODE_columns del _VSCODE_columns -# Figure out shape if not already there -if 'shape' not in _VSCODE_targetVariable: - _VSCODE_targetVariable['shape'] = str(_VSCODE_evalResult.shape) - -# Row count is actually embedded in shape. Should be the second number -import re as _VSCODE_re -_VSCODE_regex = r"\(\s*(\d+),\s*(\d+)\s*\)" -_VSCODE_matches = _VSCODE_re.search(_VSCODE_regex, _VSCODE_targetVariable['shape']) -if (_VSCODE_matches): - _VSCODE_targetVariable['rowCount'] = int(_VSCODE_matches[1]) - del _VSCODE_matches +# Figure out shape if not already there. Use the shape to compute the row count +if (hasattr(_VSCODE_evalResult, "shape")): + _VSCODE_targetVariable['rowCount'] = _VSCODE_evalResult.shape[0] +elif _VSCODE_targetVariable['type'] == 'list': + _VSCODE_targetVariable['rowCount'] = len(_VSCODE_evalResult) else: _VSCODE_targetVariable['rowCount'] = 0 -del _VSCODE_regex # Transform this back into a string print(_VSCODE_json.dumps(_VSCODE_targetVariable)) \ No newline at end of file diff --git a/pythonFiles/datascience/getJupyterVariableDataFrameRows.py b/pythonFiles/datascience/getJupyterVariableDataFrameRows.py index 390e958d67c1..fdabb62c1518 100644 --- a/pythonFiles/datascience/getJupyterVariableDataFrameRows.py +++ b/pythonFiles/datascience/getJupyterVariableDataFrameRows.py @@ -1,5 +1,6 @@ # Query Jupyter server for the rows of a data frame import json as _VSCODE_json +import pandas as _VSCODE_pd import pandas.io.json as _VSCODE_pd_json # In IJupyterVariables.getValue this '_VSCode_JupyterTestValue' will be replaced with the json stringified value of the target variable @@ -12,12 +13,20 @@ _VSCODE_startRow = max(_VSCode_JupyterStartRow, 0) _VSCODE_endRow = min(_VSCode_JupyterEndRow, _VSCODE_targetVariable['rowCount']) +# Assume we have a dataframe. If not, turn our eval result into a dataframe +_VSCODE_df = _VSCODE_evalResult +if (_VSCODE_targetVariable['type'] == 'list'): + _VSCODE_df = _VSCODE_pd.DataFrame({'_VSCode_JupyterValuesColumn':_VSCODE_evalResult}) +# If not a known type, then just let pandas handle it. +elif not (hasattr(_VSCODE_df, 'iloc')): + _VSCODE_df = _VSCODE_pd.DataFrame(_VSCODE_evalResult) + # Turn into JSON using pandas. We use pandas because it's about 3 orders of magnitude faster to turn into JSON -_VSCODE_rows = df.iloc[_VSCODE_startRow:_VSCODE_endRow] +_VSCODE_rows = _VSCODE_df.iloc[_VSCODE_startRow:_VSCODE_endRow] _VSCODE_result = _VSCODE_pd_json.to_json(None, _VSCODE_rows, orient='table', date_format='iso') -print(_VSCODE_result) # Cleanup our variables +del _VSCODE_df del _VSCODE_endRow del _VSCODE_startRow del _VSCODE_rows diff --git a/src/client/common/utils/localize.ts b/src/client/common/utils/localize.ts index d4be1acfbec8..b9176b063789 100644 --- a/src/client/common/utils/localize.ts +++ b/src/client/common/utils/localize.ts @@ -150,6 +150,7 @@ export namespace DataScience { export const noRowsInDataExplorer = localize('DataScience.noRowsInDataExplorer', 'Fetching data ...'); export const pandasTooOldForViewingFormat = localize('DataScience.pandasTooOldForViewingFormat', 'Python package \'pandas\' is version {0}. Version 0.20 or greater is required for viewing data.'); export const pandasRequiredForViewing = localize('DataScience.pandasRequiredForViewing', 'Python package \'pandas\' is required for viewing data.'); + export const valuesColumn = localize('DataScience.valuesColumn', 'values'); } export namespace DebugConfigurationPrompts { diff --git a/src/client/datascience/jupyter/jupyterVariables.ts b/src/client/datascience/jupyter/jupyterVariables.ts index 02b44eb73562..5ccafd397380 100644 --- a/src/client/datascience/jupyter/jupyterVariables.ts +++ b/src/client/datascience/jupyter/jupyterVariables.ts @@ -14,6 +14,7 @@ import * as localize from '../../common/utils/localize'; import { EXTENSION_ROOT_DIR } from '../../constants'; import { Identifiers } from '../constants'; import { ICell, IHistoryProvider, IJupyterExecution, IJupyterVariable, IJupyterVariables } from '../types'; +import { replace } from 'event-stream'; @injectable() export class JupyterVariables implements IJupyterVariables { @@ -36,7 +37,7 @@ export class JupyterVariables implements IJupyterVariables { return this.runScript( undefined, [], - (_v: IJupyterVariable | undefined) => this.fetchVariablesScript!); + this.fetchVariablesScript); } public async getValue(targetVariable: IJupyterVariable): Promise { @@ -44,13 +45,7 @@ export class JupyterVariables implements IJupyterVariables { return this.runScript( targetVariable, targetVariable, - (_v: IJupyterVariable | undefined) => { - // Prep our targetVariable to send over - const variableString = JSON.stringify(targetVariable); - - // Use just the name of the target variable to fetch the value - return this.fetchVariableValueScript!.replace(/_VSCode_JupyterTestValue/g, variableString); - }); + this.fetchVariableValueScript); } public async getDataFrameInfo(targetVariable: IJupyterVariable): Promise { @@ -58,13 +53,8 @@ export class JupyterVariables implements IJupyterVariables { return this.runScript( targetVariable, targetVariable, - (_v: IJupyterVariable | undefined) => { - // Prep our targetVariable to send over - const variableString = JSON.stringify(targetVariable); - - // Use just the name of the target variable to fetch the data - return this.fetchDataFrameInfoScript!.replace(/(_VSCode_JupyterTestValue)/g, variableString); - }); + this.fetchDataFrameInfoScript, + [{key: '_VSCode_JupyterValuesColumn', value: localize.DataScience.valuesColumn()}]); } public async getDataFrameRows(targetVariable: IJupyterVariable, start: number, end: number): Promise { @@ -72,23 +62,12 @@ export class JupyterVariables implements IJupyterVariables { return this.runScript( targetVariable, {}, - (_v: IJupyterVariable | undefined) => { - // Prep our targetVariable to send over - const variableString = JSON.stringify(targetVariable); - - // Replace the test value with our current value. Replace start and end as well - return this.fetchDataFrameRowsScript!.replace(/_VSCode_JupyterTestValue|_VSCode_JupyterStartRow|_VSCode_JupyterEndRow/g, (match: string) => { - if (match === '_VSCode_JupyterTestValue') { - return variableString; - } else if (match === '_VSCode_JupyterStartRow') { - return start.toString(); - } else if (match === '_VSCode_JupyterEndRow') { - return end.toString(); - } - - return match; - }); - }); + this.fetchDataFrameRowsScript, + [ + {key: '_VSCode_JupyterValuesColumn', value: localize.DataScience.valuesColumn()}, + {key: '_VSCode_JupyterStartRow', value: start.toString()}, + {key: '_VSCode_JupyterEndRow', value: end.toString()} + ]); } // Private methods @@ -112,19 +91,40 @@ export class JupyterVariables implements IJupyterVariables { private async runScript( targetVariable: IJupyterVariable | undefined, defaultValue: T, - fetchScriptText: (v: IJupyterVariable | undefined) => string): Promise { + scriptBaseText: string | undefined, + extraReplacements: { key: string; value: string }[] = []): Promise { if (!this.filesLoaded) { await this.loadVariableFiles(); } const activeServer = await this.jupyterExecution.getServer(await this.historyProvider.getNotebookOptions()); - if (!activeServer) { + if (!activeServer || !scriptBaseText) { // No active server just return the unchanged target variable return defaultValue; } - // Generate the new script text - const scriptText = fetchScriptText(targetVariable); + // Prep our targetVariable to send over + const variableString = JSON.stringify(targetVariable); + + // Setup a regex + const regexPattern = extraReplacements.length === 0 ? '_VSCode_JupyterTestValue' : + ['_VSCode_JupyterTestValue', ...extraReplacements.map(v => v.key)].join('|'); + const replaceRegex = new RegExp(regexPattern, 'g'); + + // Replace the test value with our current value. Replace start and end as well + const scriptText = scriptBaseText.replace(replaceRegex, (match: string) => { + if (match === '_VSCode_JupyterTestValue') { + return variableString; + } else { + const index = extraReplacements.findIndex(v => v.key === match); + if (index >= 0) { + return extraReplacements[index].value; + } + } + + return match; + }); + // Execute this on the jupyter server. const results = await activeServer.execute(scriptText, Identifiers.EmptyFileName, 0, uuid(), undefined, true); From 6b66fb344330183c53ed68ab703125624392db59 Mon Sep 17 00:00:00 2001 From: rchiodo Date: Fri, 29 Mar 2019 10:03:57 -0700 Subject: [PATCH 02/10] Support for series/np.array/dict --- .../getJupyterVariableDataFrameInfo.py | 35 +++++++++++++------ .../getJupyterVariableDataFrameRows.py | 9 +++++ .../datascience/data-viewing/dataExplorer.ts | 28 ++++++++++----- .../datascience/jupyter/jupyterVariables.ts | 2 -- .../data-explorer/mainPanel.tsx | 5 +-- 5 files changed, 57 insertions(+), 22 deletions(-) diff --git a/pythonFiles/datascience/getJupyterVariableDataFrameInfo.py b/pythonFiles/datascience/getJupyterVariableDataFrameInfo.py index 18a006686f3c..29a9e68fdc7f 100644 --- a/pythonFiles/datascience/getJupyterVariableDataFrameInfo.py +++ b/pythonFiles/datascience/getJupyterVariableDataFrameInfo.py @@ -1,5 +1,6 @@ # Query Jupyter server for the info about a dataframe import json as _VSCODE_json +import pandas as _VSCODE_pd # In IJupyterVariables.getValue this '_VSCode_JupyterTestValue' will be replaced with the json stringified value of the target variable # Indexes off of _VSCODE_targetVariable need to index types that are part of IJupyterVariable @@ -9,12 +10,26 @@ # First list out the columns of the data frame (assuming it is one for now) _VSCODE_columnTypes = [] _VSCODE_columnNames = [] -if (hasattr(_VSCODE_evalResult, 'dtypes')): - _VSCODE_columnTypes = list(_VSCODE_evalResult.dtypes) - _VSCODE_columnNames = list(_VSCODE_evalResult) -elif _VSCODE_targetVariable['type'] == 'list': +if _VSCODE_targetVariable['type'] == 'list': _VSCODE_columnTypes = ['string'] # Might be able to be more specific here? _VSCODE_columnNames = ['_VSCode_JupyterValuesColumn'] +elif _VSCODE_targetVariable['type'] == 'Series': + _VSCODE_evalResult = _VSCODE_pd.Series.to_frame(_VSCODE_evalResult) + _VSCODE_columnTypes = list(_VSCODE_evalResult.dtypes) + _VSCODE_columnNames = list(_VSCODE_evalResult) +elif _VSCODE_targetVariable['type'] == 'dict': + _VSCODE_evalResult = _VSCODE_pd.Series(_VSCODE_evalResult) + _VSCODE_evalResult = _VSCODE_pd.Series.to_frame(_VSCODE_evalResult) + _VSCODE_columnTypes = list(_VSCODE_evalResult.dtypes) + _VSCODE_columnNames = list(_VSCODE_evalResult) +elif _VSCODE_targetVariable['type'] == 'ndarray': + _VSCODE_evalResult = _VSCODE_pd.Series(_VSCODE_evalResult) + _VSCODE_evalResult = _VSCODE_pd.Series.to_frame(_VSCODE_evalResult) + _VSCODE_columnTypes = list(_VSCODE_evalResult.dtypes) + _VSCODE_columnNames = list(_VSCODE_evalResult) +elif _VSCODE_targetVariable['type'] == 'DataFrame': + _VSCODE_columnTypes = list(_VSCODE_evalResult.dtypes) + _VSCODE_columnNames = list(_VSCODE_evalResult) # Make sure we have an index column (see code in getJupyterVariableDataFrameRows.py) if 'index' not in _VSCODE_columnNames: @@ -23,13 +38,13 @@ # Then loop and generate our output json _VSCODE_columns = [] -for n in range(0, len(_VSCODE_columnNames)): - c = _VSCODE_columnNames[n] - t = _VSCODE_columnTypes[n] +for _VSCODE_n in range(0, len(_VSCODE_columnNames)): + _VSCODE_column_name = _VSCODE_columnNames[_VSCODE_n] + _VSCODE_column_type = _VSCODE_columnTypes[_VSCODE_n] _VSCODE_colobj = {} - _VSCODE_colobj['key'] = c - _VSCODE_colobj['name'] = c - _VSCODE_colobj['type'] = str(t) + _VSCODE_colobj['key'] = _VSCODE_column_name + _VSCODE_colobj['name'] = _VSCODE_column_name + _VSCODE_colobj['type'] = str(_VSCODE_column_type) _VSCODE_columns.append(_VSCODE_colobj) del _VSCODE_columnNames diff --git a/pythonFiles/datascience/getJupyterVariableDataFrameRows.py b/pythonFiles/datascience/getJupyterVariableDataFrameRows.py index fdabb62c1518..74a7b5cb99fc 100644 --- a/pythonFiles/datascience/getJupyterVariableDataFrameRows.py +++ b/pythonFiles/datascience/getJupyterVariableDataFrameRows.py @@ -17,6 +17,14 @@ _VSCODE_df = _VSCODE_evalResult if (_VSCODE_targetVariable['type'] == 'list'): _VSCODE_df = _VSCODE_pd.DataFrame({'_VSCode_JupyterValuesColumn':_VSCODE_evalResult}) +elif (_VSCODE_targetVariable['type'] == 'Series'): + _VSCODE_df = _VSCODE_pd.Series.to_frame(_VSCODE_evalResult) +elif _VSCODE_targetVariable['type'] == 'dict': + _VSCODE_evalResult = _VSCODE_pd.Series(_VSCODE_evalResult) + _VSCODE_df = _VSCODE_pd.Series.to_frame(_VSCODE_evalResult) +elif _VSCODE_targetVariable['type'] == 'ndarray': + _VSCODE_evalResult = _VSCODE_pd.Series(_VSCODE_evalResult) + _VSCODE_df = _VSCODE_pd.Series.to_frame(_VSCODE_evalResult) # If not a known type, then just let pandas handle it. elif not (hasattr(_VSCODE_df, 'iloc')): _VSCODE_df = _VSCODE_pd.DataFrame(_VSCODE_evalResult) @@ -24,6 +32,7 @@ # Turn into JSON using pandas. We use pandas because it's about 3 orders of magnitude faster to turn into JSON _VSCODE_rows = _VSCODE_df.iloc[_VSCODE_startRow:_VSCODE_endRow] _VSCODE_result = _VSCODE_pd_json.to_json(None, _VSCODE_rows, orient='table', date_format='iso') +print(_VSCODE_result) # Cleanup our variables del _VSCODE_df diff --git a/src/client/datascience/data-viewing/dataExplorer.ts b/src/client/datascience/data-viewing/dataExplorer.ts index f0f008dfb583..a0c6cecbde87 100644 --- a/src/client/datascience/data-viewing/dataExplorer.ts +++ b/src/client/datascience/data-viewing/dataExplorer.ts @@ -7,8 +7,9 @@ import { inject, injectable } from 'inversify'; import * as path from 'path'; import { ViewColumn } from 'vscode'; -import { IWebPanel, IWebPanelProvider, IWorkspaceService } from '../../common/application/types'; +import { IApplicationShell, IWebPanel, IWebPanelProvider, IWorkspaceService } from '../../common/application/types'; import { EXTENSION_ROOT_DIR } from '../../common/constants'; +import { traceError } from '../../common/logger'; import { IAsyncDisposable, IConfigurationService, IDisposable, ILogger } from '../../common/types'; import { createDeferred, Deferred } from '../../common/utils/async'; import * as localize from '../../common/utils/localize'; @@ -36,7 +37,8 @@ export class DataExplorer implements IDataExplorer, IAsyncDisposable { @inject(ICodeCssGenerator) private cssGenerator: ICodeCssGenerator, @inject(IWorkspaceService) private workspaceService: IWorkspaceService, @inject(IJupyterVariables) private variableManager: IJupyterVariables, - @inject(ILogger) private logger: ILogger + @inject(ILogger) private logger: ILogger, + @inject(IApplicationShell) private applicationShell: IApplicationShell ) { this.changeHandler = this.configuration.getSettings().onDidChange(this.onSettingsChanged.bind(this)); @@ -162,16 +164,26 @@ export class DataExplorer implements IDataExplorer, IAsyncDisposable { } private async getAllRows() { - if (this.variable && this.variable.rowCount) { - const allRows = await this.variableManager.getDataFrameRows(this.variable, 0, this.variable.rowCount); - return this.postMessage(DataExplorerMessages.GetAllRowsResponse, allRows); + try { + if (this.variable && this.variable.rowCount) { + const allRows = await this.variableManager.getDataFrameRows(this.variable, 0, this.variable.rowCount); + return this.postMessage(DataExplorerMessages.GetAllRowsResponse, allRows); + } + } catch (e) { + traceError(e); + this.applicationShell.showErrorMessage(e); } } private async getRowChunk(request: IGetRowsRequest) { - if (this.variable && this.variable.rowCount) { - const rows = await this.variableManager.getDataFrameRows(this.variable, request.start, Math.min(request.end, this.variable.rowCount)); - return this.postMessage(DataExplorerMessages.GetRowsResponse, { rows, start: request.start, end: request.end}); + try { + if (this.variable && this.variable.rowCount) { + const rows = await this.variableManager.getDataFrameRows(this.variable, request.start, Math.min(request.end, this.variable.rowCount)); + return this.postMessage(DataExplorerMessages.GetRowsResponse, { rows, start: request.start, end: request.end }); + } + } catch (e) { + traceError(e); + this.applicationShell.showErrorMessage(e); } } diff --git a/src/client/datascience/jupyter/jupyterVariables.ts b/src/client/datascience/jupyter/jupyterVariables.ts index 5ccafd397380..8f3a74e4d54f 100644 --- a/src/client/datascience/jupyter/jupyterVariables.ts +++ b/src/client/datascience/jupyter/jupyterVariables.ts @@ -14,7 +14,6 @@ import * as localize from '../../common/utils/localize'; import { EXTENSION_ROOT_DIR } from '../../constants'; import { Identifiers } from '../constants'; import { ICell, IHistoryProvider, IJupyterExecution, IJupyterVariable, IJupyterVariables } from '../types'; -import { replace } from 'event-stream'; @injectable() export class JupyterVariables implements IJupyterVariables { @@ -125,7 +124,6 @@ export class JupyterVariables implements IJupyterVariables { return match; }); - // Execute this on the jupyter server. const results = await activeServer.execute(scriptText, Identifiers.EmptyFileName, 0, uuid(), undefined, true); diff --git a/src/datascience-ui/data-explorer/mainPanel.tsx b/src/datascience-ui/data-explorer/mainPanel.tsx index 1dbe6bb2b875..928c058b33a3 100644 --- a/src/datascience-ui/data-explorer/mainPanel.tsx +++ b/src/datascience-ui/data-explorer/mainPanel.tsx @@ -268,9 +268,10 @@ export class MainPanel extends React.Component if (variable.columns) { return variable.columns.map((c: {key: string; type: string}, i: number) => { return { - ...c, + type: c.type, + key: c.key.toString(), index: i, - name: c.key, + name: c.key.toString(), ...defaultColumnProperties, formatter: CellFormatter, getRowMetaData: this.getRowMetaData.bind(this) From c1e133a37ee7209df982438fb7185c7964516d3a Mon Sep 17 00:00:00 2001 From: rchiodo Date: Fri, 29 Mar 2019 13:48:00 -0700 Subject: [PATCH 03/10] Rename Data explorer to data viewer Beginning of functional test --- package.nls.json | 2 +- src/client/common/utils/localize.ts | 2 +- src/client/datascience/constants.ts | 2 +- .../{dataExplorer.ts => dataViewer.ts} | 30 ++-- ...stener.ts => dataViewerMessageListener.ts} | 2 +- ...lorerProvider.ts => dataViewerProvider.ts} | 10 +- src/client/datascience/data-viewing/types.ts | 20 +-- src/client/datascience/history/history.ts | 10 +- .../datascience/history/historyTypes.ts | 4 +- src/client/datascience/serviceRegistry.ts | 12 +- src/client/datascience/types.ts | 10 +- src/client/telemetry/index.ts | 2 +- .../data-explorer/cellFormatter.tsx | 4 +- .../data-explorer/emptyRowsView.tsx | 2 +- .../data-explorer/mainPanel.tsx | 30 ++-- .../history-react/MainPanel.tsx | 6 +- .../datascience/dataScienceIocContainer.ts | 137 +++++++++++++- .../dataviewer.functional.test.tsx | 118 ++++++++++++ .../datascience/history.functional.test.tsx | 112 +++--------- .../datascience/liveshare.functional.test.tsx | 168 +++--------------- 20 files changed, 377 insertions(+), 306 deletions(-) rename src/client/datascience/data-viewing/{dataExplorer.ts => dataViewer.ts} (82%) rename src/client/datascience/data-viewing/{dataExplorerMessageListener.ts => dataViewerMessageListener.ts} (91%) rename src/client/datascience/data-viewing/{dataExplorerProvider.ts => dataViewerProvider.ts} (87%) create mode 100644 src/test/datascience/dataviewer.functional.test.tsx diff --git a/package.nls.json b/package.nls.json index 4158c89e7059..5155fa58bcac 100644 --- a/package.nls.json +++ b/package.nls.json @@ -226,7 +226,7 @@ "DataScience.dataExplorerInvalidVariableFormat" : "'{0}' is not an active variable.", "DataScience.jupyterGetVariablesExecutionError" : "Failure during variable extraction:\r\n{0}", "DataScience.loadingMessage" : "loading ...", - "DataScience.noRowsInDataExplorer" : "Fetching data ...", + "DataScience.noRowsInDataViewer" : "Fetching data ...", "DataScience.pandasTooOldForViewingFormat" : "Python package 'pandas' is version {0}. Version 0.20 or greater is required for viewing data.", "DataScience.pandasRequiredForViewing" : "Python package 'pandas' is required for viewing data." } diff --git a/src/client/common/utils/localize.ts b/src/client/common/utils/localize.ts index b9176b063789..ca3b28f95123 100644 --- a/src/client/common/utils/localize.ts +++ b/src/client/common/utils/localize.ts @@ -147,7 +147,7 @@ export namespace DataScience { export const pythonInteractiveCreateFailed = localize('DataScience.pythonInteractiveCreateFailed', 'Failure to create a \'Python Interactive\' window. Try reinstalling the Python extension.'); export const jupyterGetVariablesExecutionError = localize('DataScience.jupyterGetVariablesExecutionError', 'Failure during variable extraction: \r\n{0}'); export const loadingMessage = localize('DataScience.loadingMessage', 'loading ...'); - export const noRowsInDataExplorer = localize('DataScience.noRowsInDataExplorer', 'Fetching data ...'); + export const noRowsInDataViewer = localize('DataScience.noRowsInDataViewer', 'Fetching data ...'); export const pandasTooOldForViewingFormat = localize('DataScience.pandasTooOldForViewingFormat', 'Python package \'pandas\' is version {0}. Version 0.20 or greater is required for viewing data.'); export const pandasRequiredForViewing = localize('DataScience.pandasRequiredForViewing', 'Python package \'pandas\' is required for viewing data.'); export const valuesColumn = localize('DataScience.valuesColumn', 'values'); diff --git a/src/client/datascience/constants.ts b/src/client/datascience/constants.ts index 0f15ad37a66e..d128e70ca7f9 100644 --- a/src/client/datascience/constants.ts +++ b/src/client/datascience/constants.ts @@ -92,7 +92,7 @@ export enum Telemetry { RemoteAddCode = 'DATASCIENCE.LIVESHARE.ADDCODE', ShiftEnterBannerShown = 'DATASCIENCE.SHIFTENTER_BANNER_SHOWN', EnableInteractiveShiftEnter = 'DATASCIENCE.ENABLE_INTERACTIVE_SHIFT_ENTER', - ShowDataExplorer = 'DATASCIENCE.SHOW_DATA_EXPLORER', + ShowDataViewer = 'DATASCIENCE.SHOW_DATA_EXPLORER', RunFileInteractive = 'DATASCIENCE.RUN_FILE_INTERACTIVE', PandasNotInstalled = 'DATASCIENCE.SHOW_DATA_NO_PANDAS', PandasTooOld = 'DATASCIENCE.SHOW_DATA_PANDAS_TOO_OLD' diff --git a/src/client/datascience/data-viewing/dataExplorer.ts b/src/client/datascience/data-viewing/dataViewer.ts similarity index 82% rename from src/client/datascience/data-viewing/dataExplorer.ts rename to src/client/datascience/data-viewing/dataViewer.ts index a0c6cecbde87..cb0a0c2f582a 100644 --- a/src/client/datascience/data-viewing/dataExplorer.ts +++ b/src/client/datascience/data-viewing/dataViewer.ts @@ -16,17 +16,17 @@ import * as localize from '../../common/utils/localize'; import { noop } from '../../common/utils/misc'; import { sendTelemetryEvent } from '../../telemetry'; import { Telemetry } from '../constants'; -import { ICodeCssGenerator, IDataExplorer, IDataScienceExtraSettings, IJupyterVariable, IJupyterVariables } from '../types'; -import { DataExplorerMessageListener } from './dataExplorerMessageListener'; -import { DataExplorerMessages, IDataExplorerMapping, IGetRowsRequest } from './types'; +import { ICodeCssGenerator, IDataScienceExtraSettings, IDataViewer, IJupyterVariable, IJupyterVariables } from '../types'; +import { DataViewerMessageListener } from './dataViewerMessageListener'; +import { DataViewerMessages, IDataViewerMapping, IGetRowsRequest } from './types'; @injectable() -export class DataExplorer implements IDataExplorer, IAsyncDisposable { +export class DataViewer implements IDataViewer, IAsyncDisposable { private disposed: boolean = false; private webPanel: IWebPanel | undefined; private webPanelInit: Deferred; private loadPromise: Promise; - private messageListener : DataExplorerMessageListener; + private messageListener : DataViewerMessageListener; private changeHandler: IDisposable | undefined; private viewState : { visible: boolean; active: boolean } = { visible: false, active: false }; private variable : IJupyterVariable | undefined; @@ -43,7 +43,7 @@ export class DataExplorer implements IDataExplorer, IAsyncDisposable { this.changeHandler = this.configuration.getSettings().onDidChange(this.onSettingsChanged.bind(this)); // Create a message listener to listen to messages from our webpanel (or remote session) - this.messageListener = new DataExplorerMessageListener(this.onMessage, this.onViewStateChanged, this.dispose); + this.messageListener = new DataViewerMessageListener(this.onMessage, this.onViewStateChanged, this.dispose); // Setup our init promise for the web panel. We use this to make sure we're in sync with our // react control. @@ -71,7 +71,7 @@ export class DataExplorer implements IDataExplorer, IAsyncDisposable { await this.webPanel.show(true); // Send a message with our data - this.postMessage(DataExplorerMessages.InitializeData, this.variable).ignoreErrors(); + this.postMessage(DataViewerMessages.InitializeData, this.variable).ignoreErrors(); } } } @@ -95,7 +95,7 @@ export class DataExplorer implements IDataExplorer, IAsyncDisposable { // Log telemetry about number of rows try { - sendTelemetryEvent(Telemetry.ShowDataExplorer, {rows: output.rowCount ? output.rowCount : 0 }); + sendTelemetryEvent(Telemetry.ShowDataViewer, {rows: output.rowCount ? output.rowCount : 0 }); } catch { noop(); } @@ -103,7 +103,7 @@ export class DataExplorer implements IDataExplorer, IAsyncDisposable { return output; } - private async postMessage(type: T, payload?: M[T]) : Promise { + private async postMessage(type: T, payload?: M[T]) : Promise { if (this.webPanel) { // Make sure the webpanel is up before we send it anything. await this.webPanelInit.promise; @@ -116,15 +116,15 @@ export class DataExplorer implements IDataExplorer, IAsyncDisposable { //tslint:disable-next-line:no-any private onMessage = (message: string, payload: any) => { switch (message) { - case DataExplorerMessages.Started: + case DataViewerMessages.Started: this.webPanelRendered(); break; - case DataExplorerMessages.GetAllRowsRequest: + case DataViewerMessages.GetAllRowsRequest: this.getAllRows().ignoreErrors(); break; - case DataExplorerMessages.GetRowsRequest: + case DataViewerMessages.GetRowsRequest: this.getRowChunk(payload as IGetRowsRequest).ignoreErrors(); break; @@ -149,7 +149,7 @@ export class DataExplorer implements IDataExplorer, IAsyncDisposable { private onSettingsChanged = () => { // Stringify our settings to send over to the panel const dsSettings = JSON.stringify(this.generateDataScienceExtraSettings()); - this.postMessage(DataExplorerMessages.UpdateSettings, dsSettings).ignoreErrors(); + this.postMessage(DataViewerMessages.UpdateSettings, dsSettings).ignoreErrors(); } private generateDataScienceExtraSettings() : IDataScienceExtraSettings { @@ -167,7 +167,7 @@ export class DataExplorer implements IDataExplorer, IAsyncDisposable { try { if (this.variable && this.variable.rowCount) { const allRows = await this.variableManager.getDataFrameRows(this.variable, 0, this.variable.rowCount); - return this.postMessage(DataExplorerMessages.GetAllRowsResponse, allRows); + return this.postMessage(DataViewerMessages.GetAllRowsResponse, allRows); } } catch (e) { traceError(e); @@ -179,7 +179,7 @@ export class DataExplorer implements IDataExplorer, IAsyncDisposable { try { if (this.variable && this.variable.rowCount) { const rows = await this.variableManager.getDataFrameRows(this.variable, request.start, Math.min(request.end, this.variable.rowCount)); - return this.postMessage(DataExplorerMessages.GetRowsResponse, { rows, start: request.start, end: request.end }); + return this.postMessage(DataViewerMessages.GetRowsResponse, { rows, start: request.start, end: request.end }); } } catch (e) { traceError(e); diff --git a/src/client/datascience/data-viewing/dataExplorerMessageListener.ts b/src/client/datascience/data-viewing/dataViewerMessageListener.ts similarity index 91% rename from src/client/datascience/data-viewing/dataExplorerMessageListener.ts rename to src/client/datascience/data-viewing/dataViewerMessageListener.ts index e94790809381..ce21e5a03e89 100644 --- a/src/client/datascience/data-viewing/dataExplorerMessageListener.ts +++ b/src/client/datascience/data-viewing/dataViewerMessageListener.ts @@ -8,7 +8,7 @@ import { IWebPanel, IWebPanelMessageListener } from '../../common/application/ty // tslint:disable:no-any // This class listens to messages that come from the local Data Explorer window -export class DataExplorerMessageListener implements IWebPanelMessageListener { +export class DataViewerMessageListener implements IWebPanelMessageListener { private disposedCallback : () => void; private callback : (message: string, payload: any) => void; private viewChanged: (panel: IWebPanel) => void; diff --git a/src/client/datascience/data-viewing/dataExplorerProvider.ts b/src/client/datascience/data-viewing/dataViewerProvider.ts similarity index 87% rename from src/client/datascience/data-viewing/dataExplorerProvider.ts rename to src/client/datascience/data-viewing/dataViewerProvider.ts index f0566fa83fe8..849f460b050e 100644 --- a/src/client/datascience/data-viewing/dataExplorerProvider.ts +++ b/src/client/datascience/data-viewing/dataViewerProvider.ts @@ -11,12 +11,12 @@ import * as localize from '../../common/utils/localize'; import { noop } from '../../common/utils/misc'; import { IInterpreterService } from '../../interpreter/contracts'; import { IServiceContainer } from '../../ioc/types'; -import { IDataExplorer, IDataExplorerProvider, IJupyterVariables } from '../types'; +import { IDataViewer, IDataViewerProvider, IJupyterVariables } from '../types'; @injectable() -export class DataExplorerProvider implements IDataExplorerProvider, IAsyncDisposable { +export class DataViewerProvider implements IDataViewerProvider, IAsyncDisposable { - private activeExplorers: IDataExplorer[] = []; + private activeExplorers: IDataViewer[] = []; constructor( @inject(IServiceContainer) private serviceContainer: IServiceContainer, @inject(IAsyncDisposableRegistry) asyncRegistry : IAsyncDisposableRegistry, @@ -31,12 +31,12 @@ export class DataExplorerProvider implements IDataExplorerProvider, IAsyncDispos await Promise.all(this.activeExplorers.map(d => d.dispose())); } - public async create(variable: string) : Promise{ + public async create(variable: string) : Promise{ // Make sure this is a valid variable const variables = await this.variables.getVariables(); const index = variables.findIndex(v => v && v.name === variable); if (index >= 0) { - const dataExplorer = this.serviceContainer.get(IDataExplorer); + const dataExplorer = this.serviceContainer.get(IDataViewer); this.activeExplorers.push(dataExplorer); await dataExplorer.show(variables[index]); return dataExplorer; diff --git a/src/client/datascience/data-viewing/types.ts b/src/client/datascience/data-viewing/types.ts index 97049c17406e..06325e0d01ba 100644 --- a/src/client/datascience/data-viewing/types.ts +++ b/src/client/datascience/data-viewing/types.ts @@ -10,12 +10,12 @@ export const RowFetchSizeFirst = 100; export const RowFetchSizeSubsequent = 1000; export const MaxStringCompare = 200; -export namespace DataExplorerRowStates { +export namespace DataViewerRowStates { export const Fetching = 'fetching'; export const Skipped = 'skipped'; } -export namespace DataExplorerMessages { +export namespace DataViewerMessages { export const Started = 'started'; export const UpdateSettings = 'update_settings'; export const InitializeData = 'init'; @@ -37,12 +37,12 @@ export interface IGetRowsResponse { } // Map all messages to specific payloads -export class IDataExplorerMapping { - public [DataExplorerMessages.Started]: never | undefined; - public [DataExplorerMessages.UpdateSettings]: string; - public [DataExplorerMessages.InitializeData]: IJupyterVariable; - public [DataExplorerMessages.GetAllRowsRequest]: never | undefined; - public [DataExplorerMessages.GetAllRowsResponse]: JSONObject; - public [DataExplorerMessages.GetRowsRequest]: IGetRowsRequest; - public [DataExplorerMessages.GetRowsResponse]: IGetRowsResponse; +export class IDataViewerMapping { + public [DataViewerMessages.Started]: never | undefined; + public [DataViewerMessages.UpdateSettings]: string; + public [DataViewerMessages.InitializeData]: IJupyterVariable; + public [DataViewerMessages.GetAllRowsRequest]: never | undefined; + public [DataViewerMessages.GetAllRowsResponse]: JSONObject; + public [DataViewerMessages.GetRowsRequest]: IGetRowsRequest; + public [DataViewerMessages.GetRowsResponse]: IGetRowsResponse; } diff --git a/src/client/datascience/history/history.ts b/src/client/datascience/history/history.ts index 5bde0ec6fd68..95594945c628 100644 --- a/src/client/datascience/history/history.ts +++ b/src/client/datascience/history/history.ts @@ -36,8 +36,8 @@ import { ICell, ICodeCssGenerator, IConnection, - IDataExplorerProvider, IDataScienceExtraSettings, + IDataViewerProvider, IHistory, IHistoryInfo, IHistoryProvider, @@ -96,7 +96,7 @@ export class History implements IHistory { @inject(INotebookExporter) private jupyterExporter: INotebookExporter, @inject(IWorkspaceService) private workspaceService: IWorkspaceService, @inject(IHistoryProvider) private historyProvider: IHistoryProvider, - @inject(IDataExplorerProvider) private dataExplorerProvider: IDataExplorerProvider, + @inject(IDataViewerProvider) private dataExplorerProvider: IDataViewerProvider, @inject(IJupyterVariables) private jupyterVariables: IJupyterVariables ) { @@ -240,8 +240,8 @@ export class History implements IHistory { this.dispatchMessage(message, payload, this.onRemoteAddedCode); break; - case HistoryMessages.ShowDataExplorer: - this.dispatchMessage(message, payload, this.showDataExplorer); + case HistoryMessages.ShowDataViewer: + this.dispatchMessage(message, payload, this.showDataViewer); break; case HistoryMessages.GetVariablesRequest: @@ -367,7 +367,7 @@ export class History implements IHistory { } } - private async showDataExplorer(variable: string) : Promise { + private async showDataViewer(variable: string) : Promise { try { const pandasVersion = await this.dataExplorerProvider.getPandasVersion(); if (!pandasVersion) { diff --git a/src/client/datascience/history/historyTypes.ts b/src/client/datascience/history/historyTypes.ts index 9aba9da64a55..dd9a031db7db 100644 --- a/src/client/datascience/history/historyTypes.ts +++ b/src/client/datascience/history/historyTypes.ts @@ -29,7 +29,7 @@ export namespace HistoryMessages { export const AddedSysInfo = 'added_sys_info'; export const RemoteAddCode = 'remote_add_code'; export const Activate = 'activate'; - export const ShowDataExplorer = 'show_data_explorer'; + export const ShowDataViewer = 'show_data_explorer'; export const GetVariablesRequest = 'get_variables_request'; export const GetVariablesResponse = 'get_variables_response'; export const GetVariableValueRequest = 'get_variable_value_request'; @@ -95,7 +95,7 @@ export class IHistoryMapping { public [HistoryMessages.AddedSysInfo]: IAddedSysInfo; public [HistoryMessages.RemoteAddCode]: IRemoteAddCode; public [HistoryMessages.Activate] : never | undefined; - public [HistoryMessages.ShowDataExplorer]: string; + public [HistoryMessages.ShowDataViewer]: string; public [HistoryMessages.GetVariablesRequest]: never | undefined; public [HistoryMessages.GetVariablesResponse]: IJupyterVariable[]; public [HistoryMessages.GetVariableValueRequest]: IJupyterVariable; diff --git a/src/client/datascience/serviceRegistry.ts b/src/client/datascience/serviceRegistry.ts index 0e87cd6fd449..e6c589a261f0 100644 --- a/src/client/datascience/serviceRegistry.ts +++ b/src/client/datascience/serviceRegistry.ts @@ -3,8 +3,8 @@ 'use strict'; import { IServiceManager } from '../ioc/types'; import { CodeCssGenerator } from './codeCssGenerator'; -import { DataExplorer } from './data-viewing/dataExplorer'; -import { DataExplorerProvider } from './data-viewing/dataExplorerProvider'; +import { DataViewer } from './data-viewing/dataViewer'; +import { DataViewerProvider } from './data-viewing/dataViewerProvider'; import { DataScience } from './datascience'; import { DataScienceCodeLensProvider } from './editor-integration/codelensprovider'; import { CodeWatcher } from './editor-integration/codewatcher'; @@ -23,11 +23,11 @@ import { ThemeFinder } from './themeFinder'; import { ICodeCssGenerator, ICodeWatcher, - IDataExplorer, - IDataExplorerProvider, IDataScience, IDataScienceCodeLensProvider, IDataScienceCommandListener, + IDataViewer, + IDataViewerProvider, IHistory, IHistoryProvider, IJupyterCommandFactory, @@ -58,6 +58,6 @@ export function registerTypes(serviceManager: IServiceManager) { serviceManager.add(ICodeWatcher, CodeWatcher); serviceManager.add(IJupyterCommandFactory, JupyterCommandFactory); serviceManager.addSingleton(IThemeFinder, ThemeFinder); - serviceManager.addSingleton(IDataExplorerProvider, DataExplorerProvider); - serviceManager.add(IDataExplorer, DataExplorer); + serviceManager.addSingleton(IDataViewerProvider, DataViewerProvider); + serviceManager.add(IDataViewer, DataViewer); } diff --git a/src/client/datascience/types.ts b/src/client/datascience/types.ts index 80974bbce86e..b9441bb89939 100644 --- a/src/client/datascience/types.ts +++ b/src/client/datascience/types.ts @@ -278,13 +278,13 @@ export interface IJupyterVariables { getDataFrameRows(targetVariable: IJupyterVariable, start: number, end: number) : Promise; } -export const IDataExplorerProvider = Symbol('IDataExplorerProvider'); -export interface IDataExplorerProvider { - create(variable: string) : Promise; +export const IDataViewerProvider = Symbol('IDataViewerProvider'); +export interface IDataViewerProvider { + create(variable: string) : Promise; getPandasVersion() : Promise<{major: number; minor: number; build: number} | undefined>; } -export const IDataExplorer = Symbol('IDataExplorer'); +export const IDataViewer = Symbol('IDataViewer'); -export interface IDataExplorer extends IAsyncDisposable { +export interface IDataViewer extends IAsyncDisposable { show(variable: IJupyterVariable) : Promise; } diff --git a/src/client/telemetry/index.ts b/src/client/telemetry/index.ts index 98c78af9bc7f..98124e0ea89e 100644 --- a/src/client/telemetry/index.ts +++ b/src/client/telemetry/index.ts @@ -348,7 +348,7 @@ export interface IEventNamePropertyMapping { [Telemetry.SetJupyterURIToLocal]: never | undefined; [Telemetry.SetJupyterURIToUserSpecified]: never | undefined; [Telemetry.ShiftEnterBannerShown]: never | undefined; - [Telemetry.ShowDataExplorer]: {rows: number | undefined}; + [Telemetry.ShowDataViewer]: {rows: number | undefined}; [Telemetry.ShowHistoryPane]: never | undefined; [Telemetry.StartJupyter]: never | undefined; [Telemetry.SubmitCellThroughInput]: never | undefined; diff --git a/src/datascience-ui/data-explorer/cellFormatter.tsx b/src/datascience-ui/data-explorer/cellFormatter.tsx index 6670d3e9748c..de935dd0e32c 100644 --- a/src/datascience-ui/data-explorer/cellFormatter.tsx +++ b/src/datascience-ui/data-explorer/cellFormatter.tsx @@ -5,7 +5,7 @@ import './cellFormatter.css'; import { JSONObject } from '@phosphor/coreutils'; import * as React from 'react'; -import { DataExplorerRowStates } from '../../client/datascience/data-viewing/types'; +import { DataViewerRowStates } from '../../client/datascience/data-viewing/types'; import { getLocString } from '../react-common/locReactSide'; interface ICellFormatterProps { @@ -23,7 +23,7 @@ export class CellFormatter extends React.Component { public render() { // If this is our special not set value, render a 'loading ...' value. - if (this.props.row === DataExplorerRowStates.Skipped || this.props.row === DataExplorerRowStates.Fetching) { + if (this.props.row === DataViewerRowStates.Skipped || this.props.row === DataViewerRowStates.Fetching) { return ({this.loadingMessage}); } diff --git a/src/datascience-ui/data-explorer/emptyRowsView.tsx b/src/datascience-ui/data-explorer/emptyRowsView.tsx index 93ecfa88d11e..10179d24090e 100644 --- a/src/datascience-ui/data-explorer/emptyRowsView.tsx +++ b/src/datascience-ui/data-explorer/emptyRowsView.tsx @@ -17,7 +17,7 @@ export const EmptyRowsView = (props: IEmptyRowsProps) => { const style: React.CSSProperties = { width: percentText }; - const message = getLocString('DataScience.noRowsInDataExplorer', 'Fetching data ...'); + const message = getLocString('DataScience.noRowsInDataViewer', 'Fetching data ...'); return (
diff --git a/src/datascience-ui/data-explorer/mainPanel.tsx b/src/datascience-ui/data-explorer/mainPanel.tsx index 928c058b33a3..d6b8def1e5e3 100644 --- a/src/datascience-ui/data-explorer/mainPanel.tsx +++ b/src/datascience-ui/data-explorer/mainPanel.tsx @@ -8,9 +8,9 @@ import * as AdazzleReactDataGrid from 'react-data-grid'; import { Data, Toolbar } from 'react-data-grid-addons'; import { - DataExplorerMessages, - DataExplorerRowStates, - IDataExplorerMapping, + DataViewerMessages, + DataViewerRowStates, + IDataViewerMapping, IGetRowsResponse, MaxStringCompare, RowFetchAllLimit, @@ -54,10 +54,10 @@ interface IMainPanelState { sortColumn: string | number; } -class DataExplorerPostOffice extends PostOffice { } +class DataViewerPostOffice extends PostOffice { } export class MainPanel extends React.Component implements IMessageHandler { - private postOffice: DataExplorerPostOffice | undefined; + private postOffice: DataViewerPostOffice | undefined; private container: HTMLDivElement | null = null; private emptyRows: (() => JSX.Element) | undefined; private getEmptyRows: ((props: any) => JSX.Element) | undefined; @@ -116,7 +116,7 @@ export class MainPanel extends React.Component return (
- + {this.container && this.renderGrid()}
@@ -126,15 +126,15 @@ export class MainPanel extends React.Component // tslint:disable-next-line:no-any public handleMessage = (msg: string, payload?: any) => { switch (msg) { - case DataExplorerMessages.InitializeData: + case DataViewerMessages.InitializeData: this.initializeData(payload); break; - case DataExplorerMessages.GetAllRowsResponse: + case DataViewerMessages.GetAllRowsResponse: this.handleGetAllRowsResponse(payload as JSONObject); break; - case DataExplorerMessages.GetRowsResponse: + case DataViewerMessages.GetRowsResponse: this.handleGetRowChunkResponse(payload as IGetRowsResponse); break; @@ -197,7 +197,7 @@ export class MainPanel extends React.Component } private getAllRows() { - this.sendMessage(DataExplorerMessages.GetAllRowsRequest); + this.sendMessage(DataViewerMessages.GetAllRowsRequest); } private getRowsInChunks(startIndex: number, endIndex: number) { @@ -205,7 +205,7 @@ export class MainPanel extends React.Component let chunkEnd = startIndex + Math.min(RowFetchSizeFirst, endIndex); let chunkStart = startIndex; while (chunkStart < endIndex) { - this.sendMessage(DataExplorerMessages.GetRowsRequest, {start: chunkStart, end: chunkEnd}); + this.sendMessage(DataViewerMessages.GetRowsRequest, {start: chunkStart, end: chunkEnd}); chunkStart = chunkEnd; chunkEnd = Math.min(chunkEnd + RowFetchSizeSubsequent, endIndex); } @@ -258,7 +258,7 @@ export class MainPanel extends React.Component private padRows(initialRows: any[], wantedCount: number) : any[] { if (wantedCount > initialRows.length) { - const fetching : string[] = Array(wantedCount - initialRows.length).fill(DataExplorerRowStates.Fetching); + const fetching : string[] = Array(wantedCount - initialRows.length).fill(DataViewerRowStates.Fetching); return [...initialRows, ...fetching]; } return initialRows; @@ -315,14 +315,14 @@ export class MainPanel extends React.Component return (this.state.fetchedRowCount === this.state.actualRowCount); } - private updatePostOffice = (postOffice: DataExplorerPostOffice) => { + private updatePostOffice = (postOffice: DataViewerPostOffice) => { if (this.postOffice !== postOffice) { this.postOffice = postOffice; - this.sendMessage(DataExplorerMessages.Started); + this.sendMessage(DataViewerMessages.Started); } } - private sendMessage(type: T, payload?: M[T]) { + private sendMessage(type: T, payload?: M[T]) { if (this.postOffice) { this.postOffice.sendMessage(type, payload); } diff --git a/src/datascience-ui/history-react/MainPanel.tsx b/src/datascience-ui/history-react/MainPanel.tsx index 86aeb33004ac..031fac491ad1 100644 --- a/src/datascience-ui/history-react/MainPanel.tsx +++ b/src/datascience-ui/history-react/MainPanel.tsx @@ -230,7 +230,7 @@ export class MainPanel extends React.Component private renderDataFrameTestButton() { if (getSettings && getSettings().showJupyterVariableExplorer) { return ( - + D ); @@ -271,8 +271,8 @@ export class MainPanel extends React.Component } } - private showDataExplorer = () => { - this.sendMessage(HistoryMessages.ShowDataExplorer, 'df'); + private showDataViewer = () => { + this.sendMessage(HistoryMessages.ShowDataViewer, 'df'); } private sendMessage(type: T, payload?: M[T]) { diff --git a/src/test/datascience/dataScienceIocContainer.ts b/src/test/datascience/dataScienceIocContainer.ts index 4a9d55d87235..3045bafecf70 100644 --- a/src/test/datascience/dataScienceIocContainer.ts +++ b/src/test/datascience/dataScienceIocContainer.ts @@ -1,10 +1,12 @@ // Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT License. 'use strict'; -//tslint:disable:trailing-comma +//tslint:disable:trailing-comma no-any import * as child_process from 'child_process'; +import { ReactWrapper } from 'enzyme'; import { interfaces } from 'inversify'; import * as path from 'path'; +import { SemVer } from 'semver'; import * as TypeMoq from 'typemoq'; import { ConfigurationChangeEvent, @@ -13,9 +15,11 @@ import { EventEmitter, FileSystemWatcher, Uri, + ViewColumn, WorkspaceConfiguration, WorkspaceFolder } from 'vscode'; +import * as vsls from 'vsls/vscode'; import { TerminalManager } from '../../client/common/application/terminalManager'; import { @@ -23,8 +27,13 @@ import { ICommandManager, IDocumentManager, ILiveShareApi, + ILiveShareTestingApi, ITerminalManager, - IWorkspaceService + IWebPanel, + IWebPanelMessageListener, + IWebPanelProvider, + IWorkspaceService, + WebPanelMessage } from '../../client/common/application/types'; import { AsyncDisposableRegistry } from '../../client/common/asyncDisposableRegistry'; import { PythonSettings } from '../../client/common/configSettings'; @@ -67,13 +76,15 @@ import { IPersistentStateFactory, IsWindows } from '../../client/common/types'; +import { Deferred } from '../../client/common/utils/async'; import { noop } from '../../client/common/utils/misc'; +import { Architecture } from '../../client/common/utils/platform'; import { EnvironmentVariablesService } from '../../client/common/variables/environment'; import { EnvironmentVariablesProvider } from '../../client/common/variables/environmentVariablesProvider'; import { IEnvironmentVariablesProvider, IEnvironmentVariablesService } from '../../client/common/variables/types'; import { CodeCssGenerator } from '../../client/datascience/codeCssGenerator'; -import { DataExplorer } from '../../client/datascience/data-viewing/dataExplorer'; -import { DataExplorerProvider } from '../../client/datascience/data-viewing/dataExplorerProvider'; +import { DataViewer } from '../../client/datascience/data-viewing/dataViewer'; +import { DataViewerProvider } from '../../client/datascience/data-viewing/dataViewerProvider'; import { CodeWatcher } from '../../client/datascience/editor-integration/codewatcher'; import { History } from '../../client/datascience/history/history'; import { HistoryCommandListener } from '../../client/datascience/history/historycommandlistener'; @@ -90,10 +101,10 @@ import { ThemeFinder } from '../../client/datascience/themeFinder'; import { ICodeCssGenerator, ICodeWatcher, - IDataExplorer, - IDataExplorerProvider, IDataScience, IDataScienceCommandListener, + IDataViewer, + IDataViewerProvider, IHistory, IHistoryProvider, IJupyterCommandFactory, @@ -132,10 +143,12 @@ import { IInterpreterWatcherBuilder, IKnownSearchPathsForInterpreters, INTERPRETER_LOCATOR_SERVICE, + InterpreterType, IPipEnvService, IVirtualEnvironmentsSearchPathProvider, KNOWN_PATH_SERVICE, PIPENV_SERVICE, + PythonInterpreter, WINDOWS_REGISTRY_SERVICE, WORKSPACE_VIRTUAL_ENV_SERVICE } from '../../client/interpreter/contracts'; @@ -172,16 +185,22 @@ import { import { IPipEnvServiceHelper, IPythonInPathCommandProvider } from '../../client/interpreter/locators/types'; import { VirtualEnvironmentManager } from '../../client/interpreter/virtualEnvs'; import { IVirtualEnvironmentManager } from '../../client/interpreter/virtualEnvs/types'; +import { IVsCodeApi } from '../../datascience-ui/react-common/postOffice'; import { MockAutoSelectionService } from '../mocks/autoSelector'; import { UnitTestIocContainer } from '../unittests/serviceRegistry'; import { MockCommandManager } from './mockCommandManager'; import { MockDocumentManager } from './mockDocumentManager'; import { MockExtensions } from './mockExtensions'; -import { MockJupyterManager } from './mockJupyterManager'; +import { MockJupyterManager, SupportedCommands } from './mockJupyterManager'; import { MockLiveShareApi } from './mockLiveShare'; +import { blurWindow, createMessageEvent } from './reactHelpers'; export class DataScienceIocContainer extends UnitTestIocContainer { + public webPanelListener: IWebPanelMessageListener | undefined; + public wrapper: ReactWrapper, React.Component> | undefined; + public wrapperCreatedPromise: Deferred | undefined; + public postMessage: ((ev: MessageEvent) => void) | undefined; private pythonSettings = new class extends PythonSettings { public fireChangeEvent() { this.changed.fire(); @@ -195,6 +214,14 @@ export class DataScienceIocContainer extends UnitTestIocContainer { private asyncRegistry: AsyncDisposableRegistry; private configChangeEvent = new EventEmitter(); private documentManager = new MockDocumentManager(); + private workingPython: PythonInterpreter = { + path: '/foo/bar/python.exe', + version: new SemVer('3.6.6-final'), + sysVersion: '1.0.0.0', + sysPrefix: 'Python', + type: InterpreterType.Unknown, + architecture: Architecture.x64, + }; constructor() { super(); @@ -210,6 +237,13 @@ export class DataScienceIocContainer extends UnitTestIocContainer { public async dispose(): Promise { await this.asyncRegistry.dispose(); await super.dispose(); + + if (this.wrapper) { + // Blur window focus so we don't have editors polling + blurWindow(); + this.wrapper.unmount(); + this.wrapper = undefined; + } } //tslint:disable:max-func-body-length @@ -217,10 +251,10 @@ export class DataScienceIocContainer extends UnitTestIocContainer { this.registerFileSystemTypes(); this.serviceManager.addSingleton(IJupyterExecution, JupyterExecutionFactory); this.serviceManager.addSingleton(IHistoryProvider, HistoryProvider); - this.serviceManager.addSingleton(IDataExplorerProvider, DataExplorerProvider); + this.serviceManager.addSingleton(IDataViewerProvider, DataViewerProvider); this.serviceManager.addSingleton(ILogger, Logger); this.serviceManager.add(IHistory, History); - this.serviceManager.add(IDataExplorer, DataExplorer); + this.serviceManager.add(IDataViewer, DataViewer); this.serviceManager.add(INotebookImporter, JupyterImporter); this.serviceManager.add(INotebookExporter, JupyterExporter); this.serviceManager.addSingleton(ILiveShareApi, MockLiveShareApi); @@ -425,6 +459,52 @@ export class DataScienceIocContainer extends UnitTestIocContainer { interpreterManager.initialize(); } + // tslint:disable:any + public createWebView(mount: () => ReactWrapper, React.Component>, role: vsls.Role = vsls.Role.None) { + + if (this.mockJupyter) { + this.mockJupyter.addInterpreter(this.workingPython, SupportedCommands.all); + } + + // Force the container to mock actual live share if necessary + if (role !== vsls.Role.None) { + const liveShareTest = this.get(ILiveShareApi) as ILiveShareTestingApi; + liveShareTest.forceRole(role); + } + + const webPanelProvider = TypeMoq.Mock.ofType(); + const webPanel = TypeMoq.Mock.ofType(); + + this.serviceManager.addSingletonInstance(IWebPanelProvider, webPanelProvider.object); + + // 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; + + // 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) { + this.postMessage(message); + + if (this.wrapperCreatedPromise && !this.wrapperCreatedPromise.resolved) { + this.wrapperCreatedPromise.resolve(); + } + + } else { + throw new Error('postMessage callback not defined'); + } + }); + webPanel.setup(p => p.show(true)); + + // We need to mount the react control before we even create a history object. Otherwise the mount will miss rendering some parts + this.mountReactControl(mount); + } + public createMoqWorkspaceFolder(folderPath: string) { const folder = TypeMoq.Mock.ofType(); folder.setup(f => f.uri).returns(() => Uri.file(folderPath)); @@ -473,4 +553,43 @@ export class DataScienceIocContainer extends UnitTestIocContainer { return 'python'; } } + + private mountReactControl(mount: () => ReactWrapper, React.Component>) { + // Setup the acquireVsCodeApi. The react control will cache this value when it's mounted. + const globalAcquireVsCodeApi = (): IVsCodeApi => { + return { + // tslint:disable-next-line:no-any + postMessage: (msg: any) => { + if (this.webPanelListener) { + this.webPanelListener.onMessage(msg.type, msg.payload); + } + }, + // tslint:disable-next-line:no-any no-empty + setState: (_msg: any) => { + + }, + // tslint:disable-next-line:no-any no-empty + getState: () => { + return {}; + } + }; + }; + // tslint:disable-next-line:no-string-literal + (global as any)['acquireVsCodeApi'] = globalAcquireVsCodeApi; + + // Remap event handlers to point to the container. + const oldListener = window.addEventListener; + window.addEventListener = (event: string, cb: any) => { + if (event === 'message') { + this.postMessage = cb; + } + }; + + // Mount our main panel. This will make the global api be cached and have the event handler registered + this.wrapper = mount(); + + // We can remove the global api and event listener now. + delete (global as any).acquireVsCodeApi; + window.addEventListener = oldListener; + } } diff --git a/src/test/datascience/dataviewer.functional.test.tsx b/src/test/datascience/dataviewer.functional.test.tsx new file mode 100644 index 000000000000..f7bec818969f --- /dev/null +++ b/src/test/datascience/dataviewer.functional.test.tsx @@ -0,0 +1,118 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +'use strict'; +// tslint:disable:max-func-body-length trailing-comma no-any no-multiline-string +import * as assert from 'assert'; +import { mount, ReactWrapper } from 'enzyme'; +import * as React from 'react'; +import * as uuid from 'uuid/v4'; +import { Disposable } from 'vscode'; + +import { Identifiers } from '../../client/datascience/constants'; +import { DataViewerMessageListener } from '../../client/datascience/data-viewing/dataViewerMessageListener'; +import { DataViewerMessages } from '../../client/datascience/data-viewing/types'; +import { IDataViewer, IDataViewerProvider, IJupyterExecution } from '../../client/datascience/types'; +import { MainPanel } from '../../datascience-ui/data-explorer/mainPanel'; +import { DataScienceIocContainer } from './dataScienceIocContainer'; +import { CellPosition, verifyHtmlOnCell } from './historyTestHelpers'; +import { blurWindow } from './reactHelpers'; + +suite('DataViewer tests', () => { + const disposables: Disposable[] = []; + let dataProvider: IDataViewerProvider; + let ioc: DataScienceIocContainer; + + suiteSetup(function () { + // DataViewer tests require jupyter to run. Othewrise can't + // run any of our variable execution code + const isRollingBuild = process.env ? process.env.VSCODE_PYTHON_ROLLING !== undefined : false; + if (!isRollingBuild) { + // tslint:disable-next-line:no-console + console.log('Skipping DataViewer tests. Requires python environment'); + // tslint:disable-next-line:no-invalid-this + this.skip(); + } + }); + + setup(() => { + ioc = new DataScienceIocContainer(); + ioc.registerDataScienceTypes(); + }); + + function mountWebView(): ReactWrapper, React.Component> { + + // Setup our webview panel + ioc.createWebView(() => mount()); + + // Make sure the data explorer provider and execution factory in the container is created (the extension does this on startup in the extension) + dataProvider = ioc.get(IDataViewerProvider); + + // The history provider create needs to be rewritten to make the history window think the mounted web panel is + // ready. + const origFunc = (dataProvider as any).create.bind(dataProvider); + (dataProvider as any).create = async (v: string): Promise => { + const dataViewer = await origFunc(v); + + // During testing the MainPanel sends the init message before our history is created. + // Pretend like it's happening now + const listener = ((dataViewer as any).messageListener) as DataViewerMessageListener; + listener.onMessage(DataViewerMessages.Started, {}); + + return dataViewer; + }; + + return ioc.wrapper!; + } + + teardown(async () => { + for (const disposable of disposables) { + if (!disposable) { + continue; + } + // tslint:disable-next-line:no-any + const promise = disposable.dispose() as Promise; + if (promise) { + await promise; + } + } + await ioc.dispose(); + delete (global as any).ascquireVsCodeApi; + }); + + async function createDataViewer(variable: string): Promise { + return dataProvider.create(variable); + } + + async function injectCode(code: string) : Promise { + const exec = ioc.get(IJupyterExecution); + const server = await exec.connectToNotebookServer(); + if (server) { + await server.execute(code, Identifiers.EmptyFileName, 0, uuid()); + } + } + + // tslint:disable-next-line:no-any + function runMountedTest(name: string, testFunc: (wrapper: ReactWrapper, React.Component>) => Promise) { + test(name, async (_done: MochaDone) => { + const wrapper = mountWebView(); + try { + await testFunc(wrapper); + } finally { + // Blur window focus so we don't have editors polling + blurWindow(); + + // Make sure to unmount the wrapper or it will interfere with other tests + wrapper.unmount(); + } + }); + } + + runMountedTest('Data Frame', async (wrapper) => { + await injectCode('import pandas as pd\r\ndf = pd.DataFrame([0, 1, 2, 3])'); + const dv = await createDataViewer('df'); + assert.ok(dv, 'DataViewer not created'); + + verifyHtmlOnCell(wrapper, '1', CellPosition.Last); + }); + +}); diff --git a/src/test/datascience/history.functional.test.tsx b/src/test/datascience/history.functional.test.tsx index 34bb34ec3dda..c50427ce5e25 100644 --- a/src/test/datascience/history.functional.test.tsx +++ b/src/test/datascience/history.functional.test.tsx @@ -6,29 +6,18 @@ import { mount, ReactWrapper } from 'enzyme'; import * as fs from 'fs-extra'; import * as path from 'path'; import * as React from 'react'; -import { SemVer } from 'semver'; import * as TypeMoq from 'typemoq'; -import { Disposable, TextDocument, TextEditor, ViewColumn } from 'vscode'; +import { Disposable, TextDocument, TextEditor } from 'vscode'; -import { - IApplicationShell, - IDocumentManager, - IWebPanel, - IWebPanelMessageListener, - IWebPanelProvider, - WebPanelMessage -} from '../../client/common/application/types'; -import { createDeferred, Deferred } from '../../client/common/utils/async'; +import { IApplicationShell, IDocumentManager } from '../../client/common/application/types'; +import { createDeferred } from '../../client/common/utils/async'; import { noop } from '../../client/common/utils/misc'; -import { Architecture } from '../../client/common/utils/platform'; import { EditorContexts } from '../../client/datascience/constants'; import { HistoryMessageListener } from '../../client/datascience/history/historyMessageListener'; import { HistoryMessages } from '../../client/datascience/history/historyTypes'; import { IHistory, IHistoryProvider, IJupyterExecution } from '../../client/datascience/types'; -import { InterpreterType, PythonInterpreter } from '../../client/interpreter/contracts'; import { CellButton } from '../../datascience-ui/history-react/cellButton'; import { MainPanel } from '../../datascience-ui/history-react/MainPanel'; -import { IVsCodeApi } from '../../datascience-ui/react-common/postOffice'; import { sleep } from '../core'; import { DataScienceIocContainer } from './dataScienceIocContainer'; import { @@ -43,7 +32,6 @@ import { findButton, getCellResults, getLastOutputCell, - getMainPanel, initialDataScienceSettings, srcDirectory, toggleCellExpansion, @@ -51,84 +39,44 @@ import { verifyHtmlOnCell, verifyLastCellInputState } from './historyTestHelpers'; -import { SupportedCommands } from './mockJupyterManager'; import { blurWindow, waitForUpdate } from './reactHelpers'; // tslint:disable:max-func-body-length trailing-comma no-any no-multiline-string suite('History output tests', () => { const disposables: Disposable[] = []; let jupyterExecution: IJupyterExecution; - let webPanelProvider: TypeMoq.IMock; - let webPanel: TypeMoq.IMock; let historyProvider: IHistoryProvider; - let webPanelListener: IWebPanelMessageListener; - let globalAcquireVsCodeApi: () => IVsCodeApi; let ioc: DataScienceIocContainer; - let webPanelMessagePromise: Deferred | undefined; - - const workingPython: PythonInterpreter = { - path: '/foo/bar/python.exe', - version: new SemVer('3.6.6-final'), - sysVersion: '1.0.0.0', - sysPrefix: 'Python', - type: InterpreterType.Unknown, - architecture: Architecture.x64, - }; + setup(() => { ioc = new DataScienceIocContainer(); ioc.registerDataScienceTypes(); + }); - if (ioc.mockJupyter) { - ioc.mockJupyter.addInterpreter(workingPython, SupportedCommands.all); - } + function mountWebView(): ReactWrapper, React.Component> { - webPanelProvider = TypeMoq.Mock.ofType(); - webPanel = TypeMoq.Mock.ofType(); + // Setup our webview panel + ioc.createWebView(() => mount()); - ioc.serviceManager.addSingletonInstance(IWebPanelProvider, webPanelProvider.object); + // Make sure the history provider and execution factory in the container is created (the extension does this on startup in the extension) + historyProvider = ioc.get(IHistoryProvider); + jupyterExecution = ioc.get(IJupyterExecution); - // 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 - webPanelListener = listener; + // The history provider create needs to be rewritten to make the history window think the mounted web panel is + // ready. + const origFunc = (historyProvider as any).create.bind(historyProvider); + (historyProvider as any).create = async (): Promise => { + await origFunc(); + const history = historyProvider.getActive(); - // Return our dummy web panel - return webPanel.object; - }); - webPanel.setup(p => p.postMessage(TypeMoq.It.isAny())).callback((m: WebPanelMessage) => { - window.postMessage(m, '*'); - }); // See JSDOM valid target origins - webPanel.setup(p => p.show(true)); - - jupyterExecution = ioc.serviceManager.get(IJupyterExecution); - historyProvider = ioc.serviceManager.get(IHistoryProvider); - - // Setup a global for the acquireVsCodeApi so that the React PostOffice can find it - globalAcquireVsCodeApi = (): IVsCodeApi => { - return { - // tslint:disable-next-line:no-any - postMessage: (msg: any) => { - if (webPanelListener) { - webPanelListener.onMessage(msg.type, msg.payload); - } - if (webPanelMessagePromise) { - webPanelMessagePromise.resolve(); - } - }, - // tslint:disable-next-line:no-any no-empty - setState: (_msg: any) => { - - }, - // tslint:disable-next-line:no-any no-empty - getState: () => { - return {}; - } - }; + // During testing the MainPanel sends the init message before our history is created. + // Pretend like it's happening now + const listener = ((history as any).messageListener) as HistoryMessageListener; + listener.onMessage(HistoryMessages.Started, {}); }; - // tslint:disable-next-line:no-string-literal - (global as any)['acquireVsCodeApi'] = globalAcquireVsCodeApi; - }); + + return ioc.wrapper!; + } teardown(async () => { for (const disposable of disposables) { @@ -159,11 +107,9 @@ suite('History output tests', () => { // tslint:disable-next-line:no-any function runMountedTest(name: string, testFunc: (wrapper: ReactWrapper, React.Component>) => Promise) { test(name, async () => { - addMockData(ioc, 'a=1\na', 1); if (await jupyterExecution.isNotebookSupported()) { - // Create our main panel and tie it into the JSDOM. Ignore progress so we only get a single render - const wrapper = mount(); - getMainPanel(wrapper); + addMockData(ioc, 'a=1\na', 1); + const wrapper = mountWebView(); try { await testFunc(wrapper); } finally { @@ -181,10 +127,10 @@ suite('History output tests', () => { } async function waitForMessageResponse(action: () => void): Promise { - webPanelMessagePromise = createDeferred(); + ioc.wrapperCreatedPromise = createDeferred(); action(); - await webPanelMessagePromise.promise; - webPanelMessagePromise = undefined; + await ioc.wrapperCreatedPromise.promise; + ioc.wrapperCreatedPromise = undefined; } runMountedTest('Simple text', async (wrapper) => { diff --git a/src/test/datascience/liveshare.functional.test.tsx b/src/test/datascience/liveshare.functional.test.tsx index 79a60028536c..7906f988c2e2 100644 --- a/src/test/datascience/liveshare.functional.test.tsx +++ b/src/test/datascience/liveshare.functional.test.tsx @@ -4,24 +4,17 @@ import * as assert from 'assert'; import { mount, ReactWrapper } from 'enzyme'; import * as React from 'react'; -import { SemVer } from 'semver'; import * as TypeMoq from 'typemoq'; -import { Disposable, Uri, ViewColumn } from 'vscode'; +import { Disposable, Uri } from 'vscode'; import * as vsls from 'vsls/vscode'; import { ICommandManager, IDocumentManager, ILiveShareApi, - ILiveShareTestingApi, - IWebPanel, - IWebPanelMessageListener, - IWebPanelProvider, - WebPanelMessage + ILiveShareTestingApi } from '../../client/common/application/types'; import { IFileSystem } from '../../client/common/platform/types'; -import { createDeferred, Deferred } from '../../client/common/utils/async'; -import { Architecture } from '../../client/common/utils/platform'; import { Commands } from '../../client/datascience/constants'; import { HistoryMessageListener } from '../../client/datascience/history/historyMessageListener'; import { HistoryMessages } from '../../client/datascience/history/historyTypes'; @@ -32,51 +25,19 @@ import { IHistoryProvider, IJupyterExecution } from '../../client/datascience/types'; -import { InterpreterType, PythonInterpreter } from '../../client/interpreter/contracts'; import { MainPanel } from '../../datascience-ui/history-react/MainPanel'; -import { IVsCodeApi } from '../../datascience-ui/react-common/postOffice'; import { DataScienceIocContainer } from './dataScienceIocContainer'; import { createDocument } from './editor-integration/helpers'; import { addMockData, CellPosition, verifyHtmlOnCell } from './historyTestHelpers'; -import { SupportedCommands } from './mockJupyterManager'; -import { blurWindow, createMessageEvent, waitForUpdate } from './reactHelpers'; +import { waitForUpdate } from './reactHelpers'; //tslint:disable:trailing-comma no-any no-multiline-string -class ContainerData { - public ioc: DataScienceIocContainer | undefined; - public webPanelListener: IWebPanelMessageListener | undefined; - public wrapper: ReactWrapper, React.Component> | undefined; - public wrapperCreatedPromise: Deferred = createDeferred(); - public postMessage: ((ev: MessageEvent) => void) | undefined; - - public async dispose(): Promise { - if (this.ioc) { - await this.ioc.dispose(); - } - if (this.wrapper) { - // Blur window focus so we don't have editors polling - blurWindow(); - this.wrapper.unmount(); - this.wrapper = undefined; - } - } -} - // tslint:disable-next-line:max-func-body-length no-any suite('LiveShare tests', () => { const disposables: Disposable[] = []; - let hostContainer: ContainerData; - let guestContainer: ContainerData; - - const workingPython: PythonInterpreter = { - path: '/foo/bar/python.exe', - version: new SemVer('3.6.6-final'), - sysVersion: '1.0.0.0', - sysPrefix: 'Python', - type: InterpreterType.Unknown, - architecture: Architecture.x64, - }; + let hostContainer: DataScienceIocContainer; + let guestContainer: DataScienceIocContainer; setup(() => { hostContainer = createContainer(vsls.Role.Host); @@ -98,49 +59,16 @@ suite('LiveShare tests', () => { await guestContainer.dispose(); }); - function createContainer(role: vsls.Role): ContainerData { - const result = new ContainerData(); - result.ioc = new DataScienceIocContainer(); - result.ioc.registerDataScienceTypes(); - - if (result.ioc.mockJupyter) { - result.ioc.mockJupyter.addInterpreter(workingPython, SupportedCommands.all); - } - - // Force the container to mock actual live share - const liveShareTest = result.ioc.get(ILiveShareApi) as ILiveShareTestingApi; - liveShareTest.forceRole(role); - - const webPanelProvider = TypeMoq.Mock.ofType(); - const webPanel = TypeMoq.Mock.ofType(); + function createContainer(role: vsls.Role): DataScienceIocContainer { + const result = new DataScienceIocContainer(); + result.registerDataScienceTypes(); - result.ioc.serviceManager.addSingletonInstance(IWebPanelProvider, webPanelProvider.object); - - // 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 - result.webPanelListener = listener; - - // Return our dummy web panel - return webPanel.object; - }); - webPanel.setup(p => p.postMessage(TypeMoq.It.isAny())).callback((m: WebPanelMessage) => { - const message = createMessageEvent(m); - if (result.postMessage) { - result.postMessage(message); - } else { - throw new Error('postMessage callback not defined'); - } - }); - webPanel.setup(p => p.show(true)); - - // We need to mount the react control before we even create a history object. Otherwise the mount will miss rendering some parts - mountReactControl(result); + // Setup our webview panel + result.createWebView(() => mount(), role); // Make sure the history provider and execution factory in the container is created (the extension does this on startup in the extension) - const historyProvider = result.ioc.get(IHistoryProvider); - result.ioc.get(IJupyterExecution); + const historyProvider = result.get(IHistoryProvider); + result.get(IJupyterExecution); // The history provider create needs to be rewritten to make the history window think the mounted web panel is // ready. @@ -158,55 +86,15 @@ suite('LiveShare tests', () => { return result; } - function mountReactControl(container: ContainerData) { - // Setup the acquireVsCodeApi. The react control will cache this value when it's mounted. - const globalAcquireVsCodeApi = (): IVsCodeApi => { - return { - // tslint:disable-next-line:no-any - postMessage: (msg: any) => { - if (container.webPanelListener) { - container.webPanelListener.onMessage(msg.type, msg.payload); - } - }, - // tslint:disable-next-line:no-any no-empty - setState: (_msg: any) => { - - }, - // tslint:disable-next-line:no-any no-empty - getState: () => { - return {}; - } - }; - }; - // tslint:disable-next-line:no-string-literal - (global as any)['acquireVsCodeApi'] = globalAcquireVsCodeApi; - - // Remap event handlers to point to the container. - const oldListener = window.addEventListener; - window.addEventListener = (event: string, cb: any) => { - if (event === 'message') { - container.postMessage = cb; - } - }; - - // Mount our main panel. This will make the global api be cached and have the event handler registered - const mounted = mount(); - container.wrapper = mounted; - - // We can remove the global api and event listener now. - delete (global as any).acquireVsCodeApi; - window.addEventListener = oldListener; - } - function getOrCreateHistory(role: vsls.Role): Promise { // Get the container to use based on the role. const container = role === vsls.Role.Host ? hostContainer : guestContainer; - return container.ioc!.get(IHistoryProvider).getOrCreateActive(); + return container!.get(IHistoryProvider).getOrCreateActive(); } function isSessionStarted(role: vsls.Role): boolean { const container = role === vsls.Role.Host ? hostContainer : guestContainer; - const api = container.ioc!.get(ILiveShareApi) as ILiveShareTestingApi; + const api = container!.get(ILiveShareApi) as ILiveShareTestingApi; return api.isSessionStarted; } @@ -255,19 +143,19 @@ suite('LiveShare tests', () => { function startSession(role: vsls.Role): Promise { const container = role === vsls.Role.Host ? hostContainer : guestContainer; - const api = container.ioc!.get(ILiveShareApi) as ILiveShareTestingApi; + const api = container!.get(ILiveShareApi) as ILiveShareTestingApi; return api.startSession(); } function stopSession(role: vsls.Role): Promise { const container = role === vsls.Role.Host ? hostContainer : guestContainer; - const api = container.ioc!.get(ILiveShareApi) as ILiveShareTestingApi; + const api = container!.get(ILiveShareApi) as ILiveShareTestingApi; return api.stopSession(); } test('Host alone', async () => { // Should only need mock data in host - addMockData(hostContainer.ioc!, 'a=1\na', 1); + addMockData(hostContainer!, 'a=1\na', 1); // Start the host session first await startSession(vsls.Role.Host); @@ -279,7 +167,7 @@ suite('LiveShare tests', () => { test('Host & Guest Simple', async () => { // Should only need mock data in host - addMockData(hostContainer.ioc!, 'a=1\na', 1); + addMockData(hostContainer!, 'a=1\na', 1); // Create the host history and then the guest history await getOrCreateHistory(vsls.Role.Host); @@ -298,7 +186,7 @@ suite('LiveShare tests', () => { test('Host Shutdown and Run', async () => { // Should only need mock data in host - addMockData(hostContainer.ioc!, 'a=1\na', 1); + addMockData(hostContainer!, 'a=1\na', 1); // Create the host history and then the guest history await getOrCreateHistory(vsls.Role.Host); @@ -318,7 +206,7 @@ suite('LiveShare tests', () => { test('Host startup and guest restart', async () => { // Should only need mock data in host - addMockData(hostContainer.ioc!, 'a=1\na', 1); + addMockData(hostContainer!, 'a=1\na', 1); // Start the host, and add some data const host = await getOrCreateHistory(vsls.Role.Host); @@ -342,7 +230,7 @@ suite('LiveShare tests', () => { test('Going through codewatcher', async () => { // Should only need mock data in host - addMockData(hostContainer.ioc!, 'a=1\na', 1); + addMockData(hostContainer!, 'a=1\na', 1); // Start both the host and the guest await startSession(vsls.Role.Host); @@ -355,7 +243,7 @@ suite('LiveShare tests', () => { const document = createDocument(inputText, fileName, version, TypeMoq.Times.atLeastOnce()); document.setup(doc => doc.getText(TypeMoq.It.isAny())).returns(() => inputText); - const codeWatcher = guestContainer.ioc!.get(ICodeWatcher); + const codeWatcher = guestContainer!.get(ICodeWatcher); codeWatcher.setDocument(document.object); // Send code using a codewatcher instead (we're sending it through the guest) @@ -371,13 +259,13 @@ suite('LiveShare tests', () => { test('Export from guest', async () => { // Should only need mock data in host - addMockData(hostContainer.ioc!, 'a=1\na', 1); + addMockData(hostContainer!, 'a=1\na', 1); // Remap the fileSystem so we control the write for the notebook. Have to do this // before the listener is created so that it uses this file system. let outputContents: string | undefined; const fileSystem = TypeMoq.Mock.ofType(); - guestContainer.ioc!.serviceManager.rebindInstance(IFileSystem, fileSystem.object); + guestContainer!.serviceManager.rebindInstance(IFileSystem, fileSystem.object); fileSystem.setup(f => f.writeFile(TypeMoq.It.isAny(), TypeMoq.It.isAny())).returns((_f, c) => { outputContents = c.toString(); return Promise.resolve(); @@ -387,8 +275,8 @@ suite('LiveShare tests', () => { fileSystem.setup(f => f.getSubDirectories(TypeMoq.It.isAny())).returns(() => Promise.resolve([])); // Need to register commands as our extension isn't actually loading. - const listener = guestContainer.ioc!.get(IDataScienceCommandListener); - const guestCommandManager = guestContainer.ioc!.get(ICommandManager); + const listener = guestContainer!.get(IDataScienceCommandListener); + const guestCommandManager = guestContainer!.get(ICommandManager); listener.register(guestCommandManager); // Start both the host and the guest @@ -396,8 +284,8 @@ suite('LiveShare tests', () => { await startSession(vsls.Role.Guest); // Create a document on the guest - guestContainer.ioc!.addDocument('#%%\na=1\na', 'foo.py'); - guestContainer.ioc!.get(IDocumentManager).showTextDocument(Uri.file('foo.py')); + guestContainer!.addDocument('#%%\na=1\na', 'foo.py'); + guestContainer!.get(IDocumentManager).showTextDocument(Uri.file('foo.py')); // Attempt to export a file from the guest by running an ExportFileAndOutputAsNotebook const executePromise = guestCommandManager.executeCommand(Commands.ExportFileAndOutputAsNotebook, Uri.file('foo.py')) as Promise; From 5cf4e03d5a8d3912ac8c56bf907b823a11ddc15c Mon Sep 17 00:00:00 2001 From: rchiodo Date: Mon, 1 Apr 2019 10:45:43 -0700 Subject: [PATCH 04/10] Add completed message. --- src/client/datascience/data-viewing/types.ts | 2 + .../data-explorer/mainPanel.tsx | 7 +++ .../datascience/dataScienceIocContainer.ts | 7 +-- .../dataviewer.functional.test.tsx | 48 ++++++++++++++++--- 4 files changed, 55 insertions(+), 9 deletions(-) diff --git a/src/client/datascience/data-viewing/types.ts b/src/client/datascience/data-viewing/types.ts index 06325e0d01ba..81feb8d42315 100644 --- a/src/client/datascience/data-viewing/types.ts +++ b/src/client/datascience/data-viewing/types.ts @@ -23,6 +23,7 @@ export namespace DataViewerMessages { export const GetAllRowsResponse = 'get_all_rows_response'; export const GetRowsRequest = 'get_rows_request'; export const GetRowsResponse = 'get_rows_response'; + export const CompletedData = 'complete'; } export interface IGetRowsRequest { @@ -45,4 +46,5 @@ export class IDataViewerMapping { public [DataViewerMessages.GetAllRowsResponse]: JSONObject; public [DataViewerMessages.GetRowsRequest]: IGetRowsRequest; public [DataViewerMessages.GetRowsResponse]: IGetRowsResponse; + public [DataViewerMessages.CompletedData]: never | undefined; } diff --git a/src/datascience-ui/data-explorer/mainPanel.tsx b/src/datascience-ui/data-explorer/mainPanel.tsx index d6b8def1e5e3..dcfb35702052 100644 --- a/src/datascience-ui/data-explorer/mainPanel.tsx +++ b/src/datascience-ui/data-explorer/mainPanel.tsx @@ -61,6 +61,7 @@ export class MainPanel extends React.Component private container: HTMLDivElement | null = null; private emptyRows: (() => JSX.Element) | undefined; private getEmptyRows: ((props: any) => JSX.Element) | undefined; + private sentDone = false; // tslint:disable-next-line:max-func-body-length constructor(props: IMainPanelProps, _state: IMainPanelState) { @@ -112,6 +113,12 @@ export class MainPanel extends React.Component }; } public render = () => { + // Send our done message if we haven't yet and we just reached full capacity. Do it here so we + // can guarantee our render will run before somebody checks our rendered output. + if (this.state.actualRowCount === this.state.fetchedRowCount && !this.sentDone) { + this.sentDone = true; + this.sendMessage(DataViewerMessages.CompletedData); + } return (
diff --git a/src/test/datascience/dataScienceIocContainer.ts b/src/test/datascience/dataScienceIocContainer.ts index 3045bafecf70..0423a72375da 100644 --- a/src/test/datascience/dataScienceIocContainer.ts +++ b/src/test/datascience/dataScienceIocContainer.ts @@ -238,9 +238,10 @@ export class DataScienceIocContainer extends UnitTestIocContainer { await this.asyncRegistry.dispose(); await super.dispose(); - if (this.wrapper) { - // Blur window focus so we don't have editors polling - blurWindow(); + // Blur window focus so we don't have editors polling + blurWindow(); + + if (this.wrapper && this.wrapper.length) { this.wrapper.unmount(); this.wrapper = undefined; } diff --git a/src/test/datascience/dataviewer.functional.test.tsx b/src/test/datascience/dataviewer.functional.test.tsx index f7bec818969f..1ad5b3d99af2 100644 --- a/src/test/datascience/dataviewer.functional.test.tsx +++ b/src/test/datascience/dataviewer.functional.test.tsx @@ -8,6 +8,7 @@ import * as React from 'react'; import * as uuid from 'uuid/v4'; import { Disposable } from 'vscode'; +import { createDeferred } from '../../client/common/utils/async'; import { Identifiers } from '../../client/datascience/constants'; import { DataViewerMessageListener } from '../../client/datascience/data-viewing/dataViewerMessageListener'; import { DataViewerMessages } from '../../client/datascience/data-viewing/types'; @@ -16,11 +17,13 @@ import { MainPanel } from '../../datascience-ui/data-explorer/mainPanel'; import { DataScienceIocContainer } from './dataScienceIocContainer'; import { CellPosition, verifyHtmlOnCell } from './historyTestHelpers'; import { blurWindow } from './reactHelpers'; +import { nbformat } from '@jupyterlab/coreutils'; suite('DataViewer tests', () => { const disposables: Disposable[] = []; let dataProvider: IDataViewerProvider; let ioc: DataScienceIocContainer; + let messageWrapper: ((m: string, payload: any) => void) | undefined; suiteSetup(function () { // DataViewer tests require jupyter to run. Othewrise can't @@ -58,6 +61,15 @@ suite('DataViewer tests', () => { const listener = ((dataViewer as any).messageListener) as DataViewerMessageListener; listener.onMessage(DataViewerMessages.Started, {}); + // Rewrite the onMessage function to also call the local messageWrapper if it's defined + const orig = listener.onMessage.bind(listener); + listener.onMessage = (m: string, payload: any) => { + if (messageWrapper) { + messageWrapper(m, payload); + } + return orig(m, payload); + }; + return dataViewer; }; @@ -87,30 +99,54 @@ suite('DataViewer tests', () => { const exec = ioc.get(IJupyterExecution); const server = await exec.connectToNotebookServer(); if (server) { - await server.execute(code, Identifiers.EmptyFileName, 0, uuid()); + const cells = await server.execute(code, Identifiers.EmptyFileName, 0, uuid()); + assert.equal(cells.length, 1, `Wrong number of cells returned`); + assert.equal(cells[0].data.cell_type, 'code', `Wrong type of cell returned`); + const cell = cells[0].data as nbformat.ICodeCell; + assert.ok(cell.outputs.length > 0, `Cell length not correct`); + const error = cell.outputs[0].evalue; + if (error) { + assert.fail(`Unexpected error: ${error}`); + } } } + function waitForMessage(message: string) : Promise { + // Wait for the mounted web panel to send a message back to the data explorer + const promise = createDeferred(); + messageWrapper = (m: string, _p: any) => { + if (m === message) { + promise.resolve(); + } + }; + return promise.promise; + } + + function getCompletedPromise() : Promise { + return waitForMessage(DataViewerMessages.CompletedData); + } + // tslint:disable-next-line:no-any function runMountedTest(name: string, testFunc: (wrapper: ReactWrapper, React.Component>) => Promise) { - test(name, async (_done: MochaDone) => { + test(name, async () => { const wrapper = mountWebView(); try { await testFunc(wrapper); } finally { - // Blur window focus so we don't have editors polling - blurWindow(); - // Make sure to unmount the wrapper or it will interfere with other tests - wrapper.unmount(); + if (wrapper && wrapper.length) { + wrapper.unmount(); + } } }); } runMountedTest('Data Frame', async (wrapper) => { await injectCode('import pandas as pd\r\ndf = pd.DataFrame([0, 1, 2, 3])'); + const gotAllRows = getCompletedPromise(); const dv = await createDataViewer('df'); assert.ok(dv, 'DataViewer not created'); + await gotAllRows; verifyHtmlOnCell(wrapper, '1', CellPosition.Last); }); From 45a551641ab7c19806d9a3628e8aa02c0514bd2b Mon Sep 17 00:00:00 2001 From: rchiodo Date: Tue, 2 Apr 2019 14:24:56 -0700 Subject: [PATCH 05/10] Test starting up --- .../datascience/jupyter/jupyterVariables.ts | 11 +++++----- .../dataviewer.functional.test.tsx | 22 ++++++++++++------- 2 files changed, 20 insertions(+), 13 deletions(-) diff --git a/src/client/datascience/jupyter/jupyterVariables.ts b/src/client/datascience/jupyter/jupyterVariables.ts index 8f3a74e4d54f..c699af2f5c11 100644 --- a/src/client/datascience/jupyter/jupyterVariables.ts +++ b/src/client/datascience/jupyter/jupyterVariables.ts @@ -36,7 +36,7 @@ export class JupyterVariables implements IJupyterVariables { return this.runScript( undefined, [], - this.fetchVariablesScript); + () => this.fetchVariablesScript); } public async getValue(targetVariable: IJupyterVariable): Promise { @@ -44,7 +44,7 @@ export class JupyterVariables implements IJupyterVariables { return this.runScript( targetVariable, targetVariable, - this.fetchVariableValueScript); + () => this.fetchVariableValueScript); } public async getDataFrameInfo(targetVariable: IJupyterVariable): Promise { @@ -52,7 +52,7 @@ export class JupyterVariables implements IJupyterVariables { return this.runScript( targetVariable, targetVariable, - this.fetchDataFrameInfoScript, + () => this.fetchDataFrameInfoScript, [{key: '_VSCode_JupyterValuesColumn', value: localize.DataScience.valuesColumn()}]); } @@ -61,7 +61,7 @@ export class JupyterVariables implements IJupyterVariables { return this.runScript( targetVariable, {}, - this.fetchDataFrameRowsScript, + () => this.fetchDataFrameRowsScript, [ {key: '_VSCode_JupyterValuesColumn', value: localize.DataScience.valuesColumn()}, {key: '_VSCode_JupyterStartRow', value: start.toString()}, @@ -90,12 +90,13 @@ export class JupyterVariables implements IJupyterVariables { private async runScript( targetVariable: IJupyterVariable | undefined, defaultValue: T, - scriptBaseText: string | undefined, + scriptBaseTextFetcher: () => string | undefined, extraReplacements: { key: string; value: string }[] = []): Promise { if (!this.filesLoaded) { await this.loadVariableFiles(); } + const scriptBaseText = scriptBaseTextFetcher(); const activeServer = await this.jupyterExecution.getServer(await this.historyProvider.getNotebookOptions()); if (!activeServer || !scriptBaseText) { // No active server just return the unchanged target variable diff --git a/src/test/datascience/dataviewer.functional.test.tsx b/src/test/datascience/dataviewer.functional.test.tsx index 1ad5b3d99af2..2ac5ebe46d92 100644 --- a/src/test/datascience/dataviewer.functional.test.tsx +++ b/src/test/datascience/dataviewer.functional.test.tsx @@ -2,6 +2,7 @@ // Licensed under the MIT License. 'use strict'; // tslint:disable:max-func-body-length trailing-comma no-any no-multiline-string +import { nbformat } from '@jupyterlab/coreutils'; import * as assert from 'assert'; import { mount, ReactWrapper } from 'enzyme'; import * as React from 'react'; @@ -12,13 +13,12 @@ import { createDeferred } from '../../client/common/utils/async'; import { Identifiers } from '../../client/datascience/constants'; import { DataViewerMessageListener } from '../../client/datascience/data-viewing/dataViewerMessageListener'; import { DataViewerMessages } from '../../client/datascience/data-viewing/types'; -import { IDataViewer, IDataViewerProvider, IJupyterExecution } from '../../client/datascience/types'; +import { IDataViewer, IDataViewerProvider, IHistoryProvider, IJupyterExecution } from '../../client/datascience/types'; import { MainPanel } from '../../datascience-ui/data-explorer/mainPanel'; import { DataScienceIocContainer } from './dataScienceIocContainer'; import { CellPosition, verifyHtmlOnCell } from './historyTestHelpers'; -import { blurWindow } from './reactHelpers'; -import { nbformat } from '@jupyterlab/coreutils'; +// import { asyncDump } from '../common/asyncDump'; suite('DataViewer tests', () => { const disposables: Disposable[] = []; let dataProvider: IDataViewerProvider; @@ -91,22 +91,28 @@ suite('DataViewer tests', () => { delete (global as any).ascquireVsCodeApi; }); + suiteTeardown(() => { + // asyncDump(); + }); + async function createDataViewer(variable: string): Promise { return dataProvider.create(variable); } async function injectCode(code: string) : Promise { const exec = ioc.get(IJupyterExecution); - const server = await exec.connectToNotebookServer(); + const historyProvider = ioc.get(IHistoryProvider); + const server = await exec.connectToNotebookServer(await historyProvider.getNotebookOptions()); if (server) { const cells = await server.execute(code, Identifiers.EmptyFileName, 0, uuid()); assert.equal(cells.length, 1, `Wrong number of cells returned`); assert.equal(cells[0].data.cell_type, 'code', `Wrong type of cell returned`); const cell = cells[0].data as nbformat.ICodeCell; - assert.ok(cell.outputs.length > 0, `Cell length not correct`); - const error = cell.outputs[0].evalue; - if (error) { - assert.fail(`Unexpected error: ${error}`); + if (cell.outputs.length > 0) { + const error = cell.outputs[0].evalue; + if (error) { + assert.fail(`Unexpected error: ${error}`); + } } } } From ff650a0a7fc4aeb64a381c442651f30d8e7e81ab Mon Sep 17 00:00:00 2001 From: rchiodo Date: Tue, 2 Apr 2019 17:08:05 -0700 Subject: [PATCH 06/10] Finish dataviewer functional tests --- package-lock.json | 9 ++ package.json | 1 + .../data-explorer/cellFormatter.tsx | 6 +- .../data-explorer/mainPanel.tsx | 7 +- .../datascience/dataScienceIocContainer.ts | 19 +++- .../dataviewer.functional.test.tsx | 86 ++++++++++++++++--- 6 files changed, 106 insertions(+), 22 deletions(-) diff --git a/package-lock.json b/package-lock.json index e00f799eea0b..f37f825ee6f8 100644 --- a/package-lock.json +++ b/package-lock.json @@ -10463,6 +10463,15 @@ "fs-walk": "0.0.1" } }, + "node-html-parser": { + "version": "1.1.13", + "resolved": "https://registry.npmjs.org/node-html-parser/-/node-html-parser-1.1.13.tgz", + "integrity": "sha512-g8H73/DHTFH17N0dukN1XkCdJm9TF9cpsaElT/4PeIQR+hBR2T3rmheO1EeFBOqg4ot2s1530XPD1/dsVk8MNQ==", + "dev": true, + "requires": { + "he": "1.1.1" + } + }, "node-libs-browser": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/node-libs-browser/-/node-libs-browser-2.1.0.tgz", diff --git a/package.json b/package.json index af1665b86167..52246835ec3a 100644 --- a/package.json +++ b/package.json @@ -2397,6 +2397,7 @@ "mocha-junit-reporter": "^1.17.0", "mocha-multi-reporters": "^1.1.7", "node-has-native-dependencies": "^1.0.2", + "node-html-parser": "^1.1.13", "nyc": "^13.3.0", "raw-loader": "^0.5.1", "react": "^16.5.2", diff --git a/src/datascience-ui/data-explorer/cellFormatter.tsx b/src/datascience-ui/data-explorer/cellFormatter.tsx index de935dd0e32c..71637fc756d6 100644 --- a/src/datascience-ui/data-explorer/cellFormatter.tsx +++ b/src/datascience-ui/data-explorer/cellFormatter.tsx @@ -49,16 +49,16 @@ export class CellFormatter extends React.Component { // Otherwise an unknown type or a string const val = this.props.value !== null ? this.props.value.toString() : ''; - return (
{val}
); + return (
{val}
); } private renderBool(value: boolean) { - return {value.toString()}; + return
{value.toString()}
; } private renderNumber(value: number) { const val = value.toString(); - return
{val}
; + return
{val}
; } } diff --git a/src/datascience-ui/data-explorer/mainPanel.tsx b/src/datascience-ui/data-explorer/mainPanel.tsx index dcfb35702052..2241a2c22551 100644 --- a/src/datascience-ui/data-explorer/mainPanel.tsx +++ b/src/datascience-ui/data-explorer/mainPanel.tsx @@ -39,6 +39,7 @@ const defaultColumnProperties = { export interface IMainPanelProps { skipDefault?: boolean; + forceHeight?: number; } //tslint:disable:no-any @@ -77,7 +78,7 @@ export class MainPanel extends React.Component actualRowCount: data.rows.length + 100, fetchedRowCount: data.rows.length, filters: {}, - gridHeight: 100, + gridHeight: 100, sortColumn: 'index', sortDirection: 'NONE' }; @@ -115,7 +116,7 @@ export class MainPanel extends React.Component public render = () => { // Send our done message if we haven't yet and we just reached full capacity. Do it here so we // can guarantee our render will run before somebody checks our rendered output. - if (this.state.actualRowCount === this.state.fetchedRowCount && !this.sentDone) { + if (this.state.actualRowCount && this.state.actualRowCount === this.state.fetchedRowCount && !this.sentDone) { this.sentDone = true; this.sendMessage(DataViewerMessages.CompletedData); } @@ -301,7 +302,7 @@ export class MainPanel extends React.Component private updateDimensions = () => { if (this.container) { const height = this.container.offsetHeight; - this.setState({ gridHeight: height - 100 }); + this.setState({ gridHeight: this.props.forceHeight ? this.props.forceHeight : height - 100 }); } } diff --git a/src/test/datascience/dataScienceIocContainer.ts b/src/test/datascience/dataScienceIocContainer.ts index 0423a72375da..62e629edfd4e 100644 --- a/src/test/datascience/dataScienceIocContainer.ts +++ b/src/test/datascience/dataScienceIocContainer.ts @@ -222,7 +222,7 @@ export class DataScienceIocContainer extends UnitTestIocContainer { type: InterpreterType.Unknown, architecture: Architecture.x64, }; - + private extraListeners: ((m: string, p: any) => void)[] = []; constructor() { super(); const isRollingBuild = process.env ? process.env.VSCODE_PYTHON_ROLLING !== undefined : false; @@ -546,6 +546,10 @@ export class DataScienceIocContainer extends UnitTestIocContainer { this.documentManager.addDocument(code, file); } + public addMessageListener(callback: (m: string, p: any) => void) { + this.extraListeners.push(callback); + } + private findPythonPath(): string { try { const output = child_process.execFileSync('python', ['-c', 'import sys;print(sys.executable)'], { encoding: 'utf8' }); @@ -555,15 +559,22 @@ export class DataScienceIocContainer extends UnitTestIocContainer { } } + private postMessageToWebPanel(msg: any) { + if (this.webPanelListener) { + this.webPanelListener.onMessage(msg.type, msg.payload); + } + if (this.extraListeners.length) { + this.extraListeners.forEach(e => e(msg.type, msg.payload)); + } + } + private mountReactControl(mount: () => ReactWrapper, React.Component>) { // Setup the acquireVsCodeApi. The react control will cache this value when it's mounted. const globalAcquireVsCodeApi = (): IVsCodeApi => { return { // tslint:disable-next-line:no-any postMessage: (msg: any) => { - if (this.webPanelListener) { - this.webPanelListener.onMessage(msg.type, msg.payload); - } + this.postMessageToWebPanel(msg); }, // tslint:disable-next-line:no-any no-empty setState: (_msg: any) => { diff --git a/src/test/datascience/dataviewer.functional.test.tsx b/src/test/datascience/dataviewer.functional.test.tsx index 2ac5ebe46d92..ee0d77e1f226 100644 --- a/src/test/datascience/dataviewer.functional.test.tsx +++ b/src/test/datascience/dataviewer.functional.test.tsx @@ -2,9 +2,12 @@ // Licensed under the MIT License. 'use strict'; // tslint:disable:max-func-body-length trailing-comma no-any no-multiline-string +import '../../client/common/extensions'; + import { nbformat } from '@jupyterlab/coreutils'; import * as assert from 'assert'; import { mount, ReactWrapper } from 'enzyme'; +import { parse } from 'node-html-parser'; import * as React from 'react'; import * as uuid from 'uuid/v4'; import { Disposable } from 'vscode'; @@ -15,8 +18,8 @@ import { DataViewerMessageListener } from '../../client/datascience/data-viewing import { DataViewerMessages } from '../../client/datascience/data-viewing/types'; import { IDataViewer, IDataViewerProvider, IHistoryProvider, IJupyterExecution } from '../../client/datascience/types'; import { MainPanel } from '../../datascience-ui/data-explorer/mainPanel'; +import { noop } from '../core'; import { DataScienceIocContainer } from './dataScienceIocContainer'; -import { CellPosition, verifyHtmlOnCell } from './historyTestHelpers'; // import { asyncDump } from '../common/asyncDump'; suite('DataViewer tests', () => { @@ -40,12 +43,21 @@ suite('DataViewer tests', () => { setup(() => { ioc = new DataScienceIocContainer(); ioc.registerDataScienceTypes(); + + // Add a listener for our ioc that lets the test + // forward messages on + ioc.addMessageListener((m, p) => { + if (messageWrapper) { + messageWrapper(m, p); + } + }); + }); function mountWebView(): ReactWrapper, React.Component> { // Setup our webview panel - ioc.createWebView(() => mount()); + ioc.createWebView(() => mount()); // Make sure the data explorer provider and execution factory in the container is created (the extension does this on startup in the extension) dataProvider = ioc.get(IDataViewerProvider); @@ -61,15 +73,6 @@ suite('DataViewer tests', () => { const listener = ((dataViewer as any).messageListener) as DataViewerMessageListener; listener.onMessage(DataViewerMessages.Started, {}); - // Rewrite the onMessage function to also call the local messageWrapper if it's defined - const orig = listener.onMessage.bind(listener); - listener.onMessage = (m: string, payload: any) => { - if (messageWrapper) { - messageWrapper(m, payload); - } - return orig(m, payload); - }; - return dataViewer; }; @@ -147,6 +150,26 @@ suite('DataViewer tests', () => { }); } + function verifyRows(wrapper: ReactWrapper, React.Component>, rows: (string | number)[]) { + const canvas = wrapper.find('div.react-grid-Canvas'); + assert.ok(canvas.length >= 1, 'Didn\'t find any cells being rendered'); + + // Force the canvas to actually render. + const html = canvas.html(); + const root = parse(html) as any; + const cells = root.querySelectorAll('.react-grid-Cell') as HTMLElement[]; + assert.ok(cells, 'No cells found'); + assert.ok(cells.length >= rows.length, 'Not enough cells found'); + // Cells should be an array that matches up to the values we expect. + for (let i = 0; i < rows.length; i += 1) { + // Span should have our value (based on the CellFormatter's output) + const span = cells[i].querySelector('div.cell-formatter span') as HTMLSpanElement; + assert.ok(span, `Span ${i} not found`); + const val = rows[i].toString(); + assert.equal(val, span.innerHTML, `Row ${i} not matching. ${span.innerHTML} !== ${val}`); + } + } + runMountedTest('Data Frame', async (wrapper) => { await injectCode('import pandas as pd\r\ndf = pd.DataFrame([0, 1, 2, 3])'); const gotAllRows = getCompletedPromise(); @@ -154,7 +177,46 @@ suite('DataViewer tests', () => { assert.ok(dv, 'DataViewer not created'); await gotAllRows; - verifyHtmlOnCell(wrapper, '1', CellPosition.Last); + verifyRows(wrapper, [0, 0, 1, 1, 2, 2, 3, 3]); + }); + + runMountedTest('List', async (wrapper) => { + await injectCode('ls = [0, 1, 2, 3]'); + const gotAllRows = getCompletedPromise(); + const dv = await createDataViewer('ls'); + assert.ok(dv, 'DataViewer not created'); + await gotAllRows; + + verifyRows(wrapper, [0, 0, 1, 1, 2, 2, 3, 3]); + }); + + runMountedTest('Series', async (wrapper) => { + await injectCode('import pandas as pd\r\ns = pd.Series([0, 1, 2, 3])'); + const gotAllRows = getCompletedPromise(); + const dv = await createDataViewer('s'); + assert.ok(dv, 'DataViewer not created'); + await gotAllRows; + + verifyRows(wrapper, [0, 0, 1, 1, 2, 2, 3, 3]); }); + runMountedTest('np.array', async (wrapper) => { + await injectCode('import numpy as np\r\nx = np.array([0, 1, 2, 3])'); + const gotAllRows = getCompletedPromise(); + const dv = await createDataViewer('x'); + assert.ok(dv, 'DataViewer not created'); + await gotAllRows; + + verifyRows(wrapper, [0, 0, 1, 1, 2, 2, 3, 3]); + }); + + runMountedTest('Failure', async (_wrapper) => { + await injectCode('import numpy as np\r\nx = np.array([0, 1, 2, 3])'); + try { + await createDataViewer('unknown variable'); + assert.fail('Exception should have been thrown'); + } catch { + noop(); + } + }); }); From 9ac15eb825626ad0a22e8faf2727e33fc8ed91e1 Mon Sep 17 00:00:00 2001 From: rchiodo Date: Tue, 2 Apr 2019 17:38:34 -0700 Subject: [PATCH 07/10] Fix functional tests --- .../history-react/MainPanel.tsx | 11 +------- .../datascience/dataScienceIocContainer.ts | 16 +++++------ .../datascience/history.functional.test.tsx | 27 ++++++++----------- 3 files changed, 19 insertions(+), 35 deletions(-) diff --git a/src/datascience-ui/history-react/MainPanel.tsx b/src/datascience-ui/history-react/MainPanel.tsx index 93c87db16179..6f6b15da28eb 100644 --- a/src/datascience-ui/history-react/MainPanel.tsx +++ b/src/datascience-ui/history-react/MainPanel.tsx @@ -211,15 +211,6 @@ export class MainPanel extends React.Component submitInput: this.submitInput }; } - private renderDataFrameTestButton() { - if (getSettings && getSettings().showJupyterVariableExplorer) { - return ( - - D - - ); - } - private getHeaderProps = (baseTheme: string): IHeaderPanelProps => { return { addMarkdown: this.addMarkdown, @@ -233,7 +224,7 @@ export class MainPanel extends React.Component redo: this.redo, clearAll: this.clearAll, skipDefault: this.props.skipDefault, - showDataExplorer: this.showDataExplorer, + showDataExplorer: this.showDataViewer, testMode: this.props.testMode, variableExplorerRef: this.variableExplorerRef, canCollapseAll: this.canCollapseAll(), diff --git a/src/test/datascience/dataScienceIocContainer.ts b/src/test/datascience/dataScienceIocContainer.ts index 62e629edfd4e..24e66a694d60 100644 --- a/src/test/datascience/dataScienceIocContainer.ts +++ b/src/test/datascience/dataScienceIocContainer.ts @@ -458,14 +458,14 @@ export class DataScienceIocContainer extends UnitTestIocContainer { const interpreterManager = this.serviceContainer.get(IInterpreterService); interpreterManager.initialize(); - } - - // tslint:disable:any - public createWebView(mount: () => ReactWrapper, React.Component>, role: vsls.Role = vsls.Role.None) { if (this.mockJupyter) { this.mockJupyter.addInterpreter(this.workingPython, SupportedCommands.all); } + } + + // tslint:disable:any + public createWebView(mount: () => ReactWrapper, React.Component>, role: vsls.Role = vsls.Role.None) { // Force the container to mock actual live share if necessary if (role !== vsls.Role.None) { @@ -491,11 +491,6 @@ export class DataScienceIocContainer extends UnitTestIocContainer { const message = createMessageEvent(m); if (this.postMessage) { this.postMessage(message); - - if (this.wrapperCreatedPromise && !this.wrapperCreatedPromise.resolved) { - this.wrapperCreatedPromise.resolve(); - } - } else { throw new Error('postMessage callback not defined'); } @@ -566,6 +561,9 @@ export class DataScienceIocContainer extends UnitTestIocContainer { if (this.extraListeners.length) { this.extraListeners.forEach(e => e(msg.type, msg.payload)); } + if (this.wrapperCreatedPromise && !this.wrapperCreatedPromise.resolved) { + this.wrapperCreatedPromise.resolve(); + } } private mountReactControl(mount: () => ReactWrapper, React.Component>) { diff --git a/src/test/datascience/history.functional.test.tsx b/src/test/datascience/history.functional.test.tsx index c50427ce5e25..4ba6ed017378 100644 --- a/src/test/datascience/history.functional.test.tsx +++ b/src/test/datascience/history.functional.test.tsx @@ -51,6 +51,7 @@ suite('History output tests', () => { setup(() => { ioc = new DataScienceIocContainer(); ioc.registerDataScienceTypes(); + jupyterExecution = ioc.get(IJupyterExecution); }); function mountWebView(): ReactWrapper, React.Component> { @@ -60,7 +61,6 @@ suite('History output tests', () => { // Make sure the history provider and execution factory in the container is created (the extension does this on startup in the extension) historyProvider = ioc.get(IHistoryProvider); - jupyterExecution = ioc.get(IJupyterExecution); // The history provider create needs to be rewritten to make the history window think the mounted web panel is // ready. @@ -421,22 +421,17 @@ for _ in range(50): }); - test('Dispose test', async () => { + runMountedTest('Dispose test', async () => { // tslint:disable-next-line:no-any - if (await jupyterExecution.isNotebookSupported()) { - const history = await getOrCreateHistory(); - await history.show(); // Have to wait for the load to finish - await history.dispose(); - // tslint:disable-next-line:no-any - const h2 = await getOrCreateHistory(); - // Check equal and then dispose so the test goes away - const equal = Object.is(history, h2); - await h2.show(); - assert.ok(!equal, 'Disposing is not removing the active history'); - } else { - // tslint:disable-next-line:no-console - console.log('History test skipped, no Jupyter installed'); - } + const history = await getOrCreateHistory(); + await history.show(); // Have to wait for the load to finish + await history.dispose(); + // tslint:disable-next-line:no-any + const h2 = await getOrCreateHistory(); + // Check equal and then dispose so the test goes away + const equal = Object.is(history, h2); + await h2.show(); + assert.ok(!equal, 'Disposing is not removing the active history'); }); runMountedTest('Editor Context', async (wrapper) => { From f1a86e451716f54a5dde5b6802b1f81de7069c36 Mon Sep 17 00:00:00 2001 From: rchiodo Date: Tue, 2 Apr 2019 17:40:27 -0700 Subject: [PATCH 08/10] Add news entry --- news/1 Enhancements/4677.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/news/1 Enhancements/4677.md b/news/1 Enhancements/4677.md index 626cfd13cc0d..ce439bdfd757 100644 --- a/news/1 Enhancements/4677.md +++ b/news/1 Enhancements/4677.md @@ -1 +1 @@ -Add preliminary support for viewing dataframes. \ No newline at end of file +Add support for viewing dataframes, lists, dicts, nparrays. \ No newline at end of file From 0165fb55cb2778f1224ec5bb33cfe9073f0b20c3 Mon Sep 17 00:00:00 2001 From: rchiodo Date: Wed, 3 Apr 2019 16:24:01 -0700 Subject: [PATCH 09/10] Fix test failures --- package.nls.json | 3 ++- src/test/datascience/notebook.functional.test.ts | 13 ------------- 2 files changed, 2 insertions(+), 14 deletions(-) diff --git a/package.nls.json b/package.nls.json index 5155fa58bcac..6e6114b8604a 100644 --- a/package.nls.json +++ b/package.nls.json @@ -228,5 +228,6 @@ "DataScience.loadingMessage" : "loading ...", "DataScience.noRowsInDataViewer" : "Fetching data ...", "DataScience.pandasTooOldForViewingFormat" : "Python package 'pandas' is version {0}. Version 0.20 or greater is required for viewing data.", - "DataScience.pandasRequiredForViewing" : "Python package 'pandas' is required for viewing data." + "DataScience.pandasRequiredForViewing" : "Python package 'pandas' is required for viewing data.", + "DataScience.valuesColumn": "values" } diff --git a/src/test/datascience/notebook.functional.test.ts b/src/test/datascience/notebook.functional.test.ts index 653194e864ed..9dfaeb26b89b 100644 --- a/src/test/datascience/notebook.functional.test.ts +++ b/src/test/datascience/notebook.functional.test.ts @@ -44,7 +44,6 @@ import { ICellViewModel } from '../../datascience-ui/history-react/cell'; import { generateTestState } from '../../datascience-ui/history-react/mainPanelState'; import { sleep } from '../core'; import { DataScienceIocContainer } from './dataScienceIocContainer'; -import { SupportedCommands } from './mockJupyterManager'; import { MockJupyterSession } from './mockJupyterSession'; interface IJupyterServerInterface extends IRoleBasedObject, INotebookServer { @@ -59,15 +58,6 @@ suite('Jupyter notebook tests', () => { let ioc: DataScienceIocContainer; let modifiedConfig = false; - const workingPython: PythonInterpreter = { - path: '/foo/bar/python.exe', - version: new SemVer('3.6.6-final'), - sysVersion: '1.0.0.0', - sysPrefix: 'Python', - type: InterpreterType.Unknown, - architecture: Architecture.x64, - }; - setup(() => { ioc = new DataScienceIocContainer(); ioc.registerDataScienceTypes(); @@ -196,9 +186,6 @@ suite('Jupyter notebook tests', () => { function runTest(name: string, func: () => Promise, notebookProc?: ChildProcess) { test(name, async () => { console.log(`Starting test ${name} ...`); - if (ioc.mockJupyter) { - ioc.mockJupyter.addInterpreter(workingPython, SupportedCommands.all, undefined, notebookProc); - } if (await jupyterExecution.isNotebookSupported()) { return func(); } else { From 5b0ae995665f3d63d4511f75516679fa46241248 Mon Sep 17 00:00:00 2001 From: rchiodo Date: Wed, 3 Apr 2019 16:31:52 -0700 Subject: [PATCH 10/10] Fix linting issues --- src/test/datascience/notebook.functional.test.ts | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/src/test/datascience/notebook.functional.test.ts b/src/test/datascience/notebook.functional.test.ts index 9dfaeb26b89b..ed60691484d8 100644 --- a/src/test/datascience/notebook.functional.test.ts +++ b/src/test/datascience/notebook.functional.test.ts @@ -7,7 +7,6 @@ import { ChildProcess } from 'child_process'; import * as fs from 'fs-extra'; import * as os from 'os'; import * as path from 'path'; -import { SemVer } from 'semver'; import { Readable, Writable } from 'stream'; import * as uuid from 'uuid/v4'; import { Disposable, Uri } from 'vscode'; @@ -19,7 +18,6 @@ import { IFileSystem } from '../../client/common/platform/types'; import { IProcessServiceFactory, Output } from '../../client/common/process/types'; import { createDeferred } from '../../client/common/utils/async'; import { noop } from '../../client/common/utils/misc'; -import { Architecture } from '../../client/common/utils/platform'; import { concatMultilineString } from '../../client/datascience/common'; import { JupyterExecutionFactory } from '../../client/datascience/jupyter/jupyterExecutionFactory'; import { IRoleBasedObject, RoleBasedFactory } from '../../client/datascience/jupyter/liveshare/roleBasedFactory'; @@ -36,7 +34,6 @@ import { import { IInterpreterService, IKnownSearchPathsForInterpreters, - InterpreterType, PythonInterpreter } from '../../client/interpreter/contracts'; import { ClassType } from '../../client/ioc/types'; @@ -183,7 +180,7 @@ suite('Jupyter notebook tests', () => { }); } - function runTest(name: string, func: () => Promise, notebookProc?: ChildProcess) { + function runTest(name: string, func: () => Promise, _notebookProc?: ChildProcess) { test(name, async () => { console.log(`Starting test ${name} ...`); if (await jupyterExecution.isNotebookSupported()) {