使用 ViewList 和 create_child_view 在 ipywidgets 中创建子小部件会产生错误

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

总结

参见玩具示例 Azure 笔记本 hosted at this link。可以从那里克隆笔记本并 运行,或从那里下载并 运行 本地,但为了方便起见,所有代码也在下面。

当所有单元格都是运行时,javascript控制台在最后一个单元格中报告这些错误(缩写),并且最终预期的输出行没有呈现:

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

不确定我哪里错了。

更新:

因为它当前存在,代码将字符串实例(而不是 DOMWidgetModel 实例)发送到 create_child_view 方法。该字符串包含附加了模型 ID 的 "IPY_MODEL_"。这似乎是问题的根源。客户端正在从服务器端接收该字符串实例 Backbone children 模型数组项 (this.model.get('children')).

我想知道问题是否与 low-level widget tutorial 中讨论的小部件 [反] 序列化有关。但我不确定如何使用它来解决这个问题,因为我需要访问子小部件模型本身而不仅仅是一个属性。而且我相信我正确地传递了教程指定的 **widgets.widget_serialization


详情

笔记本包含 python 和 javascript 代码,并利用 ipywidgets 库,该库严重依赖 Backbone。后端代码(python,单元格 #1)创建一个 ipywidgets.DOMWidget subclass 小部件,Test(在前端镜像的 Backbone 模型)。前端代码(javascript,单元格 #2)创建了一个 ipywidgets.DOMWidgetView subclass,TestView,它在呈现到页面时由小部件实例化。

Test 模型小部件有一个 children 成员,由多个 "sub-widgets"(也是模型)组成。这些小部件是 python class Sub 的实例。当呈现 Test 的视图时,我想实例化并呈现子窗口小部件的视图并将它们附加到父 Test 窗口小部件的视图(注意:最后一部分尚未实现还低于)。

问题是,当我尝试按照 ipywidgets API 创建子视图时,通过使用 create_child_view 实例化子视图来填充 ViewList 数组每个子模型上的方法都不起作用。

这种事情的 API 没有特别详细的记录,所以我尽力遵循各种类似的例子,说明如何使用父视图中的子模型实例化子视图,例如父小部件 in ipywidgets itself and in ipyleaflet。但我所做的一切似乎都无法创建儿童视图。

请注意,我能够毫无问题地 单独 呈现每个 Sub 小部件的视图。只有当我尝试使用 create_child_view 方法从父 Test 小部件中创建视图时,我们 运行 才会遇到问题。


代码

Cell 1(服务器端 jupyter python 内核)

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(前端jupyter notebook代码)

%%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,
    };

});

单元格 3(python 测试代码)

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

输出

当前输出:

subview rendering  

subview rendering  

subview rendering  

subview rendering

预期输出:

subview rendering  

subview rendering  

subview rendering  

subview rendering  

rendered test_view

如果有人感兴趣,请访问 this github issue 了解有关我正在从事的实际项目的更多具体信息。

您需要明确告诉前端如何 de-serialize 小部件,即如何将字符串 "IPY_MODEL_*" 转换为实际模型。

您可以通过在前端显式定义模型并为 children 属性设置自定义 de-serializer 来实现。这是您在 Python 侧添加到 children traitlet 的 **widgets.widget_serialization 序列化程序的对应项。

笔记本

这是呈现子项的笔记本的修改版本:

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

全部更改

内核端,维护对JS模型的显式引用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

然后,在JS端,

  1. 导入下划线以便我们可以扩展对象:
require.undef('test');

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

  1. 定义模型模块:
    var TestModel = widgets.DOMWidgetModel.extend({}, {
        serializers: _.extend({
            children: { deserialize: widgets.unpack_models }
        }, widgets.WidgetModel.serializers)
    })

这告诉小部件管理器在反序列化 children 属性时使用 widgets.unpack_models 函数。我们也许可以在这里使用 Object.assign 而不是下划线,这将消除对下划线的依赖。

  1. 导出模型:
    return {
        SubView : SubView,
        TestView : TestView,
        TestModel : TestModel
    };

自然界的例子

我可以在 IPyleaflet 代码库中找到与此匹配的模式 here。特意在LeafletLayerModelclass.

对于使用更现代(babelified)语法的示例,我的 gmaps 包使用小部件反序列化 here