Skip to content

Commit

Permalink
HTMLManager: Support both ipywidgets 7 and 8 models (#3932)
Browse files Browse the repository at this point in the history
* 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
  • Loading branch information
martinRenou authored Aug 21, 2024
1 parent c579fcd commit 73c98f0
Show file tree
Hide file tree
Showing 11 changed files with 531 additions and 81 deletions.
4 changes: 2 additions & 2 deletions docs/requirements.txt
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,8 @@ ipyleaflet
jupyter-client
jupyter-packaging
jupyterlab >=4
jupyterlite-core >=0.3.0<0.4.0
jupyterlite-pyodide-kernel >=0.3.0<0.4.0
jupyterlite-core >=0.3.0,<0.4.0
jupyterlite-pyodide-kernel >=0.3.0,<0.4.0
matplotlib
myst-nb >=0.17,<0.18
numpy
Expand Down
5 changes: 4 additions & 1 deletion packages/html-manager/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,9 @@
"@fortawesome/fontawesome-free": "^5.12.0",
"@jupyter-widgets/base": "^6.0.8",
"@jupyter-widgets/base-manager": "^1.0.9",
"@jupyter-widgets/base7": "npm:@jupyter-widgets/[email protected]",
"@jupyter-widgets/controls": "^5.0.9",
"@jupyter-widgets/controls7": "npm:@jupyter-widgets/[email protected]",
"@jupyter-widgets/output": "^6.0.8",
"@jupyter-widgets/schema": "^0.5.5",
"@jupyterlab/outputarea": "^3.0.0 || ^4.0.0",
Expand All @@ -47,7 +49,8 @@
"@lumino/messaging": "^1.10.1 || ^2.1",
"@lumino/widgets": "^1.30.0 || ^2.1",
"ajv": "^8.6.0",
"jquery": "^3.1.1"
"jquery": "^3.1.1",
"semver": "^7.3.5"
},
"devDependencies": {
"@types/jquery": "^3.5.16",
Expand Down
11 changes: 10 additions & 1 deletion packages/html-manager/scripts/concat-amd-build.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,14 @@
var fs = require('fs');

// Make a script file that defines all of the relevant AMD modules
var files = ['base.js', 'controls.js', 'index.js', 'libembed-amd.js'];
var files = [
'base.js',
'controls.js',
'base7.js',
'controls7.js',
'index.js',
'libembed-amd.js',
];
var output = files
.map((f) => {
return fs.readFileSync('./dist/amd/' + f).toString();
Expand All @@ -17,6 +24,8 @@ fs.writeFileSync('./dist/libembed-amd.js', output);
files = [
'base.js',
'controls.js',
'base7.js',
'controls7.js',
'index.js',
'libembed-amd.js',
'embed-amd-render.js',
Expand Down
54 changes: 49 additions & 5 deletions packages/html-manager/src/htmlmanager.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
// Copyright (c) Jupyter Development Team.
// Distributed under the terms of the Modified BSD License.

import * as widgets from '@jupyter-widgets/controls';
import { maxSatisfying } from 'semver';
import * as base from '@jupyter-widgets/base';
import * as outputWidgets from './output';
import { ManagerBase } from '@jupyter-widgets/base-manager';
Expand Down Expand Up @@ -38,7 +38,7 @@ export class HTMLManager extends ManagerBase {
window.addEventListener('resize', () => {
this._viewList.forEach((view) => {
MessageLoop.postMessage(
view.luminoWidget,
view.luminoWidget || view.pWidget,
LuminoWidget.Widget.ResizeMessage.UnknownSize
);
});
Expand Down Expand Up @@ -66,7 +66,7 @@ export class HTMLManager extends ManagerBase {
v.render();
}

LuminoWidget.Widget.attach(v.luminoWidget, el);
LuminoWidget.Widget.attach(v.luminoWidget || v.pWidget, el);
this._viewList.add(v);
v.once('remove', () => {
this._viewList.delete(v);
Expand Down Expand Up @@ -112,10 +112,54 @@ export class HTMLManager extends ManagerBase {
moduleVersion: string
): Promise<typeof WidgetModel | typeof WidgetView> {
return new Promise((resolve, reject) => {
if (
moduleName === '@jupyter-widgets/base' ||
moduleName === '@jupyter-widgets/controls'
) {
moduleVersion = `^${moduleVersion}`;
}

if (moduleName === '@jupyter-widgets/base') {
resolve(base);
const best = maxSatisfying(['1.2.0', '2.0.0'], moduleVersion);

if (best === '1.2.0') {
// ipywidgets 7 model
resolve(require('@jupyter-widgets/base7'));
} else {
// ipywidgets 8 model
resolve(require('@jupyter-widgets/base'));
}
} else if (moduleName === '@jupyter-widgets/controls') {
resolve(widgets);
const best = maxSatisfying(['1.5.0', '2.0.0'], moduleVersion);

if (best === '1.5.0') {
// ipywidgets 7 controls JS and CSS
require('@jupyter-widgets/controls7/css/widgets-base.css');

// If lab variables are not found, we set them (we don't want to reset the variables if they are already defined)
if (
getComputedStyle(document.documentElement).getPropertyValue(
'--jp-layout-color0'
) === ''
) {
require('@jupyter-widgets/controls7/css/labvariables.css');
}
resolve(require('@jupyter-widgets/controls7'));
} else {
// ipywidgets 8 controls JS and CSS
require('@jupyter-widgets/controls/css/widgets-base.css');

// If lab variables are not found, we set them (we don't want to reset the variables if they are already defined)
if (
getComputedStyle(document.documentElement).getPropertyValue(
'--jp-layout-color0'
) === ''
) {
require('@jupyter-widgets/controls/css/labvariables.css');
}

resolve(require('@jupyter-widgets/controls'));
}
} else if (moduleName === '@jupyter-widgets/output') {
resolve(outputWidgets);
} else if (this.loader !== undefined) {
Expand Down
20 changes: 10 additions & 10 deletions packages/html-manager/src/libembed-amd.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,16 +3,6 @@

import * as libembed from './libembed';

let cdn = 'https://cdn.jsdelivr.net/npm/';
let onlyCDN = false;

// find the data-cdn for any script tag, assuming it is only used for embed-amd.js
const scripts = document.getElementsByTagName('script');
Array.prototype.forEach.call(scripts, (script: HTMLScriptElement) => {
cdn = script.getAttribute('data-jupyter-widgets-cdn') || cdn;
onlyCDN = onlyCDN || script.hasAttribute('data-jupyter-widgets-cdn-only');
});

/**
* Load a package using requirejs and return a promise
*
Expand All @@ -29,6 +19,16 @@ const requirePromise = function (pkg: string | string[]): Promise<any> {
});
};

let cdn = 'https://cdn.jsdelivr.net/npm/';
let onlyCDN = false;

// find the data-cdn for any script tag, assuming it is only used for embed-amd.js
const scripts = document.getElementsByTagName('script');
Array.prototype.forEach.call(scripts, (script: HTMLScriptElement) => {
cdn = script.getAttribute('data-jupyter-widgets-cdn') || cdn;
onlyCDN = onlyCDN || script.hasAttribute('data-jupyter-widgets-cdn-only');
});

function moduleNameToCDNUrl(moduleName: string, moduleVersion: string): string {
let packageName = moduleName;
let fileName = 'index'; // default filename
Expand Down
10 changes: 0 additions & 10 deletions packages/html-manager/src/libembed.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,16 +11,6 @@ import '@fortawesome/fontawesome-free/css/all.min.css';
import '@fortawesome/fontawesome-free/css/v4-shims.min.css';

import '@lumino/widgets/style/index.css';
import '@jupyter-widgets/controls/css/widgets-base.css';

// If lab variables are not found, we set them (we don't want to reset the variables if they are already defined)
if (
getComputedStyle(document.documentElement).getPropertyValue(
'--jp-layout-color0'
) === ''
) {
require('@jupyter-widgets/controls/css/labvariables.css');
}

// Used just for the typing. We must not import the javascript because we don't
// want to include it in the require embedding.
Expand Down
2 changes: 1 addition & 1 deletion packages/html-manager/src/output_renderers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ export class WidgetRenderer extends Widget implements IRenderMime.IRenderer {
try {
const wModel = await this._manager.get_model(source.model_id);
const wView = await this._manager.create_view(wModel);
Widget.attach(wView.luminoWidget, this.node);
Widget.attach(wView.luminoWidget || wView.pWidget, this.node);
} catch (err) {
console.log('Error displaying widget');
console.log(err);
Expand Down
83 changes: 80 additions & 3 deletions packages/html-manager/test/src/output_test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,83 @@ describe('Output widget', function () {
expect(elt.querySelectorAll('table').length).to.equal(1);
});

it('renders widgets ipywidgets 7', async function () {
const modelState = {
_view_module: '@jupyter-widgets/output',
outputs: [
{
output_type: 'display_data',
data: {
'application/vnd.jupyter.widget-view+json': {
model_id: 'adffc4580a0944f6929c381463b0059b',
version_minor: 0,
version_major: 2,
},
'text/plain': 'A Jupyter Widget',
},
metadata: {},
},
],
};

const elt = document.createElement('div');
elt.className = 'widget-subarea';
document.body.appendChild(elt);
const manager = new HTMLManager();

// We need to seed the manager with the state of the widgets
const managerState = {
adffc4580a0944f6929c381463b0059b: {
model_name: 'IntSliderModel',
model_module: '@jupyter-widgets/controls',
model_module_version: '1.5.0',
state: {
style: 'IPY_MODEL_3b8780f457254737a83be48bc32b0613',
_view_module: '@jupyter-widgets/controls',
layout: 'IPY_MODEL_33cb011834fd4c9d9af512e5e98c9904',
value: 45,
_model_module: '@jupyter-widgets/controls',
},
},
'3b8780f457254737a83be48bc32b0613': {
model_name: 'SliderStyleModel',
model_module: '@jupyter-widgets/controls',
model_module_version: '1.5.0',
state: {
description_width: '',
_model_module: '@jupyter-widgets/controls',
},
},
'33cb011834fd4c9d9af512e5e98c9904': {
model_name: 'LayoutModel',
model_module: '@jupyter-widgets/base',
model_module_version: '1.2.0',
state: {},
},
};
await manager.set_state({
state: managerState,
version_major: 2,
version_minor: 0,
});
const modelId = 'u-u-i-d';
const modelCreate: base.IModelOptions = {
model_name: 'OutputModel',
model_id: modelId,
model_module: '@jupyter-widgets/output',
model_module_version: '*',
};
const model = await manager.new_model(modelCreate, modelState);
await manager.display_view(manager.create_view(model), elt);

// Give the widget time to render
await new Promise((resolve) => {
setTimeout(resolve, 20);
});

expect(elt.querySelectorAll('.slider').length).to.equal(1);
});

it('renders widgets', async function () {
const modelState = {
_view_module: '@jupyter-widgets/output',
Expand Down Expand Up @@ -92,7 +169,7 @@ describe('Output widget', function () {
adffc4580a0944f6929c381463b0059b: {
model_name: 'IntSliderModel',
model_module: '@jupyter-widgets/controls',
model_module_version: '1.0.0',
model_module_version: '2.0.0',
state: {
style: 'IPY_MODEL_3b8780f457254737a83be48bc32b0613',
_view_module: '@jupyter-widgets/controls',
Expand All @@ -104,7 +181,7 @@ describe('Output widget', function () {
'3b8780f457254737a83be48bc32b0613': {
model_name: 'SliderStyleModel',
model_module: '@jupyter-widgets/controls',
model_module_version: '1.0.0',
model_module_version: '2.0.0',
state: {
description_width: '',
_model_module: '@jupyter-widgets/controls',
Expand All @@ -113,7 +190,7 @@ describe('Output widget', function () {
'33cb011834fd4c9d9af512e5e98c9904': {
model_name: 'LayoutModel',
model_module: '@jupyter-widgets/base',
model_module_version: '1.0.0',
model_module_version: '2.0.0',
state: {},
},
};
Expand Down
28 changes: 28 additions & 0 deletions packages/html-manager/webpack.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -93,4 +93,32 @@ module.exports = [
externals: ['@jupyter-widgets/base', 'module'],
...options,
},
{
// @jupyter-widgets/base ipywidgets 7
entry: ['./amd-public-path.js', '@jupyter-widgets/base7/lib/index'],
output: {
library: '@jupyter-widgets/base7',
filename: 'base7.js',
path: path.resolve(__dirname, 'dist', 'amd'),
libraryTarget: 'amd',
publicPath: '', // Set in amd-public-path.js
},
// 'module' is the magic requirejs dependency used to set the publicPath
externals: ['module'],
...options,
},
{
// @jupyter-widgets/controls
entry: ['./amd-public-path.js', '@jupyter-widgets/controls7/lib/index'],
output: {
library: '@jupyter-widgets/controls7',
filename: 'controls7.js',
path: path.resolve(__dirname, 'dist', 'amd'),
libraryTarget: 'amd',
publicPath: '', // Set in amd-public-path.js
},
// 'module' is the magic requirejs dependency used to set the publicPath
externals: ['@jupyter-widgets/base7', 'module'],
...options,
},
];
6 changes: 5 additions & 1 deletion scripts/package-integrity.js
Original file line number Diff line number Diff line change
Expand Up @@ -108,7 +108,11 @@ function validate(dname) {
problems.push('Bad core version: ' + name + ' should be ' + desired);
}
}
if (names.indexOf(name) === -1) {
if (
names.indexOf(name) === -1 &&
!name.startsWith('@jupyter-widgets/base') &&
!name.startsWith('@jupyter-widgets/controls')
) {
problems.push('Unused package: ' + name);
}
});
Expand Down
Loading

0 comments on commit 73c98f0

Please sign in to comment.