Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Child widget creation in ipywidgets produces an error using ViewList and create_child_view

Summary

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.


Details

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.


Code

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

Output

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.

like image 671
Rick supports Monica Avatar asked Oct 16 '22 14:10

Rick supports Monica


1 Answers

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.

Notebook

Here's a modified version of the notebook that renders the children:

https://gist.github.com/pbugnion/63cf43b41ec0eed2d0b7e7426d1c67d2

Full changes

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,

  1. Import underscore so we can extend objects:
require.undef('test');

define('test', ["@jupyter-widgets/base", "underscore"], function(widgets, _) {

  1. Define your model module:
    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.

  1. Export your model:
    return {
        SubView : SubView,
        TestView : TestView,
        TestModel : TestModel
    };

Examples in the wild

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.

like image 143
Pascal Bugnion Avatar answered Nov 15 '22 05:11

Pascal Bugnion