Skip to content

Commit 77ba557

Browse files
committed
Backport #1958: Add an output callback override stack
Fix a subtle async bug in processing comm messages. Basically, message processing did not wait for any promises a comm message handler may return to resolve, because comms were not set up to resolve return values of message handlers. This allows us to override output callbacks to redirect output messages, and is used to implement the Output widget, for example. It does not redirect status messages or messages on other channels. Write the get_output_callback_id function that we use. Return after error condition. Some tests for output callback overrides.
1 parent 4727efc commit 77ba557

File tree

3 files changed

+215
-26
lines changed

3 files changed

+215
-26
lines changed

notebook/static/services/kernels/comm.js

Lines changed: 5 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -129,12 +129,9 @@ define([
129129
}
130130

131131
this.comms[content.comm_id] = this.comms[content.comm_id].then(function(comm) {
132-
try {
133-
comm.handle_msg(msg);
134-
} catch (e) {
135-
console.log("Exception handling comm msg: ", e, e.stack, msg);
136-
}
137-
return comm;
132+
return (Promise.resolve(comm.handle_msg(msg))
133+
.catch(utils.reject('Exception handling comm message'))
134+
.then(function() {return comm;}));
138135
});
139136
return this.comms[content.comm_id];
140137
};
@@ -194,15 +191,15 @@ define([
194191
var callback = this['_' + key + '_callback'];
195192
if (callback) {
196193
try {
197-
callback(msg);
194+
return callback(msg);
198195
} catch (e) {
199196
console.log("Exception in Comm callback", e, e.stack, msg);
200197
}
201198
}
202199
};
203200

204201
Comm.prototype.handle_msg = function (msg) {
205-
this._callback('msg', msg);
202+
return this._callback('msg', msg);
206203
};
207204

208205
Comm.prototype.handle_close = function (msg) {

notebook/static/services/kernels/kernel.js

Lines changed: 65 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,7 @@ define([
4242
this.username = "username";
4343
this.session_id = utils.uuid();
4444
this._msg_callbacks = {};
45+
this._msg_callbacks_overrides = {};
4546
this._msg_queue = Promise.resolve();
4647
this.info_reply = {}; // kernel_info_reply stored here after starting
4748

@@ -301,6 +302,8 @@ define([
301302
this.events.trigger('kernel_restarting.Kernel', {kernel: this});
302303
this.stop_channels();
303304

305+
this._msg_callbacks = {};
306+
this._msg_callbacks_overrides = {};
304307
var that = this;
305308
var on_success = function (data, status, xhr) {
306309
that.events.trigger('kernel_created.Kernel', {kernel: that});
@@ -847,6 +850,35 @@ define([
847850
}
848851
};
849852

853+
/**
854+
* Get output callbacks for a specific message.
855+
*
856+
* @function get_output_callbacks_for_msg
857+
*
858+
* Since output callbacks can be overridden, we first check the override stack.
859+
*/
860+
Kernel.prototype.get_output_callbacks_for_msg = function (msg_id) {
861+
return this.get_callbacks_for_msg(this.get_output_callback_id(msg_id));
862+
};
863+
864+
865+
/**
866+
* Get the output callback id for a message
867+
*
868+
* Since output callbacks can be redirected, this may not be the same as
869+
* the msg_id.
870+
*
871+
* @function get_output_callback_id
872+
*/
873+
Kernel.prototype.get_output_callback_id = function (msg_id) {
874+
var callback_id = msg_id;
875+
var overrides = this._msg_callbacks_overrides[msg_id];
876+
if (overrides && overrides.length > 0) {
877+
callback_id = overrides[overrides.length-1];
878+
}
879+
return callback_id
880+
}
881+
850882
/**
851883
* Clear callbacks for a specific message.
852884
*
@@ -891,10 +923,16 @@ define([
891923
*
892924
* }
893925
*
926+
* If the third parameter is truthy, the callback is set as the last
927+
* callback registered.
928+
*
894929
* @function set_callbacks_for_msg
895930
*/
896-
Kernel.prototype.set_callbacks_for_msg = function (msg_id, callbacks) {
897-
this.last_msg_id = msg_id;
931+
Kernel.prototype.set_callbacks_for_msg = function (msg_id, callbacks, save) {
932+
var remember = save || true;
933+
if (remember) {
934+
this.last_msg_id = msg_id;
935+
}
898936
if (callbacks) {
899937
// shallow-copy mapping, because we will modify it at the top level
900938
var cbcopy = this._msg_callbacks[msg_id] = this.last_msg_callbacks = {};
@@ -903,11 +941,31 @@ define([
903941
cbcopy.input = callbacks.input;
904942
cbcopy.shell_done = (!callbacks.shell);
905943
cbcopy.iopub_done = (!callbacks.iopub);
906-
} else {
944+
} else if (remember) {
907945
this.last_msg_callbacks = {};
908946
}
909947
};
910-
948+
949+
/**
950+
* Override output callbacks for a particular msg_id
951+
*/
952+
Kernel.prototype.output_callback_overrides_push = function(msg_id, callback_id) {
953+
var output_callbacks = this._msg_callbacks_overrides[msg_id];
954+
if (output_callbacks === void 0) {
955+
this._msg_callbacks_overrides[msg_id] = output_callbacks = [];
956+
}
957+
output_callbacks.push(callback_id);
958+
}
959+
960+
Kernel.prototype.output_callback_overrides_pop = function(msg_id) {
961+
var callback_ids = this._msg_callbacks_overrides[msg_id];
962+
if (!callback_ids) {
963+
console.error("Popping callback overrides, but none registered", msg_id);
964+
return;
965+
}
966+
return callback_ids.pop();
967+
}
968+
911969
Kernel.prototype._handle_ws_message = function (e) {
912970
var that = this;
913971
this._msg_queue = this._msg_queue.then(function() {
@@ -1032,7 +1090,7 @@ define([
10321090
* @function _handle_clear_output
10331091
*/
10341092
Kernel.prototype._handle_clear_output = function (msg) {
1035-
var callbacks = this.get_callbacks_for_msg(msg.parent_header.msg_id);
1093+
var callbacks = this.get_output_callbacks_for_msg(msg.parent_header.msg_id);
10361094
if (!callbacks || !callbacks.iopub) {
10371095
return;
10381096
}
@@ -1048,7 +1106,8 @@ define([
10481106
* @function _handle_output_message
10491107
*/
10501108
Kernel.prototype._handle_output_message = function (msg) {
1051-
var callbacks = this.get_callbacks_for_msg(msg.parent_header.msg_id);
1109+
var msg_id = msg.parent_header.msg_id;
1110+
var callbacks = this.get_output_callbacks_for_msg(msg_id);
10521111
if (!callbacks || !callbacks.iopub) {
10531112
// The message came from another client. Let the UI decide what to
10541113
// do with it.

notebook/tests/notebook/output.js

Lines changed: 145 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,17 @@
44

55
casper.notebook_test(function () {
66

7+
this.compare_outputs = function(results, expected) {
8+
for (var i = 0; i < results.length; i++) {
9+
var r = results[i];
10+
var ex = expected[i];
11+
this.test.assertEquals(r.output_type, ex.output_type, "output " + i + " = " + r.output_type);
12+
if (r.output_type === 'stream') {
13+
this.test.assertEquals(r.name, ex.name, "stream " + i + " = " + r.name);
14+
this.test.assertEquals(r.text, ex.text, "content " + i);
15+
}
16+
}
17+
}
718
this.test_coalesced_output = function (msg, code, expected) {
819
this.then(function () {
920
this.echo("Test coalesced output: " + msg);
@@ -24,31 +35,23 @@ casper.notebook_test(function () {
2435
return cell.output_area.outputs;
2536
});
2637
this.test.assertEquals(results.length, expected.length, "correct number of outputs");
27-
for (var i = 0; i < results.length; i++) {
28-
var r = results[i];
29-
var ex = expected[i];
30-
this.test.assertEquals(r.output_type, ex.output_type, "output " + i);
31-
if (r.output_type === 'stream') {
32-
this.test.assertEquals(r.name, ex.name, "stream " + i);
33-
this.test.assertEquals(r.text, ex.text, "content " + i);
34-
}
35-
}
38+
this.compare_outputs(results, expected);
3639
});
3740

3841
};
39-
42+
4043
this.thenEvaluate(function () {
4144
IPython.notebook.insert_cell_at_index("code", 0);
4245
var cell = IPython.notebook.get_cell(0);
4346
cell.set_text([
4447
"from __future__ import print_function",
4548
"import sys",
46-
"from IPython.display import display"
49+
"from IPython.display import display, clear_output"
4750
].join("\n")
4851
);
4952
cell.execute();
5053
});
51-
54+
5255
this.test_coalesced_output("stdout", [
5356
"print(1)",
5457
"sys.stdout.flush()",
@@ -123,4 +126,134 @@ casper.notebook_test(function () {
123126
},
124127
}]
125128
);
129+
130+
this.then(function () {
131+
this.echo("Test output callback overrides");
132+
});
133+
134+
this.thenEvaluate(function () {
135+
IPython.notebook.insert_cell_at_index("code", 0);
136+
var cell = IPython.notebook.get_cell(0);
137+
cell.set_text(["print(1)",
138+
"sys.stdout.flush()",
139+
"print(2)",
140+
"sys.stdout.flush()",
141+
"print(3, file=sys.stderr)",
142+
"sys.stdout.flush()",
143+
"display(2)",
144+
"clear_output()",
145+
"sys.stdout.flush()",
146+
"print('remove handler')",
147+
"sys.stdout.flush()",
148+
"print('back to cell')",
149+
"sys.stdout.flush()",
150+
].join('\n'));
151+
cell.execute();
152+
var kernel = IPython.notebook.kernel;
153+
var msg_id = cell.last_msg_id;
154+
var callback_id = 'mycallbackid'
155+
cell.iopub_messages = [];
156+
var add_msg = function(msg) {
157+
if (msg.content.text==="remove handler\n") {
158+
kernel.output_callback_overrides_pop(msg_id);
159+
}
160+
msg.content.output_type = msg.msg_type;
161+
cell.iopub_messages.push(msg.content);
162+
};
163+
kernel.set_callbacks_for_msg(callback_id, {
164+
iopub: {
165+
output: add_msg,
166+
clear_output: add_msg,
167+
}
168+
}, false);
169+
kernel.output_callback_overrides_push(msg_id, callback_id);
170+
});
171+
172+
this.wait_for_idle();
173+
174+
this.then(function () {
175+
var expected_callback = [{
176+
output_type: "stream",
177+
name: "stdout",
178+
text: "1\n"
179+
}, {
180+
output_type: "stream",
181+
name: "stdout",
182+
text: "2\n"
183+
}, {
184+
output_type: "stream",
185+
name: "stderr",
186+
text: "3\n"
187+
},{
188+
output_type: "display_data",
189+
},{
190+
output_type: "clear_output",
191+
},{
192+
output_type: "stream",
193+
name: "stdout",
194+
text: "remove handler\n"
195+
},]
196+
var expected_cell = [{
197+
output_type: "stream",
198+
name: "stdout",
199+
text: "back to cell\n"
200+
}]
201+
var returned = this.evaluate(function () {
202+
var cell = IPython.notebook.get_cell(0);
203+
return [cell.output_area.outputs, cell.iopub_messages];
204+
});
205+
var cell_results = returned[0];
206+
var callback_results = returned[1];
207+
this.test.assertEquals(cell_results.length, expected_cell.length, "correct number of cell outputs");
208+
this.test.assertEquals(callback_results.length, expected_callback.length, "correct number of callback outputs");
209+
this.compare_outputs(cell_results, expected_cell);
210+
this.compare_outputs(callback_results, expected_callback);
211+
});
212+
213+
this.then(function () {
214+
this.echo("Test output callback overrides get execute_results messages too");
215+
});
216+
217+
this.thenEvaluate(function () {
218+
IPython.notebook.insert_cell_at_index("code", 0);
219+
var cell = IPython.notebook.get_cell(0);
220+
cell.set_text("'end'");
221+
cell.execute();
222+
var kernel = IPython.notebook.kernel;
223+
var msg_id = cell.last_msg_id;
224+
var callback_id = 'mycallbackid2'
225+
cell.iopub_messages = [];
226+
var add_msg = function(msg) {
227+
msg.content.output_type = msg.msg_type;
228+
cell.iopub_messages.push(msg.content);
229+
};
230+
kernel.set_callbacks_for_msg(callback_id, {
231+
iopub: {
232+
output: add_msg,
233+
clear_output: add_msg,
234+
}
235+
}, false);
236+
kernel.output_callback_overrides_push(msg_id, callback_id);
237+
});
238+
239+
this.wait_for_idle();
240+
241+
this.then(function () {
242+
var expected_callback = [{
243+
output_type: "execute_result",
244+
data: {
245+
"text/plain" : "'end'"
246+
}
247+
}];
248+
var expected_cell = [];
249+
var returned = this.evaluate(function () {
250+
var cell = IPython.notebook.get_cell(0);
251+
return [cell.output_area.outputs, cell.iopub_messages];
252+
});
253+
var cell_results = returned[0];
254+
var callback_results = returned[1];
255+
this.test.assertEquals(cell_results.length, expected_cell.length, "correct number of cell outputs");
256+
this.test.assertEquals(callback_results.length, expected_callback.length, "correct number of callback outputs");
257+
this.compare_outputs(callback_results, expected_callback);
258+
});
126259
});

0 commit comments

Comments
 (0)