Skip to content

Commit e121d5a

Browse files
authored
Refactored exporting notebooks to python script (#12232)
* some work done * linking done * quickpick menu working * added command pallete commands * moved to commandRegister * started work on file saving * file saving started * some work done * linking done * quickpick menu working * added command pallete commands * moved to commandRegister * started work on file saving * file saving started * handled some saving edge cases * moved to new file * more work * started on exportbase * saving almost done * removed save prompt * fixed * working in new files * changes to export base * small fixes * export to python workin * added tests * removed unused injections * started work on dependency checking * cleaned up * removed broken lines * attempt to fix failing tests * attempt to fix failing tests 2 * attempt to fix failing tests 3 * unmodified launch.json * created export commands * refactoring and working on dependency checks * split functionallity into multiple classes * hopefully fixed tests * almost done file opening for python * cleaned up * made test * removed notebookeditorprovider as dependency * fixed small bugs * fixed small bugs * removed sleep * fixed default file name * fixed filename when saving * added instructions for adding new export method * added missing lines to package.nls.json * python export behaviour changed to original * fixed test * added readme instructions * fixed test * fixed other test * fixed lint * fixed one test * fixed final test and made export command hidden * moved type defs into types.ts * localized text * added types file * added localized test to package.nls.json
1 parent e28a393 commit e121d5a

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

42 files changed

+648
-58
lines changed

.vscode/launch.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -204,7 +204,7 @@
204204
"env": {
205205
"VSC_PYTHON_CI_TEST_GREP": "", // Modify this to run a subset of the single workspace tests
206206
"VSC_PYTHON_CI_TEST_INVERT_GREP": "", // Initialize this to invert the grep (exclude tests with value defined in grep).
207-
207+
208208
"VSC_PYTHON_LOAD_EXPERIMENTS_FROM_FILE": "true",
209209
"TEST_FILES_SUFFIX": "ds.test"
210210
},

package.json

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,9 @@
5959
"onCommand:python.viewTestOutput",
6060
"onCommand:python.viewOutput",
6161
"onCommand:python.datascience.viewJupyterOutput",
62+
"onCommand:python.datascience.exportAsPythonScript",
63+
"onCommand:python.datascience.exportToHTML",
64+
"onCommand:python.datascience.exportToPDF",
6265
"onCommand:python.selectAndRunTestMethod",
6366
"onCommand:python.selectAndDebugTestMethod",
6467
"onCommand:python.selectAndRunTestFile",
@@ -352,6 +355,11 @@
352355
"title": "%python.command.python.datascience.viewJupyterOutput.title%",
353356
"category": "Python"
354357
},
358+
{
359+
"command": "python.datascience.exportAsPythonScript",
360+
"title": "%python.command.python.datascience.exportAsPythonScript.title%",
361+
"category": "Python"
362+
},
355363
{
356364
"command": "python.datascience.selectJupyterInterpreter",
357365
"title": "%python.command.python.datascience.selectJupyterInterpreter.title%",
@@ -844,6 +852,12 @@
844852
}
845853
],
846854
"commandPalette": [
855+
{
856+
"command": "python.datascience.exportAsPythonScript",
857+
"title": "%python.command.python.datascience.exportAsPythonScript.title%",
858+
"category": "Python",
859+
"when": "python.datascience.isnativeactive && python.datascience.featureenabled"
860+
},
847861
{
848862
"command": "python.switchOffInsidersChannel",
849863
"title": "%python.command.python.switchOffInsidersChannel.title%",

package.nls.json

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,9 @@
1717
"python.command.python.viewOutput.title": "Show Output",
1818
"python.command.python.viewTestOutput.title": "Show Test Output",
1919
"python.command.python.datascience.viewJupyterOutput.title": "Show Jupyter Output",
20+
"python.command.python.datascience.exportAsPythonScript.title": "Export as Python Script",
21+
"python.command.python.datascience.exportToHTML.title": "Export to HTML",
22+
"python.command.python.datascience.exportToPDF.title": "Export to PDF",
2023
"python.command.python.viewLanguageServerOutput.title": "Show Language Server Output",
2124
"python.command.python.selectAndRunTestMethod.title": "Run Test Method ...",
2225
"python.command.python.selectAndDebugTestMethod.title": "Debug Test Method ...",
@@ -107,6 +110,9 @@
107110
"DataScience.exportDialogFailed": "Failed to export notebook. {0}",
108111
"DataScience.exportOpenQuestion": "Open in browser",
109112
"DataScience.exportOpenQuestion1": "Open in editor",
113+
"DataScience.notebookExportAs" : "Convert and save to a python script",
114+
"DataScience.exportAsQuickPickPlaceholder" : "Export As...",
115+
"DataScience.exportPythonQuickPickLabel" : "Python Script",
110116
"DataScience.collapseInputTooltip": "Collapse input block",
111117
"DataScience.collapseVariableExplorerTooltip": "Hide variables active in jupyter kernel",
112118
"DataScience.collapseVariableExplorerLabel": "Variables",

src/client/common/application/commands.ts

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66
import { CancellationToken, Position, TextDocument, Uri } from 'vscode';
77
import { Commands as LSCommands } from '../../activation/languageServer/constants';
88
import { Commands as DSCommands } from '../../datascience/constants';
9-
import { INotebook } from '../../datascience/types';
9+
import { INotebook, INotebookModel } from '../../datascience/types';
1010
import { CommandSource } from '../../testing/common/constants';
1111
import { TestFunction, TestsToRun } from '../../testing/common/types';
1212
import { TestDataItem, TestWorkspaceFolder } from '../../testing/types';
@@ -168,6 +168,10 @@ export interface ICommandNameArgumentTypeMapping extends ICommandNameWithoutArgu
168168
[DSCommands.RunCurrentCellAndAddBelow]: [string];
169169
[DSCommands.ScrollToCell]: [string, string];
170170
[DSCommands.ViewJupyterOutput]: [];
171+
[DSCommands.ExportAsPythonScript]: [INotebookModel];
172+
[DSCommands.ExportToHTML]: [INotebookModel];
173+
[DSCommands.ExportToPDF]: [INotebookModel];
174+
[DSCommands.Export]: [INotebookModel];
171175
[DSCommands.SwitchJupyterKernel]: [INotebook | undefined, 'raw' | 'jupyter'];
172176
[DSCommands.SelectJupyterCommandLine]: [undefined | Uri];
173177
[DSCommands.SaveNotebookNonCustomEditor]: [Uri];

src/client/common/utils/localize.ts

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -560,6 +560,7 @@ export namespace DataScience {
560560
export const clearAllOutput = localize('DataScience.clearAllOutput', 'Clear All Output');
561561
export const interruptKernelStatus = localize('DataScience.interruptKernelStatus', 'Interrupting IPython Kernel');
562562
export const exportCancel = localize('DataScience.exportCancel', 'Cancel');
563+
export const exportPythonQuickPickLabel = localize('DataScience.exportPythonQuickPickLabel', 'Python Script');
563564
export const restartKernelAfterInterruptMessage = localize(
564565
'DataScience.restartKernelAfterInterruptMessage',
565566
'Interrupting the kernel timed out. Do you want to restart the kernel instead? All variables will be lost.'
@@ -764,11 +765,9 @@ export namespace DataScience {
764765
'DataScience.remoteDebuggerNotSupported',
765766
'Debugging while attached to a remote server is not currently supported.'
766767
);
767-
export const exportAsPythonFileTooltip = localize(
768-
'DataScience.exportAsPythonFileTooltip',
769-
'Convert and save to a python script'
770-
);
768+
export const notebookExportAs = localize('DataScience.notebookExportAs', 'Convert and save to a python script');
771769
export const exportAsPythonFileTitle = localize('DataScience.exportAsPythonFileTitle', 'Save As Python File');
770+
export const exportAsQuickPickPlaceholder = localize('DataScience.exportAsQuickPickPlaceholder', 'Export As...');
772771
export const runCell = localize('DataScience.runCell', 'Run cell');
773772
export const deleteCell = localize('DataScience.deleteCell', 'Delete cell');
774773
export const moveCellUp = localize('DataScience.moveCellUp', 'Move cell up');

src/client/datascience/commands/commandRegistry.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ import {
2121
INotebookEditorProvider
2222
} from '../types';
2323
import { JupyterCommandLineSelectorCommand } from './commandLineSelector';
24+
import { ExportCommands } from './exportCommands';
2425
import { KernelSwitcherCommand } from './kernelSwitcher';
2526
import { JupyterServerSelectorCommand } from './serverSelector';
2627

@@ -44,7 +45,8 @@ export class CommandRegistry implements IDisposable {
4445
@inject(IApplicationShell) private appShell: IApplicationShell,
4546
@inject(IOutputChannel) @named(JUPYTER_OUTPUT_CHANNEL) private jupyterOutput: IOutputChannel,
4647
@inject(IStartPage) private startPage: IStartPage,
47-
@inject(IExperimentService) private readonly expService: IExperimentService
48+
@inject(IExperimentService) private readonly expService: IExperimentService,
49+
@inject(ExportCommands) private readonly exportCommand: ExportCommands
4850
) {
4951
this.disposables.push(this.serverSelectedCommand);
5052
this.disposables.push(this.kernelSwitcherCommand);
@@ -53,6 +55,7 @@ export class CommandRegistry implements IDisposable {
5355
this.commandLineCommand.register();
5456
this.serverSelectedCommand.register();
5557
this.kernelSwitcherCommand.register();
58+
this.exportCommand.register();
5659
this.registerCommand(Commands.RunAllCells, this.runAllCells);
5760
this.registerCommand(Commands.RunCell, this.runCell);
5861
this.registerCommand(Commands.RunCurrentCell, this.runCurrentCell);
Lines changed: 98 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,98 @@
1+
// Copyright (c) Microsoft Corporation. All rights reserved.
2+
// Licensed under the MIT License.
3+
4+
'use strict';
5+
6+
import { inject, injectable } from 'inversify';
7+
import { QuickPickItem, QuickPickOptions } from 'vscode';
8+
import { getLocString } from '../../../datascience-ui/react-common/locReactSide';
9+
import { ICommandNameArgumentTypeMapping } from '../../common/application/commands';
10+
import { IApplicationShell, ICommandManager } from '../../common/application/types';
11+
import { IDisposable } from '../../common/types';
12+
import { DataScience } from '../../common/utils/localize';
13+
import { Commands } from '../constants';
14+
import { ExportManager } from '../export/exportManager';
15+
import { ExportFormat, IExportManager } from '../export/types';
16+
import { INotebookEditorProvider, INotebookModel } from '../types';
17+
18+
interface IExportQuickPickItem extends QuickPickItem {
19+
handler(): void;
20+
}
21+
22+
@injectable()
23+
export class ExportCommands implements IDisposable {
24+
private readonly disposables: IDisposable[] = [];
25+
constructor(
26+
@inject(ICommandManager) private readonly commandManager: ICommandManager,
27+
@inject(IExportManager) private exportManager: ExportManager,
28+
@inject(IApplicationShell) private readonly applicationShell: IApplicationShell,
29+
@inject(INotebookEditorProvider) private readonly notebookProvider: INotebookEditorProvider
30+
) {}
31+
public register() {
32+
this.registerCommand(Commands.ExportAsPythonScript, (model) => this.export(model, ExportFormat.python));
33+
this.registerCommand(Commands.ExportToHTML, (model) => this.export(model, ExportFormat.html));
34+
this.registerCommand(Commands.ExportToPDF, (model) => this.export(model, ExportFormat.pdf));
35+
this.registerCommand(Commands.Export, (model) => this.export(model));
36+
}
37+
38+
public dispose() {
39+
this.disposables.forEach((d) => d.dispose());
40+
}
41+
42+
private registerCommand<
43+
E extends keyof ICommandNameArgumentTypeMapping,
44+
U extends ICommandNameArgumentTypeMapping[E]
45+
// tslint:disable-next-line: no-any
46+
>(command: E, callback: (...args: U) => any) {
47+
const disposable = this.commandManager.registerCommand(command, callback, this);
48+
this.disposables.push(disposable);
49+
}
50+
51+
private async export(model: INotebookModel, exportMethod?: ExportFormat) {
52+
if (!model) {
53+
// if no model was passed then this was called from the command pallete,
54+
// so we need to get the active editor
55+
const activeEditor = this.notebookProvider.activeEditor;
56+
if (!activeEditor || !activeEditor.model) {
57+
return;
58+
}
59+
model = activeEditor.model;
60+
}
61+
62+
if (exportMethod) {
63+
await this.exportManager.export(exportMethod, model);
64+
} else {
65+
// if we don't have an export method we need to ask for one and display the
66+
// quickpick menu
67+
const pickedItem = await this.showExportQuickPickMenu(model).then((item) => item);
68+
if (pickedItem !== undefined) {
69+
pickedItem.handler();
70+
}
71+
}
72+
}
73+
74+
private getExportQuickPickItems(model: INotebookModel): IExportQuickPickItem[] {
75+
return [
76+
{
77+
label: DataScience.exportPythonQuickPickLabel(),
78+
picked: true,
79+
handler: () => this.commandManager.executeCommand(Commands.ExportAsPythonScript, model)
80+
}
81+
//{ label: 'HTML', picked: false, handler: () => this.commandManager.executeCommand(Commands.ExportToHTML) },
82+
//{ label: 'PDF', picked: false, handler: () => this.commandManager.executeCommand(Commands.ExportToPDF) }
83+
];
84+
}
85+
86+
private async showExportQuickPickMenu(model: INotebookModel): Promise<IExportQuickPickItem | undefined> {
87+
const items = this.getExportQuickPickItems(model);
88+
89+
const options: QuickPickOptions = {
90+
ignoreFocusOut: false,
91+
matchOnDescription: true,
92+
matchOnDetail: true,
93+
placeHolder: getLocString('DataScience.exportAsQuickPickPlaceholder', 'Export As...')
94+
};
95+
96+
return this.applicationShell.showQuickPick(items, options);
97+
}
98+
}

src/client/datascience/constants.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -79,6 +79,10 @@ export namespace Commands {
7979
export const ScrollToCell = 'python.datascience.scrolltocell';
8080
export const CreateNewNotebook = 'python.datascience.createnewnotebook';
8181
export const ViewJupyterOutput = 'python.datascience.viewJupyterOutput';
82+
export const ExportAsPythonScript = 'python.datascience.exportAsPythonScript';
83+
export const ExportToHTML = 'python.datascience.exportToHTML';
84+
export const ExportToPDF = 'python.datascience.exportToPDF';
85+
export const Export = 'python.datascience.export';
8286
export const SaveNotebookNonCustomEditor = 'python.datascience.notebookeditor.save';
8387
export const SaveAsNotebookNonCustomEditor = 'python.datascience.notebookeditor.saveAs';
8488
export const OpenNotebookNonCustomEditor = 'python.datascience.notebookeditor.open';
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
2+
## TO ADD A NEW EXPORT METHOD
3+
1. Create a new command in src/client/datascience/constants
4+
2. Register the command in this file
5+
3. Add an item to the quick pick menu for your new export method from inside the getExportQuickPickItems() method (in this file).
6+
4. Add a new command to the command pallete in package.json (optional)
7+
5. Declare and add your file extensions inside exportManagerFilePicker
8+
6. Declare and add your export method inside exportManager
9+
7. Create an injectable class that implements IExport and register it in src/client/datascience/serviceRegistry
10+
8. Implement the export method on your new class
11+
9. Inject the class inside exportManager
12+
10. Add a case for your new export method and call the export method of your new class with the appropriate arguments
13+
11. Add telementry and status messages
Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
import { inject, injectable, named } from 'inversify';
2+
import { Uri } from 'vscode';
3+
import { IFileSystem, TemporaryFile } from '../../common/platform/types';
4+
import { IDataScienceErrorHandler, INotebookModel } from '../types';
5+
import { IExportManagerFilePicker } from './exportManagerFilePicker';
6+
import { ExportFormat, IExport, IExportManager } from './types';
7+
8+
@injectable()
9+
export class ExportManager implements IExportManager {
10+
constructor(
11+
@inject(IExport) @named(ExportFormat.pdf) private readonly exportToPDF: IExport,
12+
@inject(IExport) @named(ExportFormat.html) private readonly exportToHTML: IExport,
13+
@inject(IExport) @named(ExportFormat.python) private readonly exportToPython: IExport,
14+
@inject(IFileSystem) private readonly fileSystem: IFileSystem,
15+
@inject(IDataScienceErrorHandler) private readonly errorHandler: IDataScienceErrorHandler,
16+
@inject(IExportManagerFilePicker) private readonly filePicker: IExportManagerFilePicker
17+
) {}
18+
19+
public async export(format: ExportFormat, model: INotebookModel): Promise<Uri | undefined> {
20+
// need to add telementry
21+
let target;
22+
if (format !== ExportFormat.python) {
23+
target = await this.filePicker.getExportFileLocation(format, model.file);
24+
if (!target) {
25+
return;
26+
}
27+
} else {
28+
target = Uri.file((await this.fileSystem.createTemporaryFile('.py')).filePath);
29+
}
30+
31+
const tempFile = await this.makeTemporaryFile(model);
32+
if (!tempFile) {
33+
return; // error making temp file
34+
}
35+
36+
const source = Uri.file(tempFile.filePath);
37+
try {
38+
switch (format) {
39+
case ExportFormat.python:
40+
await this.exportToPython.export(source, target);
41+
break;
42+
43+
case ExportFormat.pdf:
44+
await this.exportToPDF.export(source, target);
45+
break;
46+
47+
case ExportFormat.html:
48+
await this.exportToHTML.export(source, target);
49+
break;
50+
default:
51+
break;
52+
}
53+
} finally {
54+
tempFile.dispose();
55+
}
56+
57+
return target;
58+
}
59+
60+
private async makeTemporaryFile(model: INotebookModel): Promise<TemporaryFile | undefined> {
61+
let tempFile: TemporaryFile | undefined;
62+
try {
63+
tempFile = await this.fileSystem.createTemporaryFile('.ipynb');
64+
const content = model ? model.getContent() : '';
65+
await this.fileSystem.writeFile(tempFile.filePath, content, 'utf-8');
66+
} catch (e) {
67+
await this.errorHandler.handleError(e);
68+
}
69+
70+
return tempFile;
71+
}
72+
}
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
import { inject, injectable } from 'inversify';
2+
import { Uri } from 'vscode';
3+
import * as localize from '../../common/utils/localize';
4+
import { IJupyterExecution, IJupyterInterpreterDependencyManager, INotebookModel } from '../types';
5+
import { ExportManager } from './exportManager';
6+
import { ExportFormat, IExportManager } from './types';
7+
8+
@injectable()
9+
export class ExportManagerDependencyChecker implements IExportManager {
10+
constructor(
11+
@inject(ExportManager) private readonly manager: IExportManager,
12+
@inject(IJupyterExecution) private jupyterExecution: IJupyterExecution,
13+
@inject(IJupyterInterpreterDependencyManager)
14+
private readonly dependencyManager: IJupyterInterpreterDependencyManager
15+
) {}
16+
17+
public async export(format: ExportFormat, model: INotebookModel): Promise<Uri | undefined> {
18+
// Before we try the import, see if we don't support it, if we don't give a chance to install dependencies
19+
if (!(await this.jupyterExecution.isImportSupported())) {
20+
await this.dependencyManager.installMissingDependencies();
21+
}
22+
23+
if (await this.jupyterExecution.isImportSupported()) {
24+
return this.manager.export(format, model);
25+
} else {
26+
throw new Error(localize.DataScience.jupyterNbConvertNotSupported());
27+
}
28+
}
29+
}

0 commit comments

Comments
 (0)