See the toy example Azure notebook hosted at this link. The notebook can be cloned and run, or downloaded and run locally, from there, but all of the code is also below for convenience.
When all the cells are run, the javascript console reports these errors (abbreviated) in the final cell, and the final expected line of output does not render:
Error: Could not create a view for model id 91700d0eb745433eaee98bca2d9f3fc8
at promiseRejection (utils.js:119)
Error: Could not create view
at promiseRejection (utils.js:119)
Uncaught (in promise) TypeError: Cannot read property 'then' of undefined
Uncaught (in promise) TypeError: Cannot read property 'then' of undefined
Not sure where I am going wrong.
UPDATE:
As it currently exists, the code sends a string instance (rather than a DOMWidgetModel
instance) to the create_child_view
method. The string contains "IPY_MODEL_"
appended by the model id. This seems like it might be the root of the problem. That string instance is being received by the client from the server side Backbone children
model array items (this.model.get('children')
).
I am wondering if the problem is related to the [de]serialization of widgets discussed in the low-level widget tutorial. But I'm not sure how to use that to fix this problem, since I need access to the sub widget model itself and not just an attribute. And I believe I am correctly passing the **widgets.widget_serialization
as the tutorial specifies.
The notebook contains python and javascript code, and utilizes the ipywidgets
library, which relies heavily on Backbone. The back end code (python, cell #1) creates a ipywidgets.DOMWidget
subclass widget, Test
(a Backbone model mirrored in the front end). The front end code (javascript, cell #2) creates a ipywidgets.DOMWidgetView
subclass, TestView
, which is instantiated by the widget when it is rendered to the page.
The Test
model widget has a children
member made up of multiple "sub-widgets" (which are also models). These widgets are instances of the python class Sub
. When a view of Test
is rendered, I want to instantiate and render the views of the children widgets and attach them to the view of the parent Test
widget (note: that final part hasn't been implemented yet below).
The problem is that when I try to follow the ipywidgets
API to create children views, populating the ViewList
array by instantiating the children views using the create_child_view
method on each child model is not working.
The API for this kind of thing isn't particularly well documented, so I'm doing my best to follow various similar examples of how to instantiate sub-views using child models from within a parent view, such as the parent widgets in ipywidgets
itself and in ipyleaflet
. But nothing I do seems to get the creation of children views working.
Note that I am able to render a view of each Sub
widget individually without any problem. It is only when I try to use the create_child_view
method to create a view from within the parent Test
widget that we run into problems.
Cell 1 (server side jupyter python kernel)
import ipywidgets.widgets as widgets
from traitlets import Unicode, List, Instance
from IPython.display import display
class Sub(widgets.DOMWidget):
"""Widget intended to be part of the view of another widget."""
_view_name = Unicode('SubView').tag(sync=True)
_view_module = Unicode('test').tag(sync=True)
_view_module_version = Unicode('0.1.0').tag(sync=True)
class Test(widgets.DOMWidget):
"""A parent widget intended to be made up of child widgets."""
_view_name = Unicode('TestView').tag(sync=True)
_view_module = Unicode('test').tag(sync=True)
_view_module_version = Unicode('0.1.0').tag(sync=True)
children = List(Instance(widgets.Widget)).tag(sync=True,
**widgets.widget_serialization)
def __init__(self, subs):
super().__init__()
self.children = list(subs)
Cell 2 (front end jupyter notebook code)
%%javascript
require.undef('test');
define('test', ["@jupyter-widgets/base"], function(widgets) {
var SubView = widgets.DOMWidgetView.extend({
initialize: function() {
console.log('init SubView');
SubView.__super__.initialize.apply(this, arguments);
},
render: function() {
this.el.textContent = "subview rendering";
},
});
var TestView = widgets.DOMWidgetView.extend({
initialize: function() {
console.log('init TestView');
TestView.__super__.initialize.apply(this, arguments);
this.views = new widgets.ViewList(this.add_view, null, this);
this.listenTo(this.model, 'change:children', function(model, value) {
this.views.update(value);
}, this);
console.log('init TestView complete');
},
add_view: function (child_model) {
// error occurs on this line:
return this.create_child_view(child_model);
},
render: function() {
this.views.update(this.model.get('children'));
this.el.textContent = 'rendered test_view';
},
});
return {
SubView : SubView,
TestView : TestView,
};
});
Cell 3 (python code for testing)
models=[Sub() for _ in range(4)]
for m in models:
# view each Sub object individually
display(m) # output: 'subview rendering'
t=Test(models)
t # output: 'rendered test_view' <-- broken; see console log
Current output:
subview rendering
subview rendering
subview rendering
subview rendering
Expected output:
subview rendering
subview rendering
subview rendering
subview rendering
rendered test_view
More specific information about the actual project I am working on is at this github issue if anyone is interested.
You need to explicitly tell the frontend how to de-serialize widgets, i.e. how to turn the string "IPY_MODEL_*"
into an actual model.
You do this by explicitly defining a model on the frontend side and setting a custom de-serializer for the children
attribute. This is the counterpart to the **widgets.widget_serialization
serializers you added to the children
traitlet on the Python side.
Here's a modified version of the notebook that renders the children:
https://gist.github.com/pbugnion/63cf43b41ec0eed2d0b7e7426d1c67d2
Kernel side, maintain an explicit reference to the JS model class:
class Test(widgets.DOMWidget):
_model_name = Unicode('TestModel').tag(sync=True) # reference to JS model class
_model_module = Unicode('test').tag(sync=True) # reference to JS model module
# all the rest is unchanged
_view_name = Unicode('TestView').tag(sync=True)
_view_module = Unicode('test').tag(sync=True)
_view_module_version = Unicode('0.1.0').tag(sync=True)
children = List(Instance(widgets.Widget)).tag(sync=True, **widgets.widget_serialization)
def __init__(self, subs):
super().__init__()
self.children = subs
Then, on the JS side,
require.undef('test');
define('test', ["@jupyter-widgets/base", "underscore"], function(widgets, _) {
var TestModel = widgets.DOMWidgetModel.extend({}, {
serializers: _.extend({
children: { deserialize: widgets.unpack_models }
}, widgets.WidgetModel.serializers)
})
This tells the widget manager to use the widgets.unpack_models
function when deserializing the children
attribute. We might be able to use Object.assign
instead of underscore here, which would remove the underscore dependency.
return {
SubView : SubView,
TestView : TestView,
TestModel : TestModel
};
I could find a pattern that matches this in the IPyleaflet code base here. Look specially in the LeafletLayerModel
class.
For an example that uses a more modern (babelified) syntax, my gmaps
package uses widget deserialization here.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With