Merge pull request #1879 from minrk/display_data_update

support routing updating outputs with display_id
Kyle Kelley 10 years ago committed by GitHub
commit 6df840e19d

@ -1,6 +1,6 @@
// Copyright (c) Jupyter Development Team.
// Distributed under the terms of the Modified BSD License.
// jshint esnext: true
define([
'base/js/utils',
'base/js/security',
@ -34,6 +34,7 @@ define([
} else {
this.prompt_area = options.prompt_area;
}
this._display_id_targets = {};
this.create_elements();
this.style();
this.bind_events();
@ -218,22 +219,26 @@ define([
var json = {};
var msg_type = json.output_type = msg.header.msg_type;
var content = msg.content;
if (msg_type === "stream") {
switch(msg_type) {
case "stream" :
json.text = content.text;
json.name = content.name;
} else if (msg_type === "display_data") {
json.data = content.data;
json.metadata = content.metadata;
} else if (msg_type === "execute_result") {
break;
case "execute_result":
json.execution_count = content.execution_count;
case "update_display_data":
case "display_data":
json.transient = content.transient;
json.data = content.data;
json.metadata = content.metadata;
json.execution_count = content.execution_count;
} else if (msg_type === "error") {
break;
case "error":
json.ename = content.ename;
json.evalue = content.evalue;
json.traceback = content.traceback;
} else {
console.log("unhandled output message", msg);
break;
default:
console.error("unhandled output message", msg);
return;
}
this.append_output(json);
@ -298,6 +303,11 @@ define([
var record_output = true;
switch(json.output_type) {
case 'update_display_data':
record_output = false;
json = this.validate_mimebundle(json);
this.update_display_data(json);
return;
case 'execute_result':
json = this.validate_mimebundle(json);
this.append_execute_result(json);
@ -419,7 +429,7 @@ define([
.append($('<div/>').text('See your browser Javascript console for more details.').addClass('js-error'));
};
OutputArea.prototype._safe_append = function (toinsert) {
OutputArea.prototype._safe_append = function (toinsert, toreplace) {
/**
* safely append an item to the document
* this is an object created by user code,
@ -427,12 +437,16 @@ define([
* under any circumstances.
*/
try {
this.element.append(toinsert);
if (toreplace) {
toreplace.replaceWith(toinsert);
} else {
this.element.append(toinsert);
}
} catch(err) {
console.log(err);
console.error(err);
// Create an actual output_area and output_subarea, which creates
// the prompt area and the proper indentation.
var toinsert = this.create_output_area();
toinsert = this.create_output_area();
var subarea = $('<div/>').addClass('output_subarea');
toinsert.append(subarea);
this._append_javascript_error(err, subarea);
@ -447,6 +461,7 @@ define([
OutputArea.prototype.append_execute_result = function (json) {
var n = json.execution_count || ' ';
var toinsert = this.create_output_area();
this._record_display_id(json, toinsert);
if (this.prompt_area) {
toinsert.find('div.prompt').addClass('output_prompt').text('Out[' + n + ']:');
}
@ -564,8 +579,56 @@ define([
};
OutputArea.prototype.update_display_data = function (json, handle_inserted) {
var oa = this;
var targets;
var display_id = (json.transient || {}).display_id;
if (!display_id) {
console.warn("Handling update_display with no display_id", json);
return;
}
targets = this._display_id_targets[display_id];
if (!targets) {
console.warn("No targets for display_id", display_id, json);
return;
}
// we've seen it before, update output data
targets.map(function (target) {
oa.outputs[target.index].data = json.data;
oa.outputs[target.index].metadata = json.metadata;
var toinsert = oa.create_output_area();
if (oa.append_mime_type(json, toinsert, handle_inserted)) {
oa._safe_append(toinsert, target.element);
}
target.element = toinsert;
});
// If we just output something that could contain latex, typeset it.
if ((json.data[MIME_LATEX] !== undefined) ||
(json.data[MIME_HTML] !== undefined) ||
(json.data[MIME_MARKDOWN] !== undefined)) {
this.typeset();
}
};
OutputArea.prototype._record_display_id = function (json, element) {
// record display_id of a display_data / execute_result
var display_id = (json.transient || {}).display_id;
if (!display_id) return;
// it has a display_id;
var targets = this._display_id_targets[display_id];
if (!targets) {
targets = this._display_id_targets[display_id] = [];
}
targets.push({
index: this.outputs.length,
element: element,
});
};
OutputArea.prototype.append_display_data = function (json, handle_inserted) {
var toinsert = this.create_output_area();
this._record_display_id(json, toinsert);
if (this.append_mime_type(json, toinsert, handle_inserted)) {
this._safe_append(toinsert);
// If we just output latex, typeset it.
@ -859,7 +922,7 @@ define([
// remove form container
container.parent().remove();
// replace with plaintext version in stdout
this.append_output(content, false);
this.append_output(content);
this.events.trigger('send_input_reply.Kernel', value);
};
@ -905,6 +968,7 @@ define([
this.element.trigger('changed');
this.outputs = [];
this._display_id_targets = {};
this.trusted = true;
this.unscroll_area();
return;
@ -938,9 +1002,20 @@ define([
}
};
/**
* Return for-saving version of outputs.
* Excludes transient values.
*/
OutputArea.prototype.toJSON = function () {
return this.outputs;
return this.outputs.map(function (out) {
var out2 = {};
Object.keys(out).map(function (key) {
if (key != 'transient') {
out2[key] = out[key];
}
});
return out2;
});
};
/**

@ -41,6 +41,7 @@ define([
this.username = "username";
this.session_id = utils.uuid();
this._msg_callbacks = {};
this._display_id_to_parent_ids = {};
this._msg_queue = Promise.resolve();
this.info_reply = {}; // kernel_info_reply stored here after starting
@ -129,7 +130,7 @@ define([
* @function init_iopub_handlers
*/
Kernel.prototype.init_iopub_handlers = function () {
var output_msg_types = ['stream', 'display_data', 'execute_result', 'error'];
var output_msg_types = ['stream', 'display_data', 'execute_result', 'error', 'update_display_data'];
this._iopub_handlers = {};
this.register_iopub_handler('status', $.proxy(this._handle_status_message, this));
this.register_iopub_handler('clear_output', $.proxy(this._handle_clear_output, this));
@ -299,6 +300,8 @@ define([
*/
this.events.trigger('kernel_restarting.Kernel', {kernel: this});
this.stop_channels();
this._msg_callbacks = {};
this._display_id_to_parent_ids = {};
var that = this;
var on_success = function (data, status, xhr) {
@ -450,7 +453,7 @@ define([
var ws_host_url = this.ws_url + this.kernel_url;
console.log("Starting WebSockets:", ws_host_url);
this.ws = new this.WebSocket([
that.ws_url,
utils.url_path_join(that.kernel_url, 'channels'),
@ -631,7 +634,7 @@ define([
} else {
this._pending_messages.push(msg);
}
}
};
Kernel.prototype.send_shell_message = function (msg_type, content, callbacks, metadata, buffers) {
/**
@ -854,6 +857,25 @@ define([
*/
Kernel.prototype.clear_callbacks_for_msg = function (msg_id) {
if (this._msg_callbacks[msg_id] !== undefined ) {
var callbacks = this._msg_callbacks[msg_id];
var kernel = this;
// clear display_id:msg_id map for display_ids associated with this msg_id
if (!callbacks) return;
callbacks.display_ids.map(function (display_id) {
var msg_ids = kernel._display_id_to_parent_ids[display_id];
if (msg_ids) {
var idx = msg_ids.indexOf(msg_id);
if (idx === -1) {
return;
}
if (msg_ids.length === 1) {
delete kernel._display_id_to_parent_ids[display_id];
} else {
msg_ids.splice(idx, 1);
kernel._display_id_to_parent_ids[display_id] = msg_ids;
}
}
});
delete this._msg_callbacks[msg_id];
}
};
@ -904,6 +926,7 @@ define([
cbcopy.clear_on_done = callbacks.clear_on_done;
cbcopy.shell_done = (!callbacks.shell);
cbcopy.iopub_done = (!callbacks.iopub);
cbcopy.display_ids = [];
if (callbacks.clear_on_done === undefined) {
// default to clear-on-done
cbcopy.clear_on_done = true;
@ -1053,7 +1076,51 @@ define([
* @function _handle_output_message
*/
Kernel.prototype._handle_output_message = function (msg) {
var callbacks = this.get_callbacks_for_msg(msg.parent_header.msg_id);
var that = this;
var msg_id = msg.parent_header.msg_id;
var callbacks = this.get_callbacks_for_msg(msg_id);
if (['display_data', 'update_display_data', 'execute_result'].indexOf(msg.header.msg_type) > -1) {
// display_data messages may re-route based on their display_id
var display_id = (msg.content.transient || {}).display_id;
if (display_id) {
// it has a display_id
var parent_ids = this._display_id_to_parent_ids[display_id];
if (parent_ids) {
// we've seen it before, update existing outputs with same display_id
// by handling display_data as update_display_data
var update_msg = $.extend(true, {}, msg);
update_msg.header.msg_type = 'update_display_data';
parent_ids.map(function (parent_id) {
var callbacks = that.get_callbacks_for_msg(parent_id);
if (!callbacks) return;
var callback = callbacks.iopub.output;
if (callback) {
callback(update_msg);
}
});
}
// we're done here if it's update_display
if (msg.header.msg_type === 'update_display_data') {
// it's an update, don't proceed to the normal display
return;
}
// regular display_data with id, record it for future updating
// in _display_id_to_parent_ids for future lookup
if (this._display_id_to_parent_ids[display_id] === undefined) {
this._display_id_to_parent_ids[display_id] = [];
}
if (this._display_id_to_parent_ids[display_id].indexOf(msg_id) === -1) {
this._display_id_to_parent_ids[display_id].push(msg_id);
}
// and in callbacks for cleanup on clear_callbacks_for_msg
if (callbacks && callbacks.display_ids.indexOf(display_id) === -1) {
callbacks.display_ids.push(display_id);
}
}
}
if (!callbacks || !callbacks.iopub) {
// The message came from another client. Let the UI decide what to
// do with it.

@ -0,0 +1,82 @@
//
// Various output tests
//
casper.notebook_test(function () {
function get_outputs(cell_idx) {
var outputs_json = casper.evaluate(function (cell_idx) {
var cell = Jupyter.notebook.get_cell(cell_idx);
return JSON.stringify(cell.output_area.outputs);
}, {cell_idx: cell_idx});
return JSON.parse(outputs_json);
}
this.thenEvaluate(function () {
Jupyter.notebook.insert_cell_at_index("code", 0);
var cell = Jupyter.notebook.get_cell(0);
cell.set_text([
"ip = get_ipython()",
"from IPython.display import display",
"def display_with_id(obj, display_id, update=False):",
" iopub = ip.kernel.iopub_socket",
" session = get_ipython().kernel.session",
" data, md = ip.display_formatter.format(obj)",
" transient = {'display_id': display_id}",
" content = {'data': data, 'metadata': md, 'transient': transient}",
" msg_type = 'update_display_data' if update else 'display_data'",
" session.send(iopub, msg_type, content, parent=ip.parent_header)",
"",
].join('\n'));
cell.execute();
});
this.thenEvaluate(function () {
Jupyter.notebook.insert_cell_at_index("code", 1);
var cell = Jupyter.notebook.get_cell(1);
cell.set_text([
"display('above')",
"display_with_id(1, 'here')",
"display('below')",
].join('\n'));
cell.execute();
});
this.wait_for_output(1);
this.then(function () {
var outputs = get_outputs(1);
this.test.assertEquals(outputs.length, 3, 'cell 1 has the right number of outputs');
this.test.assertEquals(outputs[1].transient.display_id, 'here', 'has transient display_id');
this.test.assertEquals(outputs[1].data['text/plain'], '1', 'display_with_id produces output');
});
this.thenEvaluate(function () {
Jupyter.notebook.insert_cell_at_index("code", 2);
var cell = Jupyter.notebook.get_cell(2);
cell.set_text([
"display_with_id(2, 'here')",
"display_with_id(3, 'there')",
"display_with_id(4, 'here')",
].join('\n'));
cell.execute();
});
this.wait_for_output(2);
this.then(function () {
var outputs1 = get_outputs(1);
this.test.assertEquals(outputs1[1].data['text/plain'], '4', '');
this.test.assertEquals(outputs1.length, 3, 'cell 1 still has the right number of outputs');
var outputs2 = get_outputs(2);
this.test.assertEquals(outputs2.length, 3, 'cell 2 has the right number of outputs');
this.test.assertEquals(outputs2[0].transient.display_id, 'here', 'check display id 0');
this.test.assertEquals(outputs2[0].data['text/plain'], '4', 'output[2][0]');
this.test.assertEquals(outputs2[1].transient.display_id, 'there', 'display id 1');
this.test.assertEquals(outputs2[1].data['text/plain'], '3', 'output[2][1]');
this.test.assertEquals(outputs2[2].transient.display_id, 'here', 'display id 2');
this.test.assertEquals(outputs2[2].data['text/plain'], '4', 'output[2][2]');
});
});
Loading…
Cancel
Save