Skip to content

Commit aaca611

Browse files
authored
Port prune fix from main to release (#14243)
* Remove unneeded cell keys when exporting (#14241) * Remove transient output when exporting from the interactive window * Add news entry * Update changelog
1 parent f076311 commit aaca611

File tree

6 files changed

+85
-52
lines changed

6 files changed

+85
-52
lines changed

news/2 Fixes/14213.md

Lines changed: 0 additions & 1 deletion
This file was deleted.

src/client/datascience/common.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,7 @@ const dummyExecuteResultObj: nbformat.IExecuteResult = {
3838
data: {},
3939
metadata: {}
4040
};
41-
const AllowedKeys = {
41+
export const AllowedCellOutputKeys = {
4242
['stream']: new Set(Object.keys(dummyStreamObj)),
4343
['error']: new Set(Object.keys(dummyErrorObj)),
4444
['display_data']: new Set(Object.keys(dummyDisplayObj)),
@@ -73,7 +73,7 @@ function fixupOutput(output: nbformat.IOutput): nbformat.IOutput {
7373
case 'error':
7474
case 'execute_result':
7575
case 'display_data':
76-
allowedKeys = AllowedKeys[output.output_type];
76+
allowedKeys = AllowedCellOutputKeys[output.output_type];
7777
break;
7878
default:
7979
return output;

src/client/datascience/jupyter/jupyterExporter.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ import { IConfigurationService } from '../../common/types';
1717
import * as localize from '../../common/utils/localize';
1818
import { noop } from '../../common/utils/misc';
1919
import { CellMatcher } from '../cellMatcher';
20+
import { pruneCell } from '../common';
2021
import { CodeSnippets, Identifiers } from '../constants';
2122
import {
2223
CellState,
@@ -235,9 +236,11 @@ export class JupyterExporter implements INotebookExporter {
235236
};
236237

237238
private pruneCell = (cell: ICell, cellMatcher: CellMatcher): nbformat.IBaseCell => {
239+
// Prune with the common pruning function first.
240+
const copy = pruneCell({ ...cell.data });
241+
238242
// Remove the #%% of the top of the source if there is any. We don't need
239243
// this to end up in the exported ipynb file.
240-
const copy = { ...cell.data };
241244
copy.source = this.pruneSource(cell.data.source, cellMatcher);
242245
return copy;
243246
};

src/test/datascience/interactiveWindow.functional.test.tsx

Lines changed: 74 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import * as path from 'path';
99
import * as TypeMoq from 'typemoq';
1010
import { Disposable, Memento, Selection, TextDocument, TextEditor, Uri } from 'vscode';
1111

12+
import { nbformat } from '@jupyterlab/coreutils';
1213
import { ReactWrapper } from 'enzyme';
1314
import { anything, when } from 'ts-mockito';
1415
import { IApplicationShell, IDocumentManager } from '../../client/common/application/types';
@@ -17,11 +18,12 @@ import { createDeferred, sleep, waitForPromise } from '../../client/common/utils
1718
import { noop } from '../../client/common/utils/misc';
1819
import { EXTENSION_ROOT_DIR } from '../../client/constants';
1920
import { generateCellsFromDocument } from '../../client/datascience/cellFactory';
21+
import { AllowedCellOutputKeys } from '../../client/datascience/common';
2022
import { EditorContexts } from '../../client/datascience/constants';
2123
import { InteractiveWindowMessages } from '../../client/datascience/interactive-common/interactiveWindowTypes';
2224
import { InteractiveWindow } from '../../client/datascience/interactive-window/interactiveWindow';
2325
import { AskedForPerFileSettingKey } from '../../client/datascience/interactive-window/interactiveWindowProvider';
24-
import { IInteractiveWindowProvider } from '../../client/datascience/types';
26+
import { IDataScienceFileSystem, IInteractiveWindowProvider } from '../../client/datascience/types';
2527
import { IInterpreterService } from '../../client/interpreter/contracts';
2628
import { concatMultilineString } from '../../datascience-ui/common';
2729
import { InteractivePanel } from '../../datascience-ui/history-react/interactivePanel';
@@ -635,53 +637,81 @@ for i in range(0, 100):
635637
return;
636638
}
637639
};
638-
let exportCalled = false;
639-
const appShell = TypeMoq.Mock.ofType<IApplicationShell>();
640-
appShell
641-
.setup((a) => a.showErrorMessage(TypeMoq.It.isAnyString()))
642-
.returns((e) => {
643-
throw e;
644-
});
645-
appShell
646-
.setup((a) => a.showInformationMessage(TypeMoq.It.isAny(), TypeMoq.It.isAny()))
647-
.returns(() => Promise.resolve(''));
648-
appShell
649-
.setup((a) => a.showSaveDialog(TypeMoq.It.isAny()))
650-
.returns(() => {
651-
exportCalled = true;
652-
return Promise.resolve(undefined);
653-
});
654-
appShell.setup((a) => a.setStatusBarMessage(TypeMoq.It.isAny())).returns(() => dummyDisposable);
655-
ioc.serviceManager.rebindInstance<IApplicationShell>(IApplicationShell, appShell.object);
656-
657-
// Make sure to create the interactive window after the rebind or it gets the wrong application shell.
658-
await addCode(ioc, 'a=1\na');
659-
const { window, mount } = await getOrCreateInteractiveWindow(ioc);
660-
661-
// Export should cause exportCalled to change to true
662-
const exportPromise = mount.waitForMessage(InteractiveWindowMessages.ReturnAllCells);
663-
window.exportCells();
664-
await exportPromise;
665-
await sleep(100); // Give time for appshell to come up
666-
assert.equal(exportCalled, true, 'Export is not being called during export');
640+
const dsfs = ioc.get<IDataScienceFileSystem>(IDataScienceFileSystem);
641+
const tf = await dsfs.createTemporaryLocalFile('.ipynb');
642+
try {
643+
let exportCalled = false;
644+
const appShell = TypeMoq.Mock.ofType<IApplicationShell>();
645+
appShell
646+
.setup((a) => a.showErrorMessage(TypeMoq.It.isAnyString()))
647+
.returns((e) => {
648+
throw e;
649+
});
650+
appShell
651+
.setup((a) => a.showInformationMessage(TypeMoq.It.isAny(), TypeMoq.It.isAny()))
652+
.returns(() => Promise.resolve(''));
653+
appShell
654+
.setup((a) => a.showSaveDialog(TypeMoq.It.isAny()))
655+
.returns(() => {
656+
exportCalled = true;
657+
return Promise.resolve(Uri.file(tf.filePath));
658+
});
659+
appShell.setup((a) => a.setStatusBarMessage(TypeMoq.It.isAny())).returns(() => dummyDisposable);
660+
ioc.serviceManager.rebindInstance<IApplicationShell>(IApplicationShell, appShell.object);
661+
const exportCode = `
662+
for i in range(100):
663+
time.sleep(0.1)
664+
raise Exception('test')
665+
`;
667666

668-
// Remove the cell
669-
const exportButton = findButton(mount.wrapper, InteractivePanel, 6);
670-
const undo = findButton(mount.wrapper, InteractivePanel, 2);
667+
// Make sure to create the interactive window after the rebind or it gets the wrong application shell.
668+
addMockData(ioc, exportCode, 'NameError', 'type/error', 'error', [
669+
'\u001b[1;31m---------------------------------------------------------------------------\u001b[0m',
670+
'\u001b[1;31mNameError\u001b[0m Traceback (most recent call last)',
671+
"\u001b[1;32md:\\Source\\Testing_3\\manualTestFile.py\u001b[0m in \u001b[0;36m<module>\u001b[1;34m\u001b[0m\n\u001b[0;32m 1\u001b[0m \u001b[1;32mfor\u001b[0m \u001b[0mi\u001b[0m \u001b[1;32min\u001b[0m \u001b[0mrange\u001b[0m\u001b[1;33m(\u001b[0m\u001b[1;36m100\u001b[0m\u001b[1;33m)\u001b[0m\u001b[1;33m:\u001b[0m\u001b[1;33m\u001b[0m\u001b[1;33m\u001b[0m\u001b[0m\n\u001b[1;32m----> 2\u001b[1;33m \u001b[0mtime\u001b[0m\u001b[1;33m.\u001b[0m\u001b[0msleep\u001b[0m\u001b[1;33m(\u001b[0m\u001b[1;36m0.1\u001b[0m\u001b[1;33m)\u001b[0m\u001b[1;33m\u001b[0m\u001b[1;33m\u001b[0m\u001b[0m\n\u001b[0m\u001b[0;32m 3\u001b[0m \u001b[1;32mraise\u001b[0m \u001b[0mException\u001b[0m\u001b[1;33m(\u001b[0m\u001b[1;34m'test'\u001b[0m\u001b[1;33m)\u001b[0m\u001b[1;33m\u001b[0m\u001b[1;33m\u001b[0m\u001b[0m\n",
672+
"\u001b[1;31mNameError\u001b[0m: name 'time' is not defined"
673+
]);
674+
await addCode(ioc, exportCode);
675+
const { window, mount } = await getOrCreateInteractiveWindow(ioc);
671676

672-
// Now verify if we undo, we have no cells
673-
const afterUndo = await getInteractiveCellResults(ioc, () => {
674-
undo!.simulate('click');
675-
return Promise.resolve();
676-
});
677+
// Export should cause exportCalled to change to true
678+
const exportPromise = mount.waitForMessage(InteractiveWindowMessages.ReturnAllCells);
679+
window.exportCells();
680+
await exportPromise;
681+
await sleep(100); // Give time for appshell to come up
682+
assert.equal(exportCalled, true, 'Export is not being called during export');
683+
684+
// Read file contents into a jupyter structure. Make sure we have only the expected values
685+
const contents = await dsfs.readLocalFile(tf.filePath);
686+
const struct = JSON.parse(contents) as nbformat.INotebookContent;
687+
assert.strictEqual(struct.cells.length, 1, 'Wrong number of cells');
688+
const outputs = struct.cells[0].outputs as nbformat.IOutput[];
689+
assert.strictEqual(outputs.length, 1, 'Not correct number of outputs');
690+
assert.strictEqual(outputs[0].output_type, 'error', 'Error not found');
691+
const allowedKeys = [...AllowedCellOutputKeys.error];
692+
const actualKeys = Object.keys(outputs[0]);
693+
assert.deepStrictEqual(allowedKeys, actualKeys, 'Invalid keys in output');
694+
695+
// Remove the cell
696+
const exportButton = findButton(mount.wrapper, InteractivePanel, 6);
697+
const undo = findButton(mount.wrapper, InteractivePanel, 2);
698+
699+
// Now verify if we undo, we have no cells
700+
const afterUndo = await getInteractiveCellResults(ioc, () => {
701+
undo!.simulate('click');
702+
return Promise.resolve();
703+
});
677704

678-
assert.equal(afterUndo.length, 1, 'Undo should remove cells');
705+
assert.equal(afterUndo.length, 1, 'Undo should remove cells');
679706

680-
// Then verify we cannot click the button (it should be disabled)
681-
exportCalled = false;
682-
exportButton!.simulate('click');
683-
await sleep(100);
684-
assert.equal(exportCalled, false, 'Export should not be called when no cells visible');
707+
// Then verify we cannot click the button (it should be disabled)
708+
exportCalled = false;
709+
exportButton!.simulate('click');
710+
await sleep(100);
711+
assert.equal(exportCalled, false, 'Export should not be called when no cells visible');
712+
} finally {
713+
tf.dispose();
714+
}
685715
},
686716
() => {
687717
return ioc;

src/test/datascience/mockJupyterManager.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -289,13 +289,13 @@ export class MockJupyterManager implements IJupyterSessionManager {
289289
this.makeActive(interpreter);
290290
}
291291

292-
public addError(code: string, message: string) {
292+
public addError(code: string, message: string, traceback?: string[]) {
293293
// Turn the message into an nbformat.IError
294294
const result: nbformat.IError = {
295295
output_type: 'error',
296296
ename: message,
297297
evalue: message,
298-
traceback: [message]
298+
traceback: traceback ? traceback : [message]
299299
};
300300

301301
this.addCell(code, result);

src/test/datascience/testHelpers.tsx

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -89,11 +89,12 @@ export function addMockData(
8989
code: string,
9090
result: string | number | undefined | string[],
9191
mimeType?: string | string[],
92-
cellType?: string
92+
cellType?: string,
93+
traceback?: string[]
9394
) {
9495
if (ioc.mockJupyter) {
9596
if (cellType && cellType === 'error') {
96-
ioc.mockJupyter.addError(code, result ? result.toString() : '');
97+
ioc.mockJupyter.addError(code, result ? result.toString() : '', traceback);
9798
} else {
9899
if (result) {
99100
ioc.mockJupyter.addCell(code, result, mimeType);

0 commit comments

Comments
 (0)