Skip to content

Commit 280886e

Browse files
authored
Merge pull request #201 from davidbrochart/output
Change notebook code cell stream output schema
2 parents e76549a + 06565d5 commit 280886e

File tree

6 files changed

+134
-6
lines changed

6 files changed

+134
-6
lines changed

javascript/src/ycell.ts

Lines changed: 31 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -753,12 +753,38 @@ export class YCodeCell
753753
set outputs(v: Array<nbformat.IOutput>) {
754754
this.setOutputs(v);
755755
}
756+
get youtputs(): Y.Array<any> {
757+
return this._youtputs;
758+
}
756759

757760
/**
758761
* Execution, display, or stream outputs.
759762
*/
760763
getOutputs(): Array<nbformat.IOutput> {
761-
return JSONExt.deepCopy(this._youtputs.toArray());
764+
return JSONExt.deepCopy(this._youtputs.toJSON());
765+
}
766+
767+
createOutputs(outputs: Array<nbformat.IOutput>): Array<any> {
768+
const newOutputs: Array<any> = [];
769+
for (const output of outputs) {
770+
let _newOutput: { [id: string]: any };
771+
const newOutput = new Y.Map();
772+
if (output.output_type === 'stream') {
773+
// Set the text field as a Y.Array
774+
const { text, ...outputWithoutText } = output;
775+
_newOutput = outputWithoutText;
776+
const newText = new Y.Array();
777+
newText.push(text as string[]);
778+
_newOutput['text'] = newText;
779+
} else {
780+
_newOutput = output;
781+
}
782+
for (const [key, value] of Object.entries(_newOutput)) {
783+
newOutput.set(key, value);
784+
}
785+
newOutputs.push(newOutput);
786+
}
787+
return newOutputs;
762788
}
763789

764790
/**
@@ -767,7 +793,8 @@ export class YCodeCell
767793
setOutputs(outputs: Array<nbformat.IOutput>): void {
768794
this.transact(() => {
769795
this._youtputs.delete(0, this._youtputs.length);
770-
this._youtputs.insert(0, outputs);
796+
const newOutputs = this.createOutputs(outputs);
797+
this._youtputs.insert(0, newOutputs);
771798
}, false);
772799
}
773800

@@ -789,7 +816,8 @@ export class YCodeCell
789816
end < this._youtputs.length ? end - start : this._youtputs.length - start;
790817
this.transact(() => {
791818
this._youtputs.delete(start, fin);
792-
this._youtputs.insert(start, outputs);
819+
const newOutputs = this.createOutputs(outputs);
820+
this._youtputs.insert(start, newOutputs);
793821
}, false);
794822
}
795823

jupyter_ydoc/ynotebook.py

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -157,7 +157,12 @@ def create_ycell(self, value: Dict[str, Any]) -> Map:
157157
if "attachments" in cell and not cell["attachments"]:
158158
del cell["attachments"]
159159
elif cell_type == "code":
160-
cell["outputs"] = Array(cell.get("outputs", []))
160+
outputs = cell.get("outputs", [])
161+
for idx, output in enumerate(outputs):
162+
if output.get("output_type") == "stream":
163+
output["text"] = Array(output.get("text", []))
164+
outputs[idx] = Map(output)
165+
cell["outputs"] = Array(outputs)
161166

162167
return Map(cell)
163168

tests/conftest.py

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,10 @@ async def yws_server(request):
4343
@pytest.fixture
4444
def yjs_client(request):
4545
client_id = request.param
46-
p = subprocess.Popen(f"yarn node {here / 'yjs_client_'}{client_id}.js", shell=True)
46+
p = subprocess.Popen(["node", f"{here / 'yjs_client_'}{client_id}.js"])
4747
yield p
48-
p.kill()
48+
p.terminate()
49+
try:
50+
p.wait(timeout=10)
51+
except Exception:
52+
p.kill()

tests/files/nb1.ipynb

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
{
2+
"cells": [
3+
{
4+
"cell_type": "code",
5+
"execution_count": 1,
6+
"id": "4166c837-41c7-4ada-b86e-fd9a7720a409",
7+
"metadata": {},
8+
"outputs": [
9+
{
10+
"name": "stdout",
11+
"output_type": "stream",
12+
"text": [
13+
"Hello,"
14+
]
15+
}
16+
],
17+
"source": [
18+
"print(\"Hello,\", end=\"\")"
19+
]
20+
}
21+
],
22+
"metadata": {
23+
"kernelspec": {
24+
"display_name": "Python 3 (ipykernel)",
25+
"language": "python",
26+
"name": "python3"
27+
},
28+
"language_info": {
29+
"codemirror_mode": {
30+
"name": "ipython",
31+
"version": 3
32+
},
33+
"file_extension": ".py",
34+
"mimetype": "text/x-python",
35+
"name": "python",
36+
"nbconvert_exporter": "python",
37+
"pygments_lexer": "ipython3",
38+
"version": "3.12.3"
39+
}
40+
},
41+
"nbformat": 4,
42+
"nbformat_minor": 5
43+
}

tests/test_pycrdt_yjs.py

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -74,6 +74,31 @@ async def test_ypy_yjs_0(yws_server, yjs_client):
7474
assert ytest.source == nb
7575

7676

77+
@pytest.mark.asyncio
78+
@pytest.mark.parametrize("yjs_client", "1", indirect=True)
79+
async def test_ypy_yjs_1(yws_server, yjs_client):
80+
ydoc = Doc()
81+
ynotebook = YNotebook(ydoc)
82+
nb = stringify_source(json.loads((files_dir / "nb1.ipynb").read_text()))
83+
ynotebook.source = nb
84+
async with connect("ws://localhost:1234/my-roomname") as websocket, WebsocketProvider(
85+
ydoc, websocket
86+
):
87+
output_text = ynotebook.ycells[0]["outputs"][0]["text"]
88+
assert output_text.to_py() == ["Hello,"]
89+
event = Event()
90+
91+
def callback(_event):
92+
event.set()
93+
94+
output_text.observe(callback)
95+
96+
with move_on_after(10):
97+
await event.wait()
98+
99+
assert output_text.to_py() == ["Hello,", " World!"]
100+
101+
77102
def test_plotly_renderer():
78103
"""This test checks in particular that the type cast is not breaking the data."""
79104
ydoc = Doc()

tests/yjs_client_1.js

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
/*
2+
* Copyright (c) Jupyter Development Team.
3+
* Distributed under the terms of the Modified BSD License.
4+
*/
5+
6+
import { YNotebook } from '@jupyter/ydoc'
7+
import { WebsocketProvider } from 'y-websocket'
8+
import ws from 'ws'
9+
10+
const notebook = new YNotebook()
11+
12+
const wsProvider = new WebsocketProvider(
13+
'ws://localhost:1234', 'my-roomname',
14+
notebook.ydoc,
15+
{ WebSocketPolyfill: ws }
16+
)
17+
18+
wsProvider.on('sync', (isSynced) => {
19+
const cell = notebook.getCell(0)
20+
const youtput = cell.youtputs.get(0)
21+
const text = youtput.get('text')
22+
text.insert(1, [' World!'])
23+
})

0 commit comments

Comments
 (0)