Skip to content

Commit 2e129b4

Browse files
Port NB Convert Fix to point release branch (#14200)
1 parent f276e07 commit 2e129b4

18 files changed

+204
-65
lines changed

CHANGELOG.md

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,67 @@
11
# Changelog
22

3+
## 2020.9.2 (1 October 2020)
4+
5+
### Fixes
6+
7+
1. Support nbconvert version 6+ for exporting notebooks to python code.
8+
([#14169](https://github.com/Microsoft/vscode-python/issues/14169))
9+
10+
### Thanks
11+
12+
Thanks to the following projects which we fully rely on to provide some of
13+
our features:
14+
15+
- [debugpy](https://pypi.org/project/debugpy/)
16+
- [isort](https://pypi.org/project/isort/)
17+
- [jedi](https://pypi.org/project/jedi/)
18+
and [parso](https://pypi.org/project/parso/)
19+
- [Microsoft Python Language Server](https://github.com/microsoft/python-language-server)
20+
- [Pylance](https://github.com/microsoft/pylance-release)
21+
- [exuberant ctags](http://ctags.sourceforge.net/) (user-installed)
22+
- [rope](https://pypi.org/project/rope/) (user-installed)
23+
24+
Also thanks to the various projects we provide integrations with which help
25+
make this extension useful:
26+
27+
- Debugging support:
28+
[Django](https://pypi.org/project/Django/),
29+
[Flask](https://pypi.org/project/Flask/),
30+
[gevent](https://pypi.org/project/gevent/),
31+
[Jinja](https://pypi.org/project/Jinja/),
32+
[Pyramid](https://pypi.org/project/pyramid/),
33+
[PySpark](https://pypi.org/project/pyspark/),
34+
[Scrapy](https://pypi.org/project/Scrapy/),
35+
[Watson](https://pypi.org/project/Watson/)
36+
- Formatting:
37+
[autopep8](https://pypi.org/project/autopep8/),
38+
[black](https://pypi.org/project/black/),
39+
[yapf](https://pypi.org/project/yapf/)
40+
- Interpreter support:
41+
[conda](https://conda.io/),
42+
[direnv](https://direnv.net/),
43+
[pipenv](https://pypi.org/project/pipenv/),
44+
[pyenv](https://github.com/pyenv/pyenv),
45+
[venv](https://docs.python.org/3/library/venv.html#module-venv),
46+
[virtualenv](https://pypi.org/project/virtualenv/)
47+
- Linting:
48+
[bandit](https://pypi.org/project/bandit/),
49+
[flake8](https://pypi.org/project/flake8/),
50+
[mypy](https://pypi.org/project/mypy/),
51+
[prospector](https://pypi.org/project/prospector/),
52+
[pylint](https://pypi.org/project/pylint/),
53+
[pydocstyle](https://pypi.org/project/pydocstyle/),
54+
[pylama](https://pypi.org/project/pylama/)
55+
- Testing:
56+
[nose](https://pypi.org/project/nose/),
57+
[pytest](https://pypi.org/project/pytest/),
58+
[unittest](https://docs.python.org/3/library/unittest.html#module-unittest)
59+
60+
And finally thanks to the [Python](https://www.python.org/) development team and
61+
community for creating a fantastic programming language and community to be a
62+
part of!
63+
64+
365
## 2020.9.1 (29 September 2020)
466

567
### Fixes

package.nls.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,7 @@
3131
"DataScience.openExportFileYes": "Yes",
3232
"DataScience.openExportFileNo": "No",
3333
"DataScience.failedExportMessage": "Export failed.",
34-
"DataScience.exportFailedGeneralMessage": "Export failed. Please check the 'Python' [output](command:python.viewOutput) panel for further details.",
34+
"DataScience.exportFailedGeneralMessage": "Please check the 'Python' [output](command:python.viewOutput) panel for further details.",
3535
"DataScience.exportToPDFDependencyMessage": "If you have not installed xelatex (TeX) you will need to do so before you can export to PDF, for further instructions go to https://nbconvert.readthedocs.io/en/latest/install.html#installing-tex. \r\nTo avoid installing xelatex (TeX) you might want to try exporting to HTML and using your browsers \"Print to PDF\" feature.",
3636
"DataScience.launchNotebookTrustPrompt": "A notebook could execute harmful code when opened. Some outputs have been hidden. Do you trust this notebook? [Learn more.](https://aka.ms/trusted-notebooks)",
3737
"DataScience.launchNotebookTrustPrompt.yes": "Trust",

src/client/common/utils/localize.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -892,7 +892,7 @@ export namespace DataScience {
892892
export const openExportFileNo = localize('DataScience.openExportFileNo', 'No');
893893
export const exportFailedGeneralMessage = localize(
894894
'DataScience.exportFailedGeneralMessage',
895-
`Export failed. Please check the 'Python' [output](command:python.viewOutput) panel for further details.`
895+
`Please check the 'Python' [output](command:python.viewOutput) panel for further details.`
896896
);
897897
export const exportToPDFDependencyMessage = localize(
898898
'DataScience.exportToPDFDependencyMessage',

src/client/datascience/common.ts

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
'use strict';
44
import type { nbformat } from '@jupyterlab/coreutils';
55
import * as os from 'os';
6+
import { parse, SemVer } from 'semver';
67
import { Memento, Uri } from 'vscode';
78
import { splitMultilineString } from '../../datascience-ui/common';
89
import { traceError, traceInfo } from '../common/logger';
@@ -188,3 +189,14 @@ export async function getRealPath(
188189
}
189190
}
190191
}
192+
193+
// For the given string parse it out to a SemVer or return undefined
194+
export function parseSemVer(versionString: string): SemVer | undefined {
195+
const versionMatch = /^\s*(\d+)\.(\d+)\.(.+)\s*$/.exec(versionString);
196+
if (versionMatch && versionMatch.length > 2) {
197+
const major = parseInt(versionMatch[1], 10);
198+
const minor = parseInt(versionMatch[2], 10);
199+
const build = parseInt(versionMatch[3], 10);
200+
return parse(`${major}.${minor}.${build}`, true) ?? undefined;
201+
}
202+
}

src/client/datascience/constants.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -569,7 +569,7 @@ export namespace LiveShare {
569569

570570
export namespace LiveShareCommands {
571571
export const isNotebookSupported = 'isNotebookSupported';
572-
export const isImportSupported = 'isImportSupported';
572+
export const getImportPackageVersion = 'getImportPackageVersion';
573573
export const connectToNotebookServer = 'connectToNotebookServer';
574574
export const getUsableJupyterPython = 'getUsableJupyterPython';
575575
export const executeObservable = 'executeObservable';

src/client/datascience/data-viewing/dataViewerDependencyService.ts

Lines changed: 4 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
'use strict';
55

66
import { inject, injectable } from 'inversify';
7-
import { parse, SemVer } from 'semver';
7+
import { SemVer } from 'semver';
88
import { CancellationToken } from 'vscode';
99
import { IApplicationShell } from '../../common/application/types';
1010
import { Cancellation, createPromiseFromCancellation, wrapCancellationTokens } from '../../common/cancellation';
@@ -14,6 +14,7 @@ import { IInstaller, InstallerResponse, Product } from '../../common/types';
1414
import { Common, DataScience } from '../../common/utils/localize';
1515
import { PythonEnvironment } from '../../pythonEnvironments/info';
1616
import { sendTelemetryEvent } from '../../telemetry';
17+
import { parseSemVer } from '../common';
1718
import { Telemetry } from '../constants';
1819

1920
const minimumSupportedPandaVersion = '0.20.0';
@@ -104,13 +105,8 @@ export class DataViewerDependencyService {
104105
throwOnStdErr: true,
105106
token
106107
});
107-
const versionMatch = /^\s*(\d+)\.(\d+)\.(.+)\s*$/.exec(result.stdout);
108-
if (versionMatch && versionMatch.length > 2) {
109-
const major = parseInt(versionMatch[1], 10);
110-
const minor = parseInt(versionMatch[2], 10);
111-
const build = parseInt(versionMatch[3], 10);
112-
return parse(`${major}.${minor}.${build}`, true) ?? undefined;
113-
}
108+
109+
return parseSemVer(result.stdout);
114110
} catch (ex) {
115111
traceWarning('Failed to get version of Pandas to use Data Viewer', ex);
116112
return;

src/client/datascience/export/exportDependencyChecker.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,9 +17,9 @@ export class ExportDependencyChecker {
1717
// Before we try the import, see if we don't support it, if we don't give a chance to install dependencies
1818
const reporter = this.progressReporter.createProgressIndicator(`Exporting to ${format}`);
1919
try {
20-
if (!(await this.jupyterExecution.isImportSupported())) {
20+
if (!(await this.jupyterExecution.getImportPackageVersion())) {
2121
await this.dependencyManager.installMissingDependencies();
22-
if (!(await this.jupyterExecution.isImportSupported())) {
22+
if (!(await this.jupyterExecution.getImportPackageVersion())) {
2323
throw new Error(localize.DataScience.jupyterNbConvertNotSupported());
2424
}
2525
}

src/client/datascience/jupyter/interpreter/jupyterInterpreterDependencyService.ts

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
'use strict';
55

66
import { inject, injectable } from 'inversify';
7+
import { SemVer } from 'semver';
78
import { CancellationToken } from 'vscode';
89
import { IApplicationShell } from '../../../common/application/types';
910
import { Cancellation, createPromiseFromCancellation, wrapCancellationTokens } from '../../../common/cancellation';
@@ -14,6 +15,7 @@ import { Common, DataScience } from '../../../common/utils/localize';
1415
import { noop } from '../../../common/utils/misc';
1516
import { PythonEnvironment } from '../../../pythonEnvironments/info';
1617
import { sendTelemetryEvent } from '../../../telemetry';
18+
import { parseSemVer } from '../../common';
1719
import { HelpLinks, JupyterCommands, Telemetry } from '../../constants';
1820
import { reportAction } from '../../progress/decorator';
1921
import { ReportableAction } from '../../progress/types';
@@ -241,6 +243,23 @@ export class JupyterInterpreterDependencyService {
241243
return installed;
242244
}
243245

246+
public async getNbConvertVersion(
247+
interpreter: PythonEnvironment,
248+
_token?: CancellationToken
249+
): Promise<SemVer | undefined> {
250+
const command = this.commandFactory.createInterpreterCommand(
251+
JupyterCommands.ConvertCommand,
252+
'jupyter',
253+
['-m', 'jupyter', 'nbconvert'],
254+
interpreter,
255+
false
256+
);
257+
258+
const result = await command.exec(['--version'], { throwOnStdErr: true });
259+
260+
return parseSemVer(result.stdout);
261+
}
262+
244263
/**
245264
* Gets a list of the dependencies not installed, dependencies that are required to launch the jupyter notebook server.
246265
*

src/client/datascience/jupyter/interpreter/jupyterInterpreterSubCommandExecutionService.ts

Lines changed: 19 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55

66
import { inject, injectable, named } from 'inversify';
77
import * as path from 'path';
8+
import { SemVer } from 'semver';
89
import { CancellationToken, Uri } from 'vscode';
910
import { Cancellation } from '../../../common/cancellation';
1011
import { traceError, traceInfo, traceWarning } from '../../../common/logger';
@@ -76,12 +77,16 @@ export class JupyterInterpreterSubCommandExecutionService
7677
}
7778
return this.jupyterDependencyService.areDependenciesInstalled(interpreter, token);
7879
}
79-
public async isExportSupported(token?: CancellationToken): Promise<boolean> {
80+
public async getExportPackageVersion(token?: CancellationToken): Promise<SemVer | undefined> {
8081
const interpreter = await this.jupyterInterpreter.getSelectedInterpreter(token);
8182
if (!interpreter) {
82-
return false;
83+
return;
84+
}
85+
86+
// If nbconvert is there check and return the version
87+
if (await this.jupyterDependencyService.isExportSupported(interpreter, token)) {
88+
return this.jupyterDependencyService.getNbConvertVersion(interpreter, token);
8389
}
84-
return this.jupyterDependencyService.isExportSupported(interpreter, token);
8590
}
8691
public async getReasonForJupyterNotebookNotBeingSupported(token?: CancellationToken): Promise<string> {
8792
let interpreter = await this.jupyterInterpreter.getSelectedInterpreter(token);
@@ -176,11 +181,21 @@ export class JupyterInterpreterSubCommandExecutionService
176181
const args = template
177182
? [file.fsPath, '--to', 'python', '--stdout', '--template', template]
178183
: [file.fsPath, '--to', 'python', '--stdout'];
184+
179185
// Ignore stderr, as nbconvert writes conversion result to stderr.
180186
// stdout contains the generated python code.
181187
return daemon
182188
.execModule('jupyter', ['nbconvert'].concat(args), { throwOnStdErr: false, encoding: 'utf8', token })
183-
.then((output) => output.stdout);
189+
.then((output) => {
190+
// We can't check stderr (as nbconvert puts diag output there) but we need to verify here that we actually
191+
// converted something. If it's zero size then just raise an error
192+
if (output.stdout === '') {
193+
traceError('nbconvert zero size output');
194+
throw new Error(output.stderr);
195+
} else {
196+
return output.stdout;
197+
}
198+
});
184199
}
185200
public async openNotebook(notebookFile: string): Promise<void> {
186201
const interpreter = await this.getSelectedInterpreterAndThrowIfNotAvailable();

src/client/datascience/jupyter/jupyterExecution.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
// Licensed under the MIT License.
33
'use strict';
44
import * as path from 'path';
5+
import { SemVer } from 'semver';
56
import * as uuid from 'uuid/v4';
67
import { CancellationToken, CancellationTokenSource, Event, EventEmitter, Uri } from 'vscode';
78

@@ -123,9 +124,9 @@ export class JupyterExecutionBase implements IJupyterExecution {
123124
}
124125

125126
@reportAction(ReportableAction.CheckingIfImportIsSupported)
126-
public async isImportSupported(cancelToken?: CancellationToken): Promise<boolean> {
127+
public async getImportPackageVersion(cancelToken?: CancellationToken): Promise<SemVer | undefined> {
127128
// See if we can find the command nbconvert
128-
return this.jupyterInterpreterService.isExportSupported(cancelToken);
129+
return this.jupyterInterpreterService.getExportPackageVersion(cancelToken);
129130
}
130131

131132
public isSpawnSupported(cancelToken?: CancellationToken): Promise<boolean> {

src/client/datascience/jupyter/jupyterExecutionFactory.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
// Licensed under the MIT License.
33
'use strict';
44
import { inject, injectable, named } from 'inversify';
5+
import { SemVer } from 'semver';
56
import { CancellationToken, Event, EventEmitter, Uri } from 'vscode';
67

78
import { IApplicationShell, ILiveShareApi, IWorkspaceService } from '../../common/application/types';
@@ -117,9 +118,9 @@ export class JupyterExecutionFactory implements IJupyterExecution, IAsyncDisposa
117118
return execution.getNotebookError();
118119
}
119120

120-
public async isImportSupported(cancelToken?: CancellationToken): Promise<boolean> {
121+
public async getImportPackageVersion(cancelToken?: CancellationToken): Promise<SemVer | undefined> {
121122
const execution = await this.executionFactory.get();
122-
return execution.isImportSupported(cancelToken);
123+
return execution.getImportPackageVersion(cancelToken);
123124
}
124125
public async isSpawnSupported(cancelToken?: CancellationToken): Promise<boolean> {
125126
const execution = await this.executionFactory.get();

src/client/datascience/jupyter/jupyterImporter.ts

Lines changed: 33 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -26,20 +26,22 @@ import {
2626
export class JupyterImporter implements INotebookImporter {
2727
public isDisposed: boolean = false;
2828
// Template that changes markdown cells to have # %% [markdown] in the comments
29-
private readonly nbconvertTemplateFormat =
29+
private readonly nbconvertBaseTemplateFormat =
3030
// tslint:disable-next-line:no-multiline-string
31-
`{%- extends 'null.tpl' -%}
31+
`{%- extends '{0}' -%}
3232
{% block codecell %}
33-
{0}
33+
{1}
3434
{{ super() }}
3535
{% endblock codecell %}
3636
{% block in_prompt %}{% endblock in_prompt %}
3737
{% block input %}{{ cell.source | ipython2python }}{% endblock input %}
3838
{% block markdowncell scoped %}{0} [markdown]
3939
{{ cell.source | comment_lines }}
4040
{% endblock markdowncell %}`;
41-
42-
private templatePromise: Promise<string | undefined>;
41+
private readonly nbconvert5Null = 'null.tpl';
42+
private readonly nbconvert6Null = 'base/null.j2';
43+
private template5Promise?: Promise<string | undefined>;
44+
private template6Promise?: Promise<string | undefined>;
4345

4446
constructor(
4547
@inject(IDataScienceFileSystem) private fs: IDataScienceFileSystem,
@@ -50,13 +52,9 @@ export class JupyterImporter implements INotebookImporter {
5052
@inject(IPlatformService) private readonly platform: IPlatformService,
5153
@inject(IJupyterInterpreterDependencyManager)
5254
private readonly dependencyManager: IJupyterInterpreterDependencyManager
53-
) {
54-
this.templatePromise = this.createTemplateFile();
55-
}
55+
) {}
5656

5757
public async importFromFile(sourceFile: Uri): Promise<string> {
58-
const template = await this.templatePromise;
59-
6058
// If the user has requested it, add a cd command to the imported file so that relative paths still work
6159
const settings = this.configuration.getSettings();
6260
let directoryChange: string | undefined;
@@ -65,12 +63,30 @@ export class JupyterImporter implements INotebookImporter {
6563
}
6664

6765
// Before we try the import, see if we don't support it, if we don't give a chance to install dependencies
68-
if (!(await this.jupyterExecution.isImportSupported())) {
66+
if (!(await this.jupyterExecution.getImportPackageVersion())) {
6967
await this.dependencyManager.installMissingDependencies();
7068
}
7169

70+
const nbConvertVersion = await this.jupyterExecution.getImportPackageVersion();
7271
// Use the jupyter nbconvert functionality to turn the notebook into a python file
73-
if (await this.jupyterExecution.isImportSupported()) {
72+
if (nbConvertVersion) {
73+
// nbconvert 5 and 6 use a different base template file
74+
// Create and select the correct one
75+
let template: string | undefined;
76+
if (nbConvertVersion.major >= 6) {
77+
if (!this.template6Promise) {
78+
this.template6Promise = this.createTemplateFile(true);
79+
}
80+
81+
template = await this.template6Promise;
82+
} else {
83+
if (!this.template5Promise) {
84+
this.template5Promise = this.createTemplateFile(false);
85+
}
86+
87+
template = await this.template5Promise;
88+
}
89+
7490
let fileOutput: string = await this.jupyterExecution.importNotebook(sourceFile, template);
7591
if (fileOutput.includes('get_ipython()')) {
7692
fileOutput = this.addIPythonImport(fileOutput);
@@ -153,7 +169,7 @@ export class JupyterImporter implements INotebookImporter {
153169
}
154170
}
155171

156-
private async createTemplateFile(): Promise<string | undefined> {
172+
private async createTemplateFile(nbconvert6: boolean): Promise<string | undefined> {
157173
// Create a temp file on disk
158174
const file = await this.fs.createTemporaryLocalFile('.tpl');
159175

@@ -164,7 +180,10 @@ export class JupyterImporter implements INotebookImporter {
164180
this.disposableRegistry.push(file);
165181
await this.fs.appendLocalFile(
166182
file.filePath,
167-
this.nbconvertTemplateFormat.format(this.defaultCellMarker)
183+
this.nbconvertBaseTemplateFormat.format(
184+
nbconvert6 ? this.nbconvert6Null : this.nbconvert5Null,
185+
this.defaultCellMarker
186+
)
168187
);
169188

170189
// Now we should have a template that will convert

0 commit comments

Comments
 (0)