##// END OF EJS Templates
work-in-progress for custom js serializers
Jason Grout -
Show More
@@ -0,0 +1,50 b''
1 // Copyright (c) IPython Development Team.
2 // Distributed under the terms of the Modified BSD License.
3
4 define([
5 "base/js/utils"
6 ], function(utils){
7
8 return {
9 widget_serialization: {
10 deserialize: function deserialize_models(value, model) {
11 /**
12 * Replace model ids with models recursively.
13 */
14 var unpacked;
15 if ($.isArray(value)) {
16 unpacked = [];
17 _.each(value, function(sub_value, key) {
18 unpacked.push(deserialize_models(sub_value, model));
19 });
20 return Promise.all(unpacked);
21 } else if (value instanceof Object) {
22 unpacked = {};
23 _.each(value, function(sub_value, key) {
24 unpacked[key] = deserialize_models(sub_value, model);
25 });
26 return utils.resolve_promises_dict(unpacked);
27 } else if (typeof value === 'string' && value.slice(0,10) === "IPY_MODEL_") {
28 // get_model returns a promise already
29 return model.widget_manager.get_model(value.slice(10, value.length));
30 } else {
31 return Promise.resolve(value);
32 }
33 },
34 },
35
36 list_of_numbers: {
37 deserialize: function (value, model) {
38 /* value is a DataView */
39 /* create a float64 typed array */
40 return new Float64Array(value.buffer)
41 },
42 serialize: function (value, model) {
43 return value;
44 },
45 }
46 }
47
48
49
50 });
@@ -32,6 +32,7 b' define(["widgets/js/manager",'
32 this.state_lock = null;
32 this.state_lock = null;
33 this.id = model_id;
33 this.id = model_id;
34 this.views = {};
34 this.views = {};
35 this.serializers = {};
35 this._resolve_received_state = {};
36 this._resolve_received_state = {};
36
37
37 if (comm !== undefined) {
38 if (comm !== undefined) {
@@ -62,13 +63,13 b' define(["widgets/js/manager",'
62 return Backbone.Model.apply(this);
63 return Backbone.Model.apply(this);
63 },
64 },
64
65
65 send: function (content, callbacks) {
66 send: function (content, callbacks, buffers) {
66 /**
67 /**
67 * Send a custom msg over the comm.
68 * Send a custom msg over the comm.
68 */
69 */
69 if (this.comm !== undefined) {
70 if (this.comm !== undefined) {
70 var data = {method: 'custom', content: content};
71 var data = {method: 'custom', content: content};
71 this.comm.send(data, callbacks);
72 this.comm.send(data, callbacks, {}, buffers);
72 this.pending_msgs++;
73 this.pending_msgs++;
73 }
74 }
74 },
75 },
@@ -136,12 +137,37 b' define(["widgets/js/manager",'
136 * Handle incoming comm msg.
137 * Handle incoming comm msg.
137 */
138 */
138 var method = msg.content.data.method;
139 var method = msg.content.data.method;
140
139 var that = this;
141 var that = this;
140 switch (method) {
142 switch (method) {
141 case 'update':
143 case 'update':
142 this.state_change = this.state_change
144 this.state_change = this.state_change
143 .then(function() {
145 .then(function() {
144 return that.set_state(msg.content.data.state);
146 var state = msg.content.data.state || {};
147 var buffer_keys = msg.content.data.buffers || [];
148 var buffers = msg.buffers || [];
149 var metadata = msg.content.data.metadata || {};
150 var i,k;
151 for (var i=0; i<buffer_keys.length; i++) {
152 k = buffer_keys[i];
153 state[k] = buffers[i];
154 }
155
156 // for any metadata specifying a deserializer, set the
157 // state to a promise that resolves to the deserialized version
158 // also, store the serialization function for the attribute
159 var keys = Object.keys(metadata);
160 for (var i=0; i<keys.length; i++) {
161 k = keys[i];
162 if (metadata[k] && metadata[k].serialization) {
163 that.serializers[k] = utils.load_class.apply(that,
164 metadata[k].serialization);
165 state[k] = that.deserialize(that.serializers[k], state[k]);
166 }
167 }
168 return utils.resolve_promises_dict(state);
169 }).then(function(state) {
170 return that.set_state(state);
145 }).catch(utils.reject("Couldn't process update msg for model id '" + String(that.id) + "'", true))
171 }).catch(utils.reject("Couldn't process update msg for model id '" + String(that.id) + "'", true))
146 .then(function() {
172 .then(function() {
147 var parent_id = msg.parent_header.msg_id;
173 var parent_id = msg.parent_header.msg_id;
@@ -152,7 +178,7 b' define(["widgets/js/manager",'
152 }).catch(utils.reject("Couldn't resolve state request promise", true));
178 }).catch(utils.reject("Couldn't resolve state request promise", true));
153 break;
179 break;
154 case 'custom':
180 case 'custom':
155 this.trigger('msg:custom', msg.content.data.content);
181 this.trigger('msg:custom', msg.content.data.content, msg.buffers);
156 break;
182 break;
157 case 'display':
183 case 'display':
158 this.state_change = this.state_change.then(function() {
184 this.state_change = this.state_change.then(function() {
@@ -162,28 +188,37 b' define(["widgets/js/manager",'
162 }
188 }
163 },
189 },
164
190
191 deserialize: function(serializer, value) {
192 // given a serializer dict and a value,
193 // return a promise for the deserialized value
194 var that = this;
195 return serializer.then(function(s) {
196 if (s.deserialize) {
197 return s.deserialize(value, that);
198 } else {
199 return value;
200 }
201 });
202 },
203
165 set_state: function (state) {
204 set_state: function (state) {
166 var that = this;
205 var that = this;
167 // Handle when a widget is updated via the python side.
206 // Handle when a widget is updated via the python side.
168 return this._unpack_models(state).then(function(state) {
207 return new Promise(function(resolve, reject) {
169 that.state_lock = state;
208 that.state_lock = state;
170 try {
209 try {
171 WidgetModel.__super__.set.call(that, state);
210 WidgetModel.__super__.set.call(that, state);
172 } finally {
211 } finally {
173 that.state_lock = null;
212 that.state_lock = null;
174 }
213 }
214 resolve();
175 }).catch(utils.reject("Couldn't set model state", true));
215 }).catch(utils.reject("Couldn't set model state", true));
176 },
216 },
177
217
178 get_state: function() {
218 get_state: function() {
179 // Get the serializable state of the model.
219 // Get the serializable state of the model.
180 var state = this.toJSON();
220 // Equivalent to Backbone.Model.toJSON()
181 for (var key in state) {
221 return _.clone(this.attributes);
182 if (state.hasOwnProperty(key)) {
183 state[key] = this._pack_models(state[key]);
184 }
185 }
186 return state;
187 },
222 },
188
223
189 _handle_status: function (msg, callbacks) {
224 _handle_status: function (msg, callbacks) {
@@ -243,6 +278,19 b' define(["widgets/js/manager",'
243 * Handle sync to the back-end. Called when a model.save() is called.
278 * Handle sync to the back-end. Called when a model.save() is called.
244 *
279 *
245 * Make sure a comm exists.
280 * Make sure a comm exists.
281
282 * Parameters
283 * ----------
284 * method : create, update, patch, delete, read
285 * create/update always send the full attribute set
286 * patch - only send attributes listed in options.attrs, and if we are queuing
287 * up messages, combine with previous messages that have not been sent yet
288 * model : the model we are syncing
289 * will normally be the same as `this`
290 * options : dict
291 * the `attrs` key, if it exists, gives an {attr: value} dict that should be synced,
292 * otherwise, sync all attributes
293 *
246 */
294 */
247 var error = options.error || function() {
295 var error = options.error || function() {
248 console.error('Backbone sync error:', arguments);
296 console.error('Backbone sync error:', arguments);
@@ -252,8 +300,11 b' define(["widgets/js/manager",'
252 return false;
300 return false;
253 }
301 }
254
302
255 // Delete any key value pairs that the back-end already knows about.
303 var attrs = (method === 'patch') ? options.attrs : model.get_state(options);
256 var attrs = (method === 'patch') ? options.attrs : model.toJSON(options);
304
305 // the state_lock lists attributes that are currently be changed right now from a kernel message
306 // we don't want to send these non-changes back to the kernel, so we delete them out of attrs
307 // (but we only delete them if the value hasn't changed from the value stored in the state_lock
257 if (this.state_lock !== null) {
308 if (this.state_lock !== null) {
258 var keys = Object.keys(this.state_lock);
309 var keys = Object.keys(this.state_lock);
259 for (var i=0; i<keys.length; i++) {
310 for (var i=0; i<keys.length; i++) {
@@ -264,8 +315,6 b' define(["widgets/js/manager",'
264 }
315 }
265 }
316 }
266
317
267 // Only sync if there are attributes to send to the back-end.
268 attrs = this._pack_models(attrs);
269 if (_.size(attrs) > 0) {
318 if (_.size(attrs) > 0) {
270
319
271 // If this message was sent via backbone itself, it will not
320 // If this message was sent via backbone itself, it will not
@@ -297,9 +346,7 b' define(["widgets/js/manager",'
297 } else {
346 } else {
298 // We haven't exceeded the throttle, send the message like
347 // We haven't exceeded the throttle, send the message like
299 // normal.
348 // normal.
300 var data = {method: 'backbone', sync_data: attrs};
349 this.send_sync_message(attrs, callbacks);
301 this.comm.send(data, callbacks);
302 this.pending_msgs++;
303 }
350 }
304 }
351 }
305 // Since the comm is a one-way communication, assume the message
352 // Since the comm is a one-way communication, assume the message
@@ -308,68 +355,71 b' define(["widgets/js/manager",'
308 this._buffered_state_diff = {};
355 this._buffered_state_diff = {};
309 },
356 },
310
357
311 save_changes: function(callbacks) {
312 /**
313 * Push this model's state to the back-end
314 *
315 * This invokes a Backbone.Sync.
316 */
317 this.save(this._buffered_state_diff, {patch: true, callbacks: callbacks});
318 },
319
358
320 _pack_models: function(value) {
359 send_sync_message: function(attrs, callbacks) {
321 /**
360 // prepare and send a comm message syncing attrs
322 * Replace models with model ids recursively.
323 */
324 var that = this;
361 var that = this;
325 var packed;
362 // first, build a state dictionary with key=the attribute and the value
326 if (value instanceof Backbone.Model) {
363 // being the value or the promise of the serialized value
327 return "IPY_MODEL_" + value.id;
364 var state_promise_dict = {};
328
365 var keys = Object.keys(attrs);
329 } else if ($.isArray(value)) {
366 for (var i=0; i<keys.length; i++) {
330 packed = [];
367 // bind k and v locally; needed since we have an inner async function using v
331 _.each(value, function(sub_value, key) {
368 (function(k,v) {
332 packed.push(that._pack_models(sub_value));
369 if (that.serializers[k]) {
333 });
370 state_promise_dict[k] = that.serializers[k].then(function(f) {
334 return packed;
371 if (f.serialize) {
335 } else if (value instanceof Date || value instanceof String) {
372 return f.serialize(v, that);
336 return value;
337 } else if (value instanceof Object) {
338 packed = {};
339 _.each(value, function(sub_value, key) {
340 packed[key] = that._pack_models(sub_value);
341 });
342 return packed;
343
344 } else {
373 } else {
345 return value;
374 return v;
346 }
375 }
376 })
377 } else {
378 state_promise_dict[k] = v;
379 }
380 })(keys[i], attrs[keys[i]])
381 }
382 utils.resolve_promises_dict(state_promise_dict).then(function(state) {
383 // get binary values, then send
384 var keys = Object.keys(state);
385 var buffers = [];
386 var buffer_keys = [];
387 for (var i=0; i<keys.length; i++) {
388 var key = keys[i];
389 var value = state[key];
390 if (value.buffer instanceof ArrayBuffer
391 || value instanceof ArrayBuffer) {
392 buffers.push(value);
393 buffer_keys.push(key);
394 delete state[key];
395 }
396 }
397 that.comm.send({method: 'backbone', sync_data: state, buffer_keys: buffer_keys}, callbacks, {}, buffers);
398 that.pending_msgs++;
399 })
347 },
400 },
348
401
349 _unpack_models: function(value) {
402 serialize: function(model, attrs) {
403 // Serialize the attributes into a sync message
404 var keys = Object.keys(attrs);
405 var key, value;
406 var buffers, metadata, buffer_keys, serialize;
407 for (var i=0; i<keys.length; i++) {
408 key = keys[i];
409 serialize = model.serializers[key];
410 if (serialize && serialize.serialize) {
411 attrs[key] = serialize.serialize(attrs[key]);
412 }
413 }
414 },
415
416 save_changes: function(callbacks) {
350 /**
417 /**
351 * Replace model ids with models recursively.
418 * Push this model's state to the back-end
419 *
420 * This invokes a Backbone.Sync.
352 */
421 */
353 var that = this;
422 this.save(this._buffered_state_diff, {patch: true, callbacks: callbacks});
354 var unpacked;
355 if ($.isArray(value)) {
356 unpacked = [];
357 _.each(value, function(sub_value, key) {
358 unpacked.push(that._unpack_models(sub_value));
359 });
360 return Promise.all(unpacked);
361 } else if (value instanceof Object) {
362 unpacked = {};
363 _.each(value, function(sub_value, key) {
364 unpacked[key] = that._unpack_models(sub_value);
365 });
366 return utils.resolve_promises_dict(unpacked);
367 } else if (typeof value === 'string' && value.slice(0,10) === "IPY_MODEL_") {
368 // get_model returns a promise already
369 return this.widget_manager.get_model(value.slice(10, value.length));
370 } else {
371 return Promise.resolve(value);
372 }
373 },
423 },
374
424
375 on_some_change: function(keys, callback, context) {
425 on_some_change: function(keys, callback, context) {
@@ -444,11 +494,11 b' define(["widgets/js/manager",'
444 */
494 */
445 },
495 },
446
496
447 send: function (content) {
497 send: function (content, buffers) {
448 /**
498 /**
449 * Send a custom msg associated with this view.
499 * Send a custom msg associated with this view.
450 */
500 */
451 this.model.send(content, this.callbacks());
501 this.model.send(content, this.callbacks(), buffers);
452 },
502 },
453
503
454 touch: function () {
504 touch: function () {
@@ -89,6 +89,47 b' def register(key=None):'
89 return wrap
89 return wrap
90
90
91
91
92 def _widget_to_json(x):
93 if isinstance(x, dict):
94 return {k: _widget_to_json(v) for k, v in x.items()}
95 elif isinstance(x, (list, tuple)):
96 return [_widget_to_json(v) for v in x]
97 elif isinstance(x, Widget):
98 return "IPY_MODEL_" + x.model_id
99 else:
100 return x
101
102 def _json_to_widget(x):
103 if isinstance(x, dict):
104 return {k: _json_to_widget(v) for k, v in x.items()}
105 elif isinstance(x, (list, tuple)):
106 return [_json_to_widget(v) for v in x]
107 elif isinstance(x, string_types) and x.startswith('IPY_MODEL_') and x[10:] in Widget.widgets:
108 return Widget.widgets[x[10:]]
109 else:
110 return x
111
112 widget_serialization = {
113 'from_json': _json_to_widget,
114 'to_json': lambda x: (_widget_to_json(x), {'serialization': ('widget_serialization', 'widgets/js/types')})
115 }
116
117 def _to_binary_list(x):
118 import numpy
119 return memoryview(numpy.array(x, dtype=float)), {'serialization': ('list_of_numbers', 'widgets/js/types')}
120
121 def _from_binary_list(x):
122 import numpy
123 a = numpy.frombuffer(x.tobytes(), dtype=float)
124 return list(a)
125
126 list_of_numbers = {
127 'from_json': _from_binary_list,
128 'to_json': _to_binary_list
129 }
130
131
132
92 class Widget(LoggingConfigurable):
133 class Widget(LoggingConfigurable):
93 #-------------------------------------------------------------------------
134 #-------------------------------------------------------------------------
94 # Class attributes
135 # Class attributes
@@ -216,10 +257,13 b' class Widget(LoggingConfigurable):'
216 key : unicode, or iterable (optional)
257 key : unicode, or iterable (optional)
217 A single property's name or iterable of property names to sync with the front-end.
258 A single property's name or iterable of property names to sync with the front-end.
218 """
259 """
219 self._send({
260 state, buffer_keys, buffers, metadata = self.get_state(key=key)
220 "method" : "update",
261 msg = {"method": "update", "state": state}
221 "state" : self.get_state(key=key)
262 if buffer_keys:
222 })
263 msg['buffers'] = buffer_keys
264 if metadata:
265 msg['metadata'] = metadata
266 self._send(msg, buffers=buffers)
223
267
224 def get_state(self, key=None):
268 def get_state(self, key=None):
225 """Gets the widget state, or a piece of it.
269 """Gets the widget state, or a piece of it.
@@ -228,6 +272,16 b' class Widget(LoggingConfigurable):'
228 ----------
272 ----------
229 key : unicode or iterable (optional)
273 key : unicode or iterable (optional)
230 A single property's name or iterable of property names to get.
274 A single property's name or iterable of property names to get.
275
276 Returns
277 -------
278 state : dict of states
279 buffer_keys : list of strings
280 the values that are stored in buffers
281 buffers : list of binary memoryviews
282 values to transmit in binary
283 metadata : dict
284 metadata for each field: {key: metadata}
231 """
285 """
232 if key is None:
286 if key is None:
233 keys = self.keys
287 keys = self.keys
@@ -238,11 +292,21 b' class Widget(LoggingConfigurable):'
238 else:
292 else:
239 raise ValueError("key must be a string, an iterable of keys, or None")
293 raise ValueError("key must be a string, an iterable of keys, or None")
240 state = {}
294 state = {}
295 buffers = []
296 buffer_keys = []
297 metadata = {}
241 for k in keys:
298 for k in keys:
242 f = self.trait_metadata(k, 'to_json', self._trait_to_json)
299 f = self.trait_metadata(k, 'to_json', self._trait_to_json)
243 value = getattr(self, k)
300 value = getattr(self, k)
244 state[k] = f(value)
301 serialized, md = f(value)
245 return state
302 if isinstance(serialized, memoryview):
303 buffers.append(serialized)
304 buffer_keys.append(k)
305 else:
306 state[k] = serialized
307 if md is not None:
308 metadata[k] = md
309 return state, buffer_keys, buffers, metadata
246
310
247 def set_state(self, sync_data):
311 def set_state(self, sync_data):
248 """Called when a state is received from the front-end."""
312 """Called when a state is received from the front-end."""
@@ -253,15 +317,17 b' class Widget(LoggingConfigurable):'
253 with self._lock_property(name, json_value):
317 with self._lock_property(name, json_value):
254 setattr(self, name, from_json(json_value))
318 setattr(self, name, from_json(json_value))
255
319
256 def send(self, content):
320 def send(self, content, buffers=None):
257 """Sends a custom msg to the widget model in the front-end.
321 """Sends a custom msg to the widget model in the front-end.
258
322
259 Parameters
323 Parameters
260 ----------
324 ----------
261 content : dict
325 content : dict
262 Content of the message to send.
326 Content of the message to send.
327 buffers : list of binary buffers
328 Binary buffers to send with message
263 """
329 """
264 self._send({"method": "custom", "content": content})
330 self._send({"method": "custom", "content": content}, buffers=buffers)
265
331
266 def on_msg(self, callback, remove=False):
332 def on_msg(self, callback, remove=False):
267 """(Un)Register a custom msg receive callback.
333 """(Un)Register a custom msg receive callback.
@@ -269,9 +335,9 b' class Widget(LoggingConfigurable):'
269 Parameters
335 Parameters
270 ----------
336 ----------
271 callback: callable
337 callback: callable
272 callback will be passed two arguments when a message arrives::
338 callback will be passed three arguments when a message arrives::
273
339
274 callback(widget, content)
340 callback(widget, content, buffers)
275
341
276 remove: bool
342 remove: bool
277 True if the callback should be unregistered."""
343 True if the callback should be unregistered."""
@@ -346,7 +412,10 b' class Widget(LoggingConfigurable):'
346 # Handle backbone sync methods CREATE, PATCH, and UPDATE all in one.
412 # Handle backbone sync methods CREATE, PATCH, and UPDATE all in one.
347 if method == 'backbone':
413 if method == 'backbone':
348 if 'sync_data' in data:
414 if 'sync_data' in data:
415 # get binary buffers too
349 sync_data = data['sync_data']
416 sync_data = data['sync_data']
417 for i,k in enumerate(data.get('buffer_keys', [])):
418 sync_data[k] = msg['buffers'][i]
350 self.set_state(sync_data) # handles all methods
419 self.set_state(sync_data) # handles all methods
351
420
352 # Handle a state request.
421 # Handle a state request.
@@ -356,15 +425,15 b' class Widget(LoggingConfigurable):'
356 # Handle a custom msg from the front-end.
425 # Handle a custom msg from the front-end.
357 elif method == 'custom':
426 elif method == 'custom':
358 if 'content' in data:
427 if 'content' in data:
359 self._handle_custom_msg(data['content'])
428 self._handle_custom_msg(data['content'], msg['buffers'])
360
429
361 # Catch remainder.
430 # Catch remainder.
362 else:
431 else:
363 self.log.error('Unknown front-end to back-end widget msg with method "%s"' % method)
432 self.log.error('Unknown front-end to back-end widget msg with method "%s"' % method)
364
433
365 def _handle_custom_msg(self, content):
434 def _handle_custom_msg(self, content, buffers):
366 """Called when a custom msg is received."""
435 """Called when a custom msg is received."""
367 self._msg_callbacks(self, content)
436 self._msg_callbacks(self, content, buffers)
368
437
369 def _notify_trait(self, name, old_value, new_value):
438 def _notify_trait(self, name, old_value, new_value):
370 """Called when a property has been changed."""
439 """Called when a property has been changed."""
@@ -391,29 +460,13 b' class Widget(LoggingConfigurable):'
391 Traverse lists/tuples and dicts and serialize their values as well.
460 Traverse lists/tuples and dicts and serialize their values as well.
392 Replace any widgets with their model_id
461 Replace any widgets with their model_id
393 """
462 """
394 if isinstance(x, dict):
463 return x, None
395 return {k: self._trait_to_json(v) for k, v in x.items()}
396 elif isinstance(x, (list, tuple)):
397 return [self._trait_to_json(v) for v in x]
398 elif isinstance(x, Widget):
399 return "IPY_MODEL_" + x.model_id
400 else:
401 return x # Value must be JSON-able
402
464
403 def _trait_from_json(self, x):
465 def _trait_from_json(self, x):
404 """Convert json values to objects
466 """Convert json values to objects
405
467
406 Replace any strings representing valid model id values to Widget references.
468 Replace any strings representing valid model id values to Widget references.
407 """
469 """
408 if isinstance(x, dict):
409 return {k: self._trait_from_json(v) for k, v in x.items()}
410 elif isinstance(x, (list, tuple)):
411 return [self._trait_from_json(v) for v in x]
412 elif isinstance(x, string_types) and x.startswith('IPY_MODEL_') and x[10:] in Widget.widgets:
413 # we want to support having child widgets at any level in a hierarchy
414 # trusting that a widget UUID will not appear out in the wild
415 return Widget.widgets[x[10:]]
416 else:
417 return x
470 return x
418
471
419 def _ipython_display_(self, **kwargs):
472 def _ipython_display_(self, **kwargs):
@@ -423,9 +476,9 b' class Widget(LoggingConfigurable):'
423 self._send({"method": "display"})
476 self._send({"method": "display"})
424 self._handle_displayed(**kwargs)
477 self._handle_displayed(**kwargs)
425
478
426 def _send(self, msg):
479 def _send(self, msg, buffers=None):
427 """Sends a message to the model in the front-end."""
480 """Sends a message to the model in the front-end."""
428 self.comm.send(msg)
481 self.comm.send(data=msg, buffers=buffers)
429
482
430
483
431 class DOMWidget(Widget):
484 class DOMWidget(Widget):
@@ -6,7 +6,7 b' Represents a container that can be used to group other widgets.'
6 # Copyright (c) IPython Development Team.
6 # Copyright (c) IPython Development Team.
7 # Distributed under the terms of the Modified BSD License.
7 # Distributed under the terms of the Modified BSD License.
8
8
9 from .widget import DOMWidget, register
9 from .widget import DOMWidget, register, widget_serialization
10 from IPython.utils.traitlets import Unicode, Tuple, TraitError, Int, CaselessStrEnum
10 from IPython.utils.traitlets import Unicode, Tuple, TraitError, Int, CaselessStrEnum
11 from IPython.utils.warn import DeprecatedClass
11 from IPython.utils.warn import DeprecatedClass
12
12
@@ -18,7 +18,9 b' class Box(DOMWidget):'
18 # Child widgets in the container.
18 # Child widgets in the container.
19 # Using a tuple here to force reassignment to update the list.
19 # Using a tuple here to force reassignment to update the list.
20 # When a proper notifying-list trait exists, that is what should be used here.
20 # When a proper notifying-list trait exists, that is what should be used here.
21 children = Tuple(sync=True)
21 # TODO: make this tuple serialize models
22 # TODO: enforce that tuples here have a single datatype
23 children = Tuple(sync=True, **widget_serialization)
22
24
23 _overflow_values = ['visible', 'hidden', 'scroll', 'auto', 'initial', 'inherit', '']
25 _overflow_values = ['visible', 'hidden', 'scroll', 'auto', 'initial', 'inherit', '']
24 overflow_x = CaselessStrEnum(
26 overflow_x = CaselessStrEnum(
@@ -67,7 +67,7 b' class Button(DOMWidget):'
67 Set to true to remove the callback from the list of callbacks."""
67 Set to true to remove the callback from the list of callbacks."""
68 self._click_handlers.register_callback(callback, remove=remove)
68 self._click_handlers.register_callback(callback, remove=remove)
69
69
70 def _handle_button_msg(self, _, content):
70 def _handle_button_msg(self, _, content, buffers):
71 """Handle a msg from the front-end.
71 """Handle a msg from the front-end.
72
72
73 Parameters
73 Parameters
General Comments 0
You need to be logged in to leave comments. Login now