Skip to content

Commit 2e6e81f

Browse files
agoose77stevejpurvesfwkoch
authored
🤖 Store execution outputs in Outputs (#1903)
* feat:add outputs node * chore: add changeset * fix: drop style change * fix: only label first code and outputs * fix: handle IDs * fix: return type * fix: selectAll → select * refactor: move user expression lowering into function * refactor: nest declaration * fix: rename outputs * refactor: simplify conditions We don't need to delete nodes without children: they will simply vanish when lifted * test: fix tests * fix: outputs for JATS * fix: use outputs- for all prefixes * fix: outputs for JATS * fix: early exit non-matplotlib * test: partially fix JATS test * fix: final JATS test * test: fix myst-execute tests * test: fix test titles * test: fix missing children * test: more fixes * test: fix test titles * fix: handle visibility properly * fix: update test cases * 🪪 adding id to outputs enables in-browser compute for figures and embeds * feat: bump version * feat: add warning for unbounded migration * test: bump version in test * test: bump version * fix: align isCodeBlock with old PR after rebase * 🎋post rebase build * bump date on migrate step --------- Co-authored-by: Steve Purves <[email protected]> Co-authored-by: Franklin Koch <[email protected]>
1 parent d7fec52 commit 2e6e81f

File tree

25 files changed

+683
-466
lines changed

25 files changed

+683
-466
lines changed
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
---
2+
"mystmd": minor
3+
"myst-directives": patch
4+
"myst-transforms": patch
5+
"myst-spec-ext": patch
6+
"myst-execute": patch
7+
"myst-cli": patch
8+
---
9+
10+
Add support for new Outputs node

‎packages/myst-cli/src/process/notebook.ts‎

Lines changed: 24 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -84,6 +84,21 @@ export async function processNotebook(
8484
return mdast;
8585
}
8686

87+
/**
88+
* Embed the Jupyter output data for a user expression into the AST
89+
*/
90+
function embedInlineExpressions(
91+
userExpressions: IUserExpressionMetadata[] | undefined,
92+
block: GenericNode,
93+
) {
94+
const inlineNodes = selectAll('inlineExpression', block) as InlineExpression[];
95+
inlineNodes.forEach((inlineExpression) => {
96+
const data = findExpression(userExpressions, inlineExpression.value);
97+
if (!data) return;
98+
inlineExpression.result = data.result as unknown as Record<string, unknown>;
99+
});
100+
}
101+
87102
export async function processNotebookFull(
88103
session: ISession,
89104
file: string,
@@ -136,17 +151,7 @@ export async function processNotebookFull(
136151
return acc.concat(...cellMdast.children);
137152
}
138153
const block = blockParent(cell, cellMdast.children) as GenericNode;
139-
140-
// Embed expression results into expression
141-
const userExpressions = block.data?.[metadataSection] as
142-
| IUserExpressionMetadata[]
143-
| undefined;
144-
const inlineNodes = selectAll('inlineExpression', block) as InlineExpression[];
145-
inlineNodes.forEach((inlineExpression) => {
146-
const data = findExpression(userExpressions, inlineExpression.value);
147-
if (!data) return;
148-
inlineExpression.result = data.result as unknown as Record<string, unknown>;
149-
});
154+
embedInlineExpressions(block.data?.[metadataSection], block);
150155
return acc.concat(block);
151156
}
152157
if (cell.cell_type === CELL_TYPES.raw) {
@@ -165,17 +170,16 @@ export async function processNotebookFull(
165170
value: ensureString(cell.source),
166171
};
167172

168-
// Embed outputs in an output block
169-
const output: { type: 'output'; id: string; data: IOutput[] } = {
170-
type: 'output',
173+
const outputs = {
174+
type: 'outputs',
171175
id: nanoid(),
172-
data: [],
176+
children: (cell.outputs as IOutput[]).map((output) => ({
177+
type: 'output',
178+
jupyter_data: output,
179+
children: [],
180+
})),
173181
};
174-
175-
if (cell.outputs && (cell.outputs as IOutput[]).length > 0) {
176-
output.data = cell.outputs as IOutput[];
177-
}
178-
return acc.concat(blockParent(cell, [code, output]));
182+
return acc.concat(blockParent(cell, [code, outputs]));
179183
}
180184
return acc;
181185
},
Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
export const SPEC_VERSION = 2;
1+
export const SPEC_VERSION = 3;

‎packages/myst-cli/src/transforms/code.spec.ts‎

Lines changed: 21 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -162,7 +162,10 @@ function build_mdast(tags: string[], has_output: boolean) {
162162
],
163163
};
164164
if (has_output) {
165-
mdast.children[0].children.push({ type: 'output' });
165+
mdast.children[0].children.push({
166+
type: 'outputs',
167+
children: [{ type: 'output', children: [] }],
168+
});
166169
}
167170
return mdast;
168171
}
@@ -261,7 +264,7 @@ describe('propagateBlockDataToCode', () => {
261264
const mdast = build_mdast([tag], has_output);
262265
propagateBlockDataToCode(new Session(), new VFile(), mdast);
263266
let result = '';
264-
const outputNode = mdast.children[0].children[1];
267+
const outputsNode = mdast.children[0].children[1];
265268
switch (target) {
266269
case 'cell':
267270
result = mdast.children[0].visibility;
@@ -270,12 +273,14 @@ describe('propagateBlockDataToCode', () => {
270273
result = mdast.children[0].children[0].visibility;
271274
break;
272275
case 'output':
273-
if (!has_output && target == 'output') {
274-
expect(outputNode).toEqual(undefined);
276+
if (!has_output) {
277+
expect(outputsNode).toEqual(undefined);
275278
continue;
276279
}
277-
result = outputNode.visibility;
280+
result = outputsNode.visibility;
278281
break;
282+
default:
283+
throw new Error();
279284
}
280285
expect(result).toEqual(action);
281286
}
@@ -290,18 +295,18 @@ describe('propagateBlockDataToCode', () => {
290295
propagateBlockDataToCode(new Session(), new VFile(), mdast);
291296
const blockNode = mdast.children[0];
292297
const codeNode = mdast.children[0].children[0];
293-
const outputNode = mdast.children[0].children[1];
298+
const outputsNode = mdast.children[0].children[1];
294299
expect(blockNode.visibility).toEqual(action);
295300
expect(codeNode.visibility).toEqual(action);
296301
if (has_output) {
297-
expect(outputNode.visibility).toEqual(action);
302+
expect(outputsNode.visibility).toEqual(action);
298303
} else {
299-
expect(outputNode).toEqual(undefined);
304+
expect(outputsNode).toEqual(undefined);
300305
}
301306
}
302307
}
303308
});
304-
it('placeholder creates image node child of output', async () => {
309+
it('placeholder creates image node child of outputs', async () => {
305310
const mdast: any = {
306311
type: 'root',
307312
children: [
@@ -313,7 +318,8 @@ describe('propagateBlockDataToCode', () => {
313318
executable: true,
314319
},
315320
{
316-
type: 'output',
321+
type: 'outputs',
322+
children: [],
317323
},
318324
],
319325
data: {
@@ -323,12 +329,12 @@ describe('propagateBlockDataToCode', () => {
323329
],
324330
};
325331
propagateBlockDataToCode(new Session(), new VFile(), mdast);
326-
const outputNode = mdast.children[0].children[1];
327-
expect(outputNode.children?.length).toEqual(1);
328-
expect(outputNode.children[0].type).toEqual('image');
329-
expect(outputNode.children[0].placeholder).toBeTruthy();
332+
const outputsNode = mdast.children[0].children[1];
333+
expect(outputsNode.children?.length).toEqual(1);
334+
expect(outputsNode.children[0].type).toEqual('image');
335+
expect(outputsNode.children[0].placeholder).toBeTruthy();
330336
});
331-
it('placeholder passes with no output', async () => {
337+
it('placeholder passes with no outputs', async () => {
332338
const mdast: any = {
333339
type: 'root',
334340
children: [

‎packages/myst-cli/src/transforms/code.ts‎

Lines changed: 9 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import type { GenericNode, GenericParent } from 'myst-common';
22
import { NotebookCellTags, RuleId, fileError, fileWarn } from 'myst-common';
3-
import type { Image, Output } from 'myst-spec-ext';
3+
import type { Image, Outputs } from 'myst-spec-ext';
44
import { select, selectAll } from 'unist-util-select';
55
import yaml from 'js-yaml';
66
import type { VFile } from 'vfile';
@@ -156,10 +156,9 @@ export function propagateBlockDataToCode(session: ISession, vfile: VFile, mdast:
156156
const blocks = selectAll('block', mdast) as GenericNode[];
157157
blocks.forEach((block) => {
158158
if (!block.data) return;
159-
const outputNode = select('output', block) as Output | null;
160-
if (block.data.placeholder && outputNode) {
161-
if (!outputNode.children) outputNode.children = [];
162-
outputNode.children.push({
159+
const outputsNode = select('outputs', block) as Outputs | null;
160+
if (block.data.placeholder && outputsNode) {
161+
outputsNode.children.push({
163162
type: 'image',
164163
placeholder: true,
165164
url: block.data.placeholder as string,
@@ -195,21 +194,21 @@ export function propagateBlockDataToCode(session: ISession, vfile: VFile, mdast:
195194
if (codeNode) codeNode.visibility = 'remove';
196195
break;
197196
case NotebookCellTags.hideOutput:
198-
if (outputNode) outputNode.visibility = 'hide';
197+
if (outputsNode) outputsNode.visibility = 'hide';
199198
break;
200199
case NotebookCellTags.removeOutput:
201-
if (outputNode) outputNode.visibility = 'remove';
200+
if (outputsNode) outputsNode.visibility = 'remove';
202201
break;
203202
case NotebookCellTags.scrollOutput:
204-
if (outputNode) outputNode.scroll = true;
203+
if (outputsNode) outputsNode.scroll = true;
205204
break;
206205
default:
207206
session.log.debug(`tag '${tag}' is not valid in code-cell tags'`);
208207
}
209208
});
210209
if (!block.visibility) block.visibility = 'show';
211210
if (codeNode && !codeNode.visibility) codeNode.visibility = 'show';
212-
if (outputNode && !outputNode.visibility) outputNode.visibility = 'show';
211+
if (outputsNode && !outputsNode.visibility) outputsNode.visibility = 'show';
213212
});
214213
}
215214

@@ -236,7 +235,7 @@ export function transformLiftCodeBlocksInJupytext(mdast: GenericParent) {
236235
child.type === 'block' &&
237236
child.children?.length === 2 &&
238237
child.children?.[0].type === 'code' &&
239-
child.children?.[1].type === 'output'
238+
child.children?.[1].type === 'outputs'
240239
) {
241240
newBlocks.push(child as GenericParent);
242241
newBlocks.push({ type: 'block', children: [] });

‎packages/myst-cli/src/transforms/outputs.spec.ts‎

Lines changed: 86 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -20,17 +20,40 @@ describe('reduceOutputs', () => {
2020
],
2121
},
2222
{
23-
type: 'output',
24-
id: 'abc123',
25-
data: [],
23+
type: 'outputs',
24+
children: [
25+
{
26+
type: 'output',
27+
id: 'abc123',
28+
jupyter_data: null,
29+
children: [],
30+
},
31+
],
2632
},
2733
],
2834
},
2935
],
3036
};
31-
expect(mdast.children[0].children.length).toEqual(2);
3237
reduceOutputs(new Session(), mdast, 'notebook.ipynb', '/my/folder');
33-
expect(mdast.children[0].children.length).toEqual(1);
38+
expect(mdast).toEqual({
39+
type: 'root',
40+
children: [
41+
{
42+
type: 'block',
43+
children: [
44+
{
45+
type: 'paragraph',
46+
children: [
47+
{
48+
type: 'text',
49+
value: 'hi',
50+
},
51+
],
52+
},
53+
],
54+
},
55+
],
56+
});
3457
});
3558
it('output with complex data is removed', async () => {
3659
const mdast = {
@@ -49,18 +72,22 @@ describe('reduceOutputs', () => {
4972
],
5073
},
5174
{
52-
type: 'output',
75+
type: 'outputs',
5376
id: 'abc123',
54-
data: [
77+
children: [
5578
{
56-
output_type: 'display_data',
57-
execution_count: 3,
58-
metadata: {},
59-
data: {
60-
'application/octet-stream': {
61-
content_type: 'application/octet-stream',
62-
hash: 'def456',
63-
path: '/my/path/def456.png',
79+
type: 'output',
80+
children: [],
81+
jupyter_data: {
82+
output_type: 'display_data',
83+
execution_count: 3,
84+
metadata: {},
85+
data: {
86+
'application/octet-stream': {
87+
content_type: 'application/octet-stream',
88+
hash: 'def456',
89+
path: '/my/path/def456.png',
90+
},
6491
},
6592
},
6693
},
@@ -72,9 +99,27 @@ describe('reduceOutputs', () => {
7299
};
73100
expect(mdast.children[0].children.length).toEqual(2);
74101
reduceOutputs(new Session(), mdast, 'notebook.ipynb', '/my/folder');
75-
expect(mdast.children[0].children.length).toEqual(1);
102+
expect(mdast).toEqual({
103+
type: 'root',
104+
children: [
105+
{
106+
type: 'block',
107+
children: [
108+
{
109+
type: 'paragraph',
110+
children: [
111+
{
112+
type: 'text',
113+
value: 'hi',
114+
},
115+
],
116+
},
117+
],
118+
},
119+
],
120+
});
76121
});
77-
it('output is replaced with placeholder image', async () => {
122+
it('outputs is replaced with placeholder image', async () => {
78123
const mdast = {
79124
type: 'root',
80125
children: [
@@ -91,9 +136,8 @@ describe('reduceOutputs', () => {
91136
],
92137
},
93138
{
94-
type: 'output',
139+
type: 'outputs',
95140
id: 'abc123',
96-
data: [],
97141
children: [
98142
{
99143
type: 'image',
@@ -108,11 +152,29 @@ describe('reduceOutputs', () => {
108152
};
109153
expect(mdast.children[0].children.length).toEqual(2);
110154
reduceOutputs(new Session(), mdast, 'notebook.ipynb', '/my/folder');
111-
expect(mdast.children[0].children.length).toEqual(2);
112-
expect(mdast.children[0].children[1]).toEqual({
113-
type: 'image',
114-
placeholder: true,
115-
url: 'placeholder.png',
155+
expect(mdast).toEqual({
156+
type: 'root',
157+
children: [
158+
{
159+
type: 'block',
160+
children: [
161+
{
162+
type: 'paragraph',
163+
children: [
164+
{
165+
type: 'text',
166+
value: 'hi',
167+
},
168+
],
169+
},
170+
{
171+
type: 'image',
172+
placeholder: true,
173+
url: 'placeholder.png',
174+
},
175+
],
176+
},
177+
],
116178
});
117179
});
118180
// // These tests now require file IO...

0 commit comments

Comments
 (0)