Skip to content

Refactored exporting notebooks to python script #12232

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 61 commits into from
Jun 15, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
61 commits
Select commit Hold shift + click to select a range
6c95bed
some work done
techwithtim Jun 3, 2020
b1b30f6
linking done
techwithtim Jun 3, 2020
6e0701a
quickpick menu working
techwithtim Jun 3, 2020
016e1ca
added command pallete commands
techwithtim Jun 3, 2020
ede2b2a
moved to commandRegister
techwithtim Jun 4, 2020
ae7b713
started work on file saving
techwithtim Jun 4, 2020
1029564
file saving started
techwithtim Jun 4, 2020
4643c48
some work done
techwithtim Jun 3, 2020
c971224
linking done
techwithtim Jun 3, 2020
d1ccc13
quickpick menu working
techwithtim Jun 3, 2020
9c2acb7
added command pallete commands
techwithtim Jun 3, 2020
13bb1d6
moved to commandRegister
techwithtim Jun 4, 2020
bb14f2b
started work on file saving
techwithtim Jun 4, 2020
24c6bf2
file saving started
techwithtim Jun 4, 2020
e54d481
handled some saving edge cases
techwithtim Jun 5, 2020
0167dee
moved to new file
techwithtim Jun 5, 2020
c2df552
more work
techwithtim Jun 5, 2020
ac1128b
started on exportbase
techwithtim Jun 5, 2020
424b7fc
saving almost done
techwithtim Jun 8, 2020
e256536
removed save prompt
techwithtim Jun 8, 2020
1c364d7
fixed
techwithtim Jun 8, 2020
b907076
working in new files
techwithtim Jun 8, 2020
8af6d5f
changes to export base
techwithtim Jun 8, 2020
b4a6257
small fixes
techwithtim Jun 8, 2020
55d5e66
export to python workin
techwithtim Jun 9, 2020
4b18eac
added tests
techwithtim Jun 9, 2020
918ee21
removed unused injections
techwithtim Jun 9, 2020
2dd5d8d
started work on dependency checking
techwithtim Jun 9, 2020
5794d2b
cleaned up
techwithtim Jun 9, 2020
7d31944
removed broken lines
techwithtim Jun 9, 2020
d059cdc
attempt to fix failing tests
techwithtim Jun 9, 2020
a398a42
attempt to fix failing tests 2
techwithtim Jun 9, 2020
95c74c0
attempt to fix failing tests 3
techwithtim Jun 9, 2020
5f8e4f7
unmodified launch.json
techwithtim Jun 9, 2020
1a9f542
created export commands
techwithtim Jun 10, 2020
e98d530
refactoring and working on dependency checks
techwithtim Jun 10, 2020
8e49409
split functionallity into multiple classes
techwithtim Jun 10, 2020
0d172bb
hopefully fixed tests
techwithtim Jun 10, 2020
42ead55
almost done file opening for python
techwithtim Jun 11, 2020
0b67685
cleaned up
techwithtim Jun 11, 2020
0f2c25b
made test
techwithtim Jun 11, 2020
027f1c1
removed notebookeditorprovider as dependency
techwithtim Jun 11, 2020
1ad5056
fixed small bugs
techwithtim Jun 11, 2020
b14dcf8
fixed small bugs
techwithtim Jun 11, 2020
7cd6e5c
removed sleep
techwithtim Jun 11, 2020
73cb156
fixed default file name
techwithtim Jun 12, 2020
1e41928
fixed filename when saving
techwithtim Jun 12, 2020
5a3140a
added instructions for adding new export method
techwithtim Jun 12, 2020
49d6031
added missing lines to package.nls.json
techwithtim Jun 12, 2020
859656d
python export behaviour changed to original
techwithtim Jun 12, 2020
3d521b0
fixed test
techwithtim Jun 12, 2020
ad62e9a
added readme instructions
techwithtim Jun 12, 2020
8fc4ca7
fixed test
techwithtim Jun 12, 2020
a3db99d
fixed other test
techwithtim Jun 12, 2020
62f981e
fixed lint
techwithtim Jun 12, 2020
6e95623
fixed one test
techwithtim Jun 15, 2020
6fa3d7a
fixed final test and made export command hidden
techwithtim Jun 15, 2020
b653338
moved type defs into types.ts
techwithtim Jun 15, 2020
c917924
localized text
techwithtim Jun 15, 2020
df0fcb4
added types file
techwithtim Jun 15, 2020
a302609
added localized test to package.nls.json
techwithtim Jun 15, 2020
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .vscode/launch.json
Original file line number Diff line number Diff line change
Expand Up @@ -204,7 +204,7 @@
"env": {
"VSC_PYTHON_CI_TEST_GREP": "", // Modify this to run a subset of the single workspace tests
"VSC_PYTHON_CI_TEST_INVERT_GREP": "", // Initialize this to invert the grep (exclude tests with value defined in grep).

"VSC_PYTHON_LOAD_EXPERIMENTS_FROM_FILE": "true",
"TEST_FILES_SUFFIX": "ds.test"
},
Expand Down
14 changes: 14 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,9 @@
"onCommand:python.viewTestOutput",
"onCommand:python.viewOutput",
"onCommand:python.datascience.viewJupyterOutput",
"onCommand:python.datascience.exportAsPythonScript",
"onCommand:python.datascience.exportToHTML",
"onCommand:python.datascience.exportToPDF",
"onCommand:python.selectAndRunTestMethod",
"onCommand:python.selectAndDebugTestMethod",
"onCommand:python.selectAndRunTestFile",
Expand Down Expand Up @@ -321,6 +324,11 @@
"title": "%python.command.python.datascience.viewJupyterOutput.title%",
"category": "Python"
},
{
"command": "python.datascience.exportAsPythonScript",
"title": "%python.command.python.datascience.exportAsPythonScript.title%",
"category": "Python"
},
{
"command": "python.datascience.selectJupyterInterpreter",
"title": "%python.command.python.datascience.selectJupyterInterpreter.title%",
Expand Down Expand Up @@ -813,6 +821,12 @@
}
],
"commandPalette": [
{
"command": "python.datascience.exportAsPythonScript",
"title": "%python.command.python.datascience.exportAsPythonScript.title%",
"category": "Python",
"when": "python.datascience.isnativeactive && python.datascience.featureenabled"
},
{
"command": "python.switchOffInsidersChannel",
"title": "%python.command.python.switchOffInsidersChannel.title%",
Expand Down
6 changes: 6 additions & 0 deletions package.nls.json
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,9 @@
"python.command.python.viewOutput.title": "Show Output",
"python.command.python.viewTestOutput.title": "Show Test Output",
"python.command.python.datascience.viewJupyterOutput.title": "Show Jupyter Output",
"python.command.python.datascience.exportAsPythonScript.title": "Export as Python Script",
"python.command.python.datascience.exportToHTML.title": "Export to HTML",
"python.command.python.datascience.exportToPDF.title": "Export to PDF",
"python.command.python.viewLanguageServerOutput.title": "Show Language Server Output",
"python.command.python.selectAndRunTestMethod.title": "Run Test Method ...",
"python.command.python.selectAndDebugTestMethod.title": "Debug Test Method ...",
Expand Down Expand Up @@ -107,6 +110,9 @@
"DataScience.exportDialogFailed": "Failed to export notebook. {0}",
"DataScience.exportOpenQuestion": "Open in browser",
"DataScience.exportOpenQuestion1": "Open in editor",
"DataScience.notebookExportAs" : "Convert and save to a python script",
"DataScience.exportAsQuickPickPlaceholder" : "Export As...",
"DataScience.exportPythonQuickPickLabel" : "Python Script",
"DataScience.collapseInputTooltip": "Collapse input block",
"DataScience.collapseVariableExplorerTooltip": "Hide variables active in jupyter kernel",
"DataScience.collapseVariableExplorerLabel": "Variables",
Expand Down
6 changes: 5 additions & 1 deletion src/client/common/application/commands.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
import { CancellationToken, Position, TextDocument, Uri } from 'vscode';
import { Commands as LSCommands } from '../../activation/languageServer/constants';
import { Commands as DSCommands } from '../../datascience/constants';
import { INotebook } from '../../datascience/types';
import { INotebook, INotebookModel } from '../../datascience/types';
import { CommandSource } from '../../testing/common/constants';
import { TestFunction, TestsToRun } from '../../testing/common/types';
import { TestDataItem, TestWorkspaceFolder } from '../../testing/types';
Expand Down Expand Up @@ -168,6 +168,10 @@ export interface ICommandNameArgumentTypeMapping extends ICommandNameWithoutArgu
[DSCommands.RunCurrentCellAndAddBelow]: [string];
[DSCommands.ScrollToCell]: [string, string];
[DSCommands.ViewJupyterOutput]: [];
[DSCommands.ExportAsPythonScript]: [INotebookModel];
[DSCommands.ExportToHTML]: [INotebookModel];
[DSCommands.ExportToPDF]: [INotebookModel];
[DSCommands.Export]: [INotebookModel];
[DSCommands.SwitchJupyterKernel]: [INotebook | undefined, 'raw' | 'jupyter'];
[DSCommands.SelectJupyterCommandLine]: [undefined | Uri];
[DSCommands.SaveNotebookNonCustomEditor]: [Uri];
Expand Down
7 changes: 3 additions & 4 deletions src/client/common/utils/localize.ts
Original file line number Diff line number Diff line change
Expand Up @@ -560,6 +560,7 @@ export namespace DataScience {
export const clearAllOutput = localize('DataScience.clearAllOutput', 'Clear All Output');
export const interruptKernelStatus = localize('DataScience.interruptKernelStatus', 'Interrupting IPython Kernel');
export const exportCancel = localize('DataScience.exportCancel', 'Cancel');
export const exportPythonQuickPickLabel = localize('DataScience.exportPythonQuickPickLabel', 'Python Script');
export const restartKernelAfterInterruptMessage = localize(
'DataScience.restartKernelAfterInterruptMessage',
'Interrupting the kernel timed out. Do you want to restart the kernel instead? All variables will be lost.'
Expand Down Expand Up @@ -764,11 +765,9 @@ export namespace DataScience {
'DataScience.remoteDebuggerNotSupported',
'Debugging while attached to a remote server is not currently supported.'
);
export const exportAsPythonFileTooltip = localize(
'DataScience.exportAsPythonFileTooltip',
'Convert and save to a python script'
);
export const notebookExportAs = localize('DataScience.notebookExportAs', 'Convert and save to a python script');
export const exportAsPythonFileTitle = localize('DataScience.exportAsPythonFileTitle', 'Save As Python File');
export const exportAsQuickPickPlaceholder = localize('DataScience.exportAsQuickPickPlaceholder', 'Export As...');
export const runCell = localize('DataScience.runCell', 'Run cell');
export const deleteCell = localize('DataScience.deleteCell', 'Delete cell');
export const moveCellUp = localize('DataScience.moveCellUp', 'Move cell up');
Expand Down
5 changes: 4 additions & 1 deletion src/client/datascience/commands/commandRegistry.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ import {
INotebookEditorProvider
} from '../types';
import { JupyterCommandLineSelectorCommand } from './commandLineSelector';
import { ExportCommands } from './exportCommands';
import { KernelSwitcherCommand } from './kernelSwitcher';
import { JupyterServerSelectorCommand } from './serverSelector';

Expand All @@ -44,7 +45,8 @@ export class CommandRegistry implements IDisposable {
@inject(IApplicationShell) private appShell: IApplicationShell,
@inject(IOutputChannel) @named(JUPYTER_OUTPUT_CHANNEL) private jupyterOutput: IOutputChannel,
@inject(IStartPage) private startPage: IStartPage,
@inject(IExperimentService) private readonly expService: IExperimentService
@inject(IExperimentService) private readonly expService: IExperimentService,
@inject(ExportCommands) private readonly exportCommand: ExportCommands
) {
this.disposables.push(this.serverSelectedCommand);
this.disposables.push(this.kernelSwitcherCommand);
Expand All @@ -53,6 +55,7 @@ export class CommandRegistry implements IDisposable {
this.commandLineCommand.register();
this.serverSelectedCommand.register();
this.kernelSwitcherCommand.register();
this.exportCommand.register();
this.registerCommand(Commands.RunAllCells, this.runAllCells);
this.registerCommand(Commands.RunCell, this.runCell);
this.registerCommand(Commands.RunCurrentCell, this.runCurrentCell);
Expand Down
98 changes: 98 additions & 0 deletions src/client/datascience/commands/exportCommands.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License.

'use strict';

import { inject, injectable } from 'inversify';
import { QuickPickItem, QuickPickOptions } from 'vscode';
import { getLocString } from '../../../datascience-ui/react-common/locReactSide';
import { ICommandNameArgumentTypeMapping } from '../../common/application/commands';
import { IApplicationShell, ICommandManager } from '../../common/application/types';
import { IDisposable } from '../../common/types';
import { DataScience } from '../../common/utils/localize';
import { Commands } from '../constants';
import { ExportManager } from '../export/exportManager';
import { ExportFormat, IExportManager } from '../export/types';
import { INotebookEditorProvider, INotebookModel } from '../types';

interface IExportQuickPickItem extends QuickPickItem {
handler(): void;
}

@injectable()
export class ExportCommands implements IDisposable {
private readonly disposables: IDisposable[] = [];
constructor(
@inject(ICommandManager) private readonly commandManager: ICommandManager,
@inject(IExportManager) private exportManager: ExportManager,
@inject(IApplicationShell) private readonly applicationShell: IApplicationShell,
@inject(INotebookEditorProvider) private readonly notebookProvider: INotebookEditorProvider
) {}
public register() {
this.registerCommand(Commands.ExportAsPythonScript, (model) => this.export(model, ExportFormat.python));
this.registerCommand(Commands.ExportToHTML, (model) => this.export(model, ExportFormat.html));
this.registerCommand(Commands.ExportToPDF, (model) => this.export(model, ExportFormat.pdf));
this.registerCommand(Commands.Export, (model) => this.export(model));
}

public dispose() {
this.disposables.forEach((d) => d.dispose());
}

private registerCommand<
E extends keyof ICommandNameArgumentTypeMapping,
U extends ICommandNameArgumentTypeMapping[E]
// tslint:disable-next-line: no-any
>(command: E, callback: (...args: U) => any) {
const disposable = this.commandManager.registerCommand(command, callback, this);
this.disposables.push(disposable);
}

private async export(model: INotebookModel, exportMethod?: ExportFormat) {
if (!model) {
// if no model was passed then this was called from the command pallete,
// so we need to get the active editor
const activeEditor = this.notebookProvider.activeEditor;
if (!activeEditor || !activeEditor.model) {
return;
}
model = activeEditor.model;
}

if (exportMethod) {
await this.exportManager.export(exportMethod, model);
} else {
// if we don't have an export method we need to ask for one and display the
// quickpick menu
const pickedItem = await this.showExportQuickPickMenu(model).then((item) => item);
if (pickedItem !== undefined) {
pickedItem.handler();
}
}
}

private getExportQuickPickItems(model: INotebookModel): IExportQuickPickItem[] {
return [
{
label: DataScience.exportPythonQuickPickLabel(),
picked: true,
handler: () => this.commandManager.executeCommand(Commands.ExportAsPythonScript, model)
}
//{ label: 'HTML', picked: false, handler: () => this.commandManager.executeCommand(Commands.ExportToHTML) },
//{ label: 'PDF', picked: false, handler: () => this.commandManager.executeCommand(Commands.ExportToPDF) }
];
}

private async showExportQuickPickMenu(model: INotebookModel): Promise<IExportQuickPickItem | undefined> {
const items = this.getExportQuickPickItems(model);

const options: QuickPickOptions = {
ignoreFocusOut: false,
matchOnDescription: true,
matchOnDetail: true,
placeHolder: getLocString('DataScience.exportAsQuickPickPlaceholder', 'Export As...')
};

return this.applicationShell.showQuickPick(items, options);
}
}
4 changes: 4 additions & 0 deletions src/client/datascience/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,10 @@ export namespace Commands {
export const ScrollToCell = 'python.datascience.scrolltocell';
export const CreateNewNotebook = 'python.datascience.createnewnotebook';
export const ViewJupyterOutput = 'python.datascience.viewJupyterOutput';
export const ExportAsPythonScript = 'python.datascience.exportAsPythonScript';
export const ExportToHTML = 'python.datascience.exportToHTML';
export const ExportToPDF = 'python.datascience.exportToPDF';
export const Export = 'python.datascience.export';
export const SaveNotebookNonCustomEditor = 'python.datascience.notebookeditor.save';
export const SaveAsNotebookNonCustomEditor = 'python.datascience.notebookeditor.saveAs';
export const OpenNotebookNonCustomEditor = 'python.datascience.notebookeditor.open';
Expand Down
13 changes: 13 additions & 0 deletions src/client/datascience/export/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@

## TO ADD A NEW EXPORT METHOD
1. Create a new command in src/client/datascience/constants
2. Register the command in this file
3. Add an item to the quick pick menu for your new export method from inside the getExportQuickPickItems() method (in this file).
4. Add a new command to the command pallete in package.json (optional)
5. Declare and add your file extensions inside exportManagerFilePicker
6. Declare and add your export method inside exportManager
7. Create an injectable class that implements IExport and register it in src/client/datascience/serviceRegistry
8. Implement the export method on your new class
9. Inject the class inside exportManager
10. Add a case for your new export method and call the export method of your new class with the appropriate arguments
11. Add telementry and status messages
72 changes: 72 additions & 0 deletions src/client/datascience/export/exportManager.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
import { inject, injectable, named } from 'inversify';
import { Uri } from 'vscode';
import { IFileSystem, TemporaryFile } from '../../common/platform/types';
import { IDataScienceErrorHandler, INotebookModel } from '../types';
import { IExportManagerFilePicker } from './exportManagerFilePicker';
import { ExportFormat, IExport, IExportManager } from './types';

@injectable()
export class ExportManager implements IExportManager {
constructor(
@inject(IExport) @named(ExportFormat.pdf) private readonly exportToPDF: IExport,
@inject(IExport) @named(ExportFormat.html) private readonly exportToHTML: IExport,
@inject(IExport) @named(ExportFormat.python) private readonly exportToPython: IExport,
@inject(IFileSystem) private readonly fileSystem: IFileSystem,
@inject(IDataScienceErrorHandler) private readonly errorHandler: IDataScienceErrorHandler,
@inject(IExportManagerFilePicker) private readonly filePicker: IExportManagerFilePicker
) {}

public async export(format: ExportFormat, model: INotebookModel): Promise<Uri | undefined> {
// need to add telementry
let target;
if (format !== ExportFormat.python) {
target = await this.filePicker.getExportFileLocation(format, model.file);
if (!target) {
return;
}
} else {
target = Uri.file((await this.fileSystem.createTemporaryFile('.py')).filePath);
}

const tempFile = await this.makeTemporaryFile(model);
if (!tempFile) {
return; // error making temp file
}

const source = Uri.file(tempFile.filePath);
try {
switch (format) {
case ExportFormat.python:
await this.exportToPython.export(source, target);
break;

case ExportFormat.pdf:
await this.exportToPDF.export(source, target);
break;

case ExportFormat.html:
await this.exportToHTML.export(source, target);
break;
default:
break;
}
} finally {
tempFile.dispose();
}

return target;
}

private async makeTemporaryFile(model: INotebookModel): Promise<TemporaryFile | undefined> {
let tempFile: TemporaryFile | undefined;
try {
tempFile = await this.fileSystem.createTemporaryFile('.ipynb');
const content = model ? model.getContent() : '';
await this.fileSystem.writeFile(tempFile.filePath, content, 'utf-8');
} catch (e) {
await this.errorHandler.handleError(e);
}

return tempFile;
}
}
29 changes: 29 additions & 0 deletions src/client/datascience/export/exportManagerDependencyChecker.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import { inject, injectable } from 'inversify';
import { Uri } from 'vscode';
import * as localize from '../../common/utils/localize';
import { IJupyterExecution, IJupyterInterpreterDependencyManager, INotebookModel } from '../types';
import { ExportManager } from './exportManager';
import { ExportFormat, IExportManager } from './types';

@injectable()
export class ExportManagerDependencyChecker implements IExportManager {
constructor(
@inject(ExportManager) private readonly manager: IExportManager,
@inject(IJupyterExecution) private jupyterExecution: IJupyterExecution,
@inject(IJupyterInterpreterDependencyManager)
private readonly dependencyManager: IJupyterInterpreterDependencyManager
) {}

public async export(format: ExportFormat, model: INotebookModel): Promise<Uri | undefined> {
// Before we try the import, see if we don't support it, if we don't give a chance to install dependencies
if (!(await this.jupyterExecution.isImportSupported())) {
await this.dependencyManager.installMissingDependencies();
}

if (await this.jupyterExecution.isImportSupported()) {
return this.manager.export(format, model);
} else {
throw new Error(localize.DataScience.jupyterNbConvertNotSupported());
}
}
}
Loading