Skip to content

Commit 5414ca7

Browse files
[Notebooks]: Support Jupyter Notebooks (#2048)
* feat(plugins): add jupyter plugin - #2029 * feat-fix(notebook): add required nbformat package - #2029 * test(plugin): use strictEqual - #2029 * feat-fix(plugin): add plugin to registery - #2029 --------- Co-authored-by: Florian Sihler <florian.sihler@uni-ulm.de>
1 parent 925c86e commit 5414ca7

File tree

11 files changed

+229
-8
lines changed

11 files changed

+229
-8
lines changed

package-lock.json

Lines changed: 25 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -198,6 +198,7 @@
198198
},
199199
"dependencies": {
200200
"@eagleoutice/tree-sitter-r": "^1.1.2",
201+
"@jupyterlab/nbformat": "^4.5.0",
201202
"@xmldom/xmldom": "^0.9.7",
202203
"clipboardy": "^4.0.0",
203204
"command-line-args": "^6.0.1",
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
import type { PathLike } from 'fs';
2+
import { SemVer } from 'semver';
3+
import type { FlowrAnalyzerContext } from '../../../context/flowr-analyzer-context';
4+
import type { FlowrFileProvider } from '../../../context/flowr-file';
5+
import { FlowrAnalyzerFilePlugin } from '../flowr-analyzer-file-plugin';
6+
import { platformBasename } from '../../../../dataflow/internal/process/functions/call/built-in/built-in-source';
7+
import { FlowrJupyterFile } from './flowr-jupyter-file';
8+
9+
const IpynbPattern = /\.ipynb$/i;
10+
11+
/**
12+
* The plugin provides support for Jupyter (`.ipynb`) files
13+
*/
14+
export class FlowrAnalyzerJupyterFilePlugin extends FlowrAnalyzerFilePlugin {
15+
public readonly name = 'ipynb-file-plugin';
16+
public readonly description = 'Parses Jupyter files';
17+
public readonly version = new SemVer('0.1.0');
18+
private readonly pattern: RegExp;
19+
20+
/**
21+
* Creates a new instance of the Jupyter file plugin.
22+
* @param filePattern - The pattern to identify Jupyter files, see {@link IpynbPattern} for the default pattern.
23+
*/
24+
constructor(filePattern: RegExp = IpynbPattern) {
25+
super();
26+
this.pattern = filePattern;
27+
}
28+
29+
public applies(file: PathLike): boolean {
30+
return this.pattern.test(platformBasename(file.toString()));
31+
}
32+
33+
protected process(_ctx: FlowrAnalyzerContext, arg: FlowrFileProvider<string>): FlowrJupyterFile {
34+
return FlowrJupyterFile.from(arg);
35+
}
36+
}

src/project/plugins/file-plugins/notebooks/flowr-analyzer-qmd-file-plugin.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,6 @@ export class FlowrAnalyzerQmdFilePlugin extends FlowrAnalyzerFilePlugin {
3131
}
3232

3333
protected process(_ctx: FlowrAnalyzerContext, arg: FlowrFileProvider<string>): FlowrRMarkdownFile {
34-
return new FlowrRMarkdownFile(arg);
34+
return FlowrRMarkdownFile.from(arg);
3535
}
36-
}
36+
}
Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
import type { INotebookContent } from '@jupyterlab/nbformat';
2+
import type { FlowrFileProvider } from '../../../context/flowr-file';
3+
import { FileRole, FlowrFile } from '../../../context/flowr-file';
4+
5+
6+
/**
7+
* This decorates a text file and parses its contents as a Jupyter file.
8+
* Finnaly, it provides access to the single cells, and all cells fused together as one R file.
9+
*/
10+
export class FlowrJupyterFile extends FlowrFile<string> {
11+
private readonly wrapped: FlowrFileProvider<string>;
12+
13+
/**
14+
* Prefer the static {@link FlowrRMarkdownFile.from} method
15+
* @param file - the file to load as R Markdown
16+
*/
17+
constructor(file: FlowrFileProvider<string>) {
18+
super(file.path(), FileRole.Source);
19+
this.wrapped = file;
20+
}
21+
22+
/**
23+
* Loads and parses the content of the wrapped file.
24+
* @returns RmdInfo
25+
*/
26+
protected loadContent(): string {
27+
return loadJupyter(this.wrapped.content());
28+
}
29+
30+
public static from(file: FlowrFileProvider<string> | FlowrJupyterFile): FlowrJupyterFile {
31+
return file instanceof FlowrJupyterFile ? file : new FlowrJupyterFile(file);
32+
}
33+
}
34+
35+
function loadJupyter(content: string): string {
36+
const nb = JSON.parse(content) as INotebookContent;
37+
38+
return nb.cells.map(cell => {
39+
if(cell.cell_type === 'code') {
40+
return typeof cell.source === 'object' ? cell.source.join('') : cell.source;
41+
} else {
42+
return typeof cell.source === 'object' ? cell.source.map(s => `# ${s}`).join('') : `# ${cell.source}`;
43+
}
44+
}).join('\n');
45+
}

src/project/plugins/file-plugins/notebooks/notebook.ts

Whitespace-only changes.

src/project/plugins/flowr-analyzer-plugin-defaults.ts

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import {
88
} from './loading-order-plugins/flowr-analyzer-loading-order-description-file-plugin';
99
import { FlowrAnalyzerRmdFilePlugin } from './file-plugins/notebooks/flowr-analyzer-rmd-file-plugin';
1010
import { FlowrAnalyzerQmdFilePlugin } from './file-plugins/notebooks/flowr-analyzer-qmd-file-plugin';
11+
import { FlowrAnalyzerJupyterFilePlugin } from './file-plugins/notebooks/flowr-analyzer-jupyter-file-plugin';
1112

1213
/**
1314
* Provides the default set of Flowr Analyzer plugins.
@@ -18,6 +19,7 @@ export function FlowrAnalyzerPluginDefaults(): FlowrAnalyzerPlugin[] {
1819
new FlowrAnalyzerPackageVersionsDescriptionFilePlugin(),
1920
new FlowrAnalyzerLoadingOrderDescriptionFilePlugin(),
2021
new FlowrAnalyzerRmdFilePlugin(),
21-
new FlowrAnalyzerQmdFilePlugin()
22+
new FlowrAnalyzerQmdFilePlugin(),
23+
new FlowrAnalyzerJupyterFilePlugin(),
2224
];
23-
}
25+
}

src/project/plugins/plugin-registry.ts

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import {
99
import { FlowrAnalyzerRmdFilePlugin } from './file-plugins/notebooks/flowr-analyzer-rmd-file-plugin';
1010
import { FlowrAnalyzerQmdFilePlugin } from './file-plugins/notebooks/flowr-analyzer-qmd-file-plugin';
1111
import { guard } from '../../util/assert';
12+
import { FlowrAnalyzerJupyterFilePlugin } from './file-plugins/notebooks/flowr-analyzer-jupyter-file-plugin';
1213

1314
/**
1415
* The built-in Flowr Analyzer plugins that are always available.
@@ -18,7 +19,8 @@ export const BuiltInPlugins = [
1819
['versions:description', FlowrAnalyzerPackageVersionsDescriptionFilePlugin],
1920
['loading-order:description', FlowrAnalyzerLoadingOrderDescriptionFilePlugin],
2021
['file:rmd', FlowrAnalyzerRmdFilePlugin],
21-
['file:qmd', FlowrAnalyzerQmdFilePlugin]
22+
['file:qmd', FlowrAnalyzerQmdFilePlugin],
23+
['file:ipynb', FlowrAnalyzerJupyterFilePlugin],
2224
] as const satisfies [string, PluginProducer][];
2325

2426
export type BuiltInFlowrPluginName = typeof BuiltInPlugins[number][0];
@@ -79,4 +81,4 @@ export function makePlugin<T extends BuiltInFlowrPluginName | string>(toRegister
7981
const plugin = getPlugin(toRegister, []);
8082
guard(plugin !== undefined, () => `Unknown Flowr Analyzer plugin: ${toRegister.toString()}`);
8183
return plugin;
82-
}
84+
}
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
import { describe } from 'vitest';
2+
import { testFileLoadPlugin } from './plugin-test-helper';
3+
import { FlowrAnalyzerJupyterFilePlugin } from '../../../../src/project/plugins/file-plugins/notebooks/flowr-analyzer-jupyter-file-plugin';
4+
import { FlowrJupyterFile } from '../../../../src/project/plugins/file-plugins/notebooks/flowr-jupyter-file';
5+
6+
describe('Jupyter-file', async() => {
7+
await testFileLoadPlugin(FlowrAnalyzerJupyterFilePlugin, FlowrJupyterFile, 'test/testfiles/notebook/example.ipynb', `x <- 5
8+
cat(x)
9+
# # Hello
10+
# This is a cool test
11+
y <- c(1,2,3)
12+
plot(y)
13+
# Hi`);
14+
});

test/functionality/project/plugin/plugin-test-helper.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -43,8 +43,8 @@ export async function testFileLoadPlugin<F extends ConstructTo<FlowrFile>, P ext
4343
await analyzer.parse();
4444

4545
const files = analyzer.inspectContext().files.getFilesByRole(FileRole.Source);
46-
assert(files.length === 1);
46+
assert.strictEqual(files.length, 1);
4747
assert(files[0] instanceof pluginFileType);
48-
assert(files[0].content() === expectedContent);
48+
assert.strictEqual(files[0].content(), expectedContent);
4949
});
5050
}

0 commit comments

Comments
 (0)