Skip to content

Commit 73c98f0

Browse files
authored
HTMLManager: Support both ipywidgets 7 and 8 models (#3932)
* HTMLManager: Support both ipywidgets 7 and 8 models * Linter * Fix integrity test * Add test * Fix the tests * Apply same special case for jupyter-widgets packages as in the lab manager * Reduce PR diff
1 parent c579fcd commit 73c98f0

11 files changed

+531
-81
lines changed

Diff for: docs/requirements.txt

+2-2
Original file line numberDiff line numberDiff line change
@@ -6,8 +6,8 @@ ipyleaflet
66
jupyter-client
77
jupyter-packaging
88
jupyterlab >=4
9-
jupyterlite-core >=0.3.0<0.4.0
10-
jupyterlite-pyodide-kernel >=0.3.0<0.4.0
9+
jupyterlite-core >=0.3.0,<0.4.0
10+
jupyterlite-pyodide-kernel >=0.3.0,<0.4.0
1111
matplotlib
1212
myst-nb >=0.17,<0.18
1313
numpy

Diff for: packages/html-manager/package.json

+4-1
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,9 @@
3838
"@fortawesome/fontawesome-free": "^5.12.0",
3939
"@jupyter-widgets/base": "^6.0.8",
4040
"@jupyter-widgets/base-manager": "^1.0.9",
41+
"@jupyter-widgets/base7": "npm:@jupyter-widgets/[email protected]",
4142
"@jupyter-widgets/controls": "^5.0.9",
43+
"@jupyter-widgets/controls7": "npm:@jupyter-widgets/[email protected]",
4244
"@jupyter-widgets/output": "^6.0.8",
4345
"@jupyter-widgets/schema": "^0.5.5",
4446
"@jupyterlab/outputarea": "^3.0.0 || ^4.0.0",
@@ -47,7 +49,8 @@
4749
"@lumino/messaging": "^1.10.1 || ^2.1",
4850
"@lumino/widgets": "^1.30.0 || ^2.1",
4951
"ajv": "^8.6.0",
50-
"jquery": "^3.1.1"
52+
"jquery": "^3.1.1",
53+
"semver": "^7.3.5"
5154
},
5255
"devDependencies": {
5356
"@types/jquery": "^3.5.16",

Diff for: packages/html-manager/scripts/concat-amd-build.js

+10-1
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,14 @@
44
var fs = require('fs');
55

66
// Make a script file that defines all of the relevant AMD modules
7-
var files = ['base.js', 'controls.js', 'index.js', 'libembed-amd.js'];
7+
var files = [
8+
'base.js',
9+
'controls.js',
10+
'base7.js',
11+
'controls7.js',
12+
'index.js',
13+
'libembed-amd.js',
14+
];
815
var output = files
916
.map((f) => {
1017
return fs.readFileSync('./dist/amd/' + f).toString();
@@ -17,6 +24,8 @@ fs.writeFileSync('./dist/libembed-amd.js', output);
1724
files = [
1825
'base.js',
1926
'controls.js',
27+
'base7.js',
28+
'controls7.js',
2029
'index.js',
2130
'libembed-amd.js',
2231
'embed-amd-render.js',

Diff for: packages/html-manager/src/htmlmanager.ts

+49-5
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
// Copyright (c) Jupyter Development Team.
22
// Distributed under the terms of the Modified BSD License.
33

4-
import * as widgets from '@jupyter-widgets/controls';
4+
import { maxSatisfying } from 'semver';
55
import * as base from '@jupyter-widgets/base';
66
import * as outputWidgets from './output';
77
import { ManagerBase } from '@jupyter-widgets/base-manager';
@@ -38,7 +38,7 @@ export class HTMLManager extends ManagerBase {
3838
window.addEventListener('resize', () => {
3939
this._viewList.forEach((view) => {
4040
MessageLoop.postMessage(
41-
view.luminoWidget,
41+
view.luminoWidget || view.pWidget,
4242
LuminoWidget.Widget.ResizeMessage.UnknownSize
4343
);
4444
});
@@ -66,7 +66,7 @@ export class HTMLManager extends ManagerBase {
6666
v.render();
6767
}
6868

69-
LuminoWidget.Widget.attach(v.luminoWidget, el);
69+
LuminoWidget.Widget.attach(v.luminoWidget || v.pWidget, el);
7070
this._viewList.add(v);
7171
v.once('remove', () => {
7272
this._viewList.delete(v);
@@ -112,10 +112,54 @@ export class HTMLManager extends ManagerBase {
112112
moduleVersion: string
113113
): Promise<typeof WidgetModel | typeof WidgetView> {
114114
return new Promise((resolve, reject) => {
115+
if (
116+
moduleName === '@jupyter-widgets/base' ||
117+
moduleName === '@jupyter-widgets/controls'
118+
) {
119+
moduleVersion = `^${moduleVersion}`;
120+
}
121+
115122
if (moduleName === '@jupyter-widgets/base') {
116-
resolve(base);
123+
const best = maxSatisfying(['1.2.0', '2.0.0'], moduleVersion);
124+
125+
if (best === '1.2.0') {
126+
// ipywidgets 7 model
127+
resolve(require('@jupyter-widgets/base7'));
128+
} else {
129+
// ipywidgets 8 model
130+
resolve(require('@jupyter-widgets/base'));
131+
}
117132
} else if (moduleName === '@jupyter-widgets/controls') {
118-
resolve(widgets);
133+
const best = maxSatisfying(['1.5.0', '2.0.0'], moduleVersion);
134+
135+
if (best === '1.5.0') {
136+
// ipywidgets 7 controls JS and CSS
137+
require('@jupyter-widgets/controls7/css/widgets-base.css');
138+
139+
// If lab variables are not found, we set them (we don't want to reset the variables if they are already defined)
140+
if (
141+
getComputedStyle(document.documentElement).getPropertyValue(
142+
'--jp-layout-color0'
143+
) === ''
144+
) {
145+
require('@jupyter-widgets/controls7/css/labvariables.css');
146+
}
147+
resolve(require('@jupyter-widgets/controls7'));
148+
} else {
149+
// ipywidgets 8 controls JS and CSS
150+
require('@jupyter-widgets/controls/css/widgets-base.css');
151+
152+
// If lab variables are not found, we set them (we don't want to reset the variables if they are already defined)
153+
if (
154+
getComputedStyle(document.documentElement).getPropertyValue(
155+
'--jp-layout-color0'
156+
) === ''
157+
) {
158+
require('@jupyter-widgets/controls/css/labvariables.css');
159+
}
160+
161+
resolve(require('@jupyter-widgets/controls'));
162+
}
119163
} else if (moduleName === '@jupyter-widgets/output') {
120164
resolve(outputWidgets);
121165
} else if (this.loader !== undefined) {

Diff for: packages/html-manager/src/libembed-amd.ts

+10-10
Original file line numberDiff line numberDiff line change
@@ -3,16 +3,6 @@
33

44
import * as libembed from './libembed';
55

6-
let cdn = 'https://cdn.jsdelivr.net/npm/';
7-
let onlyCDN = false;
8-
9-
// find the data-cdn for any script tag, assuming it is only used for embed-amd.js
10-
const scripts = document.getElementsByTagName('script');
11-
Array.prototype.forEach.call(scripts, (script: HTMLScriptElement) => {
12-
cdn = script.getAttribute('data-jupyter-widgets-cdn') || cdn;
13-
onlyCDN = onlyCDN || script.hasAttribute('data-jupyter-widgets-cdn-only');
14-
});
15-
166
/**
177
* Load a package using requirejs and return a promise
188
*
@@ -29,6 +19,16 @@ const requirePromise = function (pkg: string | string[]): Promise<any> {
2919
});
3020
};
3121

22+
let cdn = 'https://cdn.jsdelivr.net/npm/';
23+
let onlyCDN = false;
24+
25+
// find the data-cdn for any script tag, assuming it is only used for embed-amd.js
26+
const scripts = document.getElementsByTagName('script');
27+
Array.prototype.forEach.call(scripts, (script: HTMLScriptElement) => {
28+
cdn = script.getAttribute('data-jupyter-widgets-cdn') || cdn;
29+
onlyCDN = onlyCDN || script.hasAttribute('data-jupyter-widgets-cdn-only');
30+
});
31+
3232
function moduleNameToCDNUrl(moduleName: string, moduleVersion: string): string {
3333
let packageName = moduleName;
3434
let fileName = 'index'; // default filename

Diff for: packages/html-manager/src/libembed.ts

-10
Original file line numberDiff line numberDiff line change
@@ -11,16 +11,6 @@ import '@fortawesome/fontawesome-free/css/all.min.css';
1111
import '@fortawesome/fontawesome-free/css/v4-shims.min.css';
1212

1313
import '@lumino/widgets/style/index.css';
14-
import '@jupyter-widgets/controls/css/widgets-base.css';
15-
16-
// If lab variables are not found, we set them (we don't want to reset the variables if they are already defined)
17-
if (
18-
getComputedStyle(document.documentElement).getPropertyValue(
19-
'--jp-layout-color0'
20-
) === ''
21-
) {
22-
require('@jupyter-widgets/controls/css/labvariables.css');
23-
}
2414

2515
// Used just for the typing. We must not import the javascript because we don't
2616
// want to include it in the require embedding.

Diff for: packages/html-manager/src/output_renderers.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@ export class WidgetRenderer extends Widget implements IRenderMime.IRenderer {
2727
try {
2828
const wModel = await this._manager.get_model(source.model_id);
2929
const wView = await this._manager.create_view(wModel);
30-
Widget.attach(wView.luminoWidget, this.node);
30+
Widget.attach(wView.luminoWidget || wView.pWidget, this.node);
3131
} catch (err) {
3232
console.log('Error displaying widget');
3333
console.log(err);

Diff for: packages/html-manager/test/src/output_test.ts

+80-3
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,83 @@ describe('Output widget', function () {
6363
expect(elt.querySelectorAll('table').length).to.equal(1);
6464
});
6565

66+
it('renders widgets ipywidgets 7', async function () {
67+
const modelState = {
68+
_view_module: '@jupyter-widgets/output',
69+
outputs: [
70+
{
71+
output_type: 'display_data',
72+
data: {
73+
'application/vnd.jupyter.widget-view+json': {
74+
model_id: 'adffc4580a0944f6929c381463b0059b',
75+
version_minor: 0,
76+
version_major: 2,
77+
},
78+
'text/plain': 'A Jupyter Widget',
79+
},
80+
metadata: {},
81+
},
82+
],
83+
};
84+
85+
const elt = document.createElement('div');
86+
elt.className = 'widget-subarea';
87+
document.body.appendChild(elt);
88+
const manager = new HTMLManager();
89+
90+
// We need to seed the manager with the state of the widgets
91+
const managerState = {
92+
adffc4580a0944f6929c381463b0059b: {
93+
model_name: 'IntSliderModel',
94+
model_module: '@jupyter-widgets/controls',
95+
model_module_version: '1.5.0',
96+
state: {
97+
style: 'IPY_MODEL_3b8780f457254737a83be48bc32b0613',
98+
_view_module: '@jupyter-widgets/controls',
99+
layout: 'IPY_MODEL_33cb011834fd4c9d9af512e5e98c9904',
100+
value: 45,
101+
_model_module: '@jupyter-widgets/controls',
102+
},
103+
},
104+
'3b8780f457254737a83be48bc32b0613': {
105+
model_name: 'SliderStyleModel',
106+
model_module: '@jupyter-widgets/controls',
107+
model_module_version: '1.5.0',
108+
state: {
109+
description_width: '',
110+
_model_module: '@jupyter-widgets/controls',
111+
},
112+
},
113+
'33cb011834fd4c9d9af512e5e98c9904': {
114+
model_name: 'LayoutModel',
115+
model_module: '@jupyter-widgets/base',
116+
model_module_version: '1.2.0',
117+
state: {},
118+
},
119+
};
120+
await manager.set_state({
121+
state: managerState,
122+
version_major: 2,
123+
version_minor: 0,
124+
});
125+
const modelId = 'u-u-i-d';
126+
const modelCreate: base.IModelOptions = {
127+
model_name: 'OutputModel',
128+
model_id: modelId,
129+
model_module: '@jupyter-widgets/output',
130+
model_module_version: '*',
131+
};
132+
const model = await manager.new_model(modelCreate, modelState);
133+
await manager.display_view(manager.create_view(model), elt);
134+
135+
// Give the widget time to render
136+
await new Promise((resolve) => {
137+
setTimeout(resolve, 20);
138+
});
139+
140+
expect(elt.querySelectorAll('.slider').length).to.equal(1);
141+
});
142+
66143
it('renders widgets', async function () {
67144
const modelState = {
68145
_view_module: '@jupyter-widgets/output',
@@ -92,7 +169,7 @@ describe('Output widget', function () {
92169
adffc4580a0944f6929c381463b0059b: {
93170
model_name: 'IntSliderModel',
94171
model_module: '@jupyter-widgets/controls',
95-
model_module_version: '1.0.0',
172+
model_module_version: '2.0.0',
96173
state: {
97174
style: 'IPY_MODEL_3b8780f457254737a83be48bc32b0613',
98175
_view_module: '@jupyter-widgets/controls',
@@ -104,7 +181,7 @@ describe('Output widget', function () {
104181
'3b8780f457254737a83be48bc32b0613': {
105182
model_name: 'SliderStyleModel',
106183
model_module: '@jupyter-widgets/controls',
107-
model_module_version: '1.0.0',
184+
model_module_version: '2.0.0',
108185
state: {
109186
description_width: '',
110187
_model_module: '@jupyter-widgets/controls',
@@ -113,7 +190,7 @@ describe('Output widget', function () {
113190
'33cb011834fd4c9d9af512e5e98c9904': {
114191
model_name: 'LayoutModel',
115192
model_module: '@jupyter-widgets/base',
116-
model_module_version: '1.0.0',
193+
model_module_version: '2.0.0',
117194
state: {},
118195
},
119196
};

Diff for: packages/html-manager/webpack.config.js

+28
Original file line numberDiff line numberDiff line change
@@ -93,4 +93,32 @@ module.exports = [
9393
externals: ['@jupyter-widgets/base', 'module'],
9494
...options,
9595
},
96+
{
97+
// @jupyter-widgets/base ipywidgets 7
98+
entry: ['./amd-public-path.js', '@jupyter-widgets/base7/lib/index'],
99+
output: {
100+
library: '@jupyter-widgets/base7',
101+
filename: 'base7.js',
102+
path: path.resolve(__dirname, 'dist', 'amd'),
103+
libraryTarget: 'amd',
104+
publicPath: '', // Set in amd-public-path.js
105+
},
106+
// 'module' is the magic requirejs dependency used to set the publicPath
107+
externals: ['module'],
108+
...options,
109+
},
110+
{
111+
// @jupyter-widgets/controls
112+
entry: ['./amd-public-path.js', '@jupyter-widgets/controls7/lib/index'],
113+
output: {
114+
library: '@jupyter-widgets/controls7',
115+
filename: 'controls7.js',
116+
path: path.resolve(__dirname, 'dist', 'amd'),
117+
libraryTarget: 'amd',
118+
publicPath: '', // Set in amd-public-path.js
119+
},
120+
// 'module' is the magic requirejs dependency used to set the publicPath
121+
externals: ['@jupyter-widgets/base7', 'module'],
122+
...options,
123+
},
96124
];

Diff for: scripts/package-integrity.js

+5-1
Original file line numberDiff line numberDiff line change
@@ -108,7 +108,11 @@ function validate(dname) {
108108
problems.push('Bad core version: ' + name + ' should be ' + desired);
109109
}
110110
}
111-
if (names.indexOf(name) === -1) {
111+
if (
112+
names.indexOf(name) === -1 &&
113+
!name.startsWith('@jupyter-widgets/base') &&
114+
!name.startsWith('@jupyter-widgets/controls')
115+
) {
112116
problems.push('Unused package: ' + name);
113117
}
114118
});

0 commit comments

Comments
 (0)