使用 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端,
- 导入下划线以便我们可以扩展对象:
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)
})
这告诉小部件管理器在反序列化 children
属性时使用 widgets.unpack_models
函数。我们也许可以在这里使用 Object.assign
而不是下划线,这将消除对下划线的依赖。
- 导出模型:
return {
SubView : SubView,
TestView : TestView,
TestModel : TestModel
};
自然界的例子
我可以在 IPyleaflet 代码库中找到与此匹配的模式 here。特意在LeafletLayerModel
class.
对于使用更现代(babelified)语法的示例,我的 gmaps
包使用小部件反序列化 here。
总结
参见玩具示例 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端,
- 导入下划线以便我们可以扩展对象:
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)
})
这告诉小部件管理器在反序列化 children
属性时使用 widgets.unpack_models
函数。我们也许可以在这里使用 Object.assign
而不是下划线,这将消除对下划线的依赖。
- 导出模型:
return {
SubView : SubView,
TestView : TestView,
TestModel : TestModel
};
自然界的例子
我可以在 IPyleaflet 代码库中找到与此匹配的模式 here。特意在LeafletLayerModel
class.
对于使用更现代(babelified)语法的示例,我的 gmaps
包使用小部件反序列化 here。