Foundational classes and utilities for building interactive widgets in Jupyter environments
—
Pending
Does it follow best practices?
Impact
Pending
No eval scenarios have been run
Pending
The risk profile of this skill
The widget registry system provides dynamic loading and registration of widget modules, enabling extensibility and plugin-like architecture for Jupyter widget ecosystems.
Core interface for widget registries that support external widget module registration.
/**
* Interface for Jupyter widget registries
*/
interface IJupyterWidgetRegistry {
/**
* Register a widget module with the registry
* @param data - Widget module registration data
*/
registerWidget(data: IWidgetRegistryData): void;
}
/**
* Lumino token for dependency injection of widget registries
*/
const IJupyterWidgetRegistry: Token<IJupyterWidgetRegistry>;Usage Examples:
// Get registry from dependency injection container
const registry = app.serviceManager.get(IJupyterWidgetRegistry);
// Register a widget module
registry.registerWidget({
name: '@my-org/custom-widgets',
version: '1.2.0',
exports: {
MySliderModel: MySliderModel,
MySliderView: MySliderView,
MyButtonModel: MyButtonModel,
MyButtonView: MyButtonView
}
});Configuration structure for registering widget modules with their exports.
/**
* Data structure for widget module registration
*/
interface IWidgetRegistryData {
/**
* Name of the widget module (typically npm package name)
*/
name: string;
/**
* Version of the widget module
*/
version: string;
/**
* Widget class exports from the module
*/
exports: ExportData;
}
/**
* Map of export names to widget classes
*/
type ExportMap = {
[key: string]: typeof WidgetModel | typeof WidgetView;
};
/**
* Flexible export data supporting synchronous and asynchronous loading
*/
type ExportData =
| ExportMap
| Promise<ExportMap>
| (() => ExportMap)
| (() => Promise<ExportMap>);Usage Examples:
// Synchronous registration with direct exports
const syncRegistration: IWidgetRegistryData = {
name: 'my-widgets',
version: '1.0.0',
exports: {
CounterModel: CounterModel,
CounterView: CounterView,
SliderModel: SliderModel,
SliderView: SliderView
}
};
// Asynchronous registration with promise
const asyncRegistration: IWidgetRegistryData = {
name: 'heavy-widgets',
version: '2.0.0',
exports: import('./heavy-widgets').then(module => ({
ChartModel: module.ChartModel,
ChartView: module.ChartView,
GraphModel: module.GraphModel,
GraphView: module.GraphView
}))
};
// Lazy loading with function
const lazyRegistration: IWidgetRegistryData = {
name: 'optional-widgets',
version: '1.5.0',
exports: () => {
// Load only when needed
const module = require('./optional-widgets');
return {
OptionalModel: module.OptionalModel,
OptionalView: module.OptionalView
};
}
};
// Async lazy loading
const asyncLazyRegistration: IWidgetRegistryData = {
name: 'remote-widgets',
version: '3.0.0',
exports: async () => {
// Dynamic import for code splitting
const module = await import('./remote-widgets');
return {
RemoteModel: module.RemoteModel,
RemoteView: module.RemoteView
};
}
};Example implementation of a widget registry:
class WidgetRegistry implements IJupyterWidgetRegistry {
private _modules = new Map<string, IWidgetRegistryData>();
private _resolvedExports = new Map<string, ExportMap>();
registerWidget(data: IWidgetRegistryData): void {
const key = `${data.name}@${data.version}`;
this._modules.set(key, data);
console.log(`Registered widget module: ${key}`);
}
async getExports(name: string, version: string): Promise<ExportMap> {
const key = `${name}@${version}`;
// Check cache first
if (this._resolvedExports.has(key)) {
return this._resolvedExports.get(key)!;
}
const data = this._modules.get(key);
if (!data) {
throw new Error(`Widget module not found: ${key}`);
}
// Resolve exports based on type
let exports: ExportMap;
if (typeof data.exports === 'function') {
exports = await data.exports();
} else if (data.exports instanceof Promise) {
exports = await data.exports;
} else {
exports = data.exports;
}
// Cache resolved exports
this._resolvedExports.set(key, exports);
return exports;
}
async getWidgetClass(
name: string,
version: string,
className: string
): Promise<typeof WidgetModel | typeof WidgetView> {
const exports = await this.getExports(name, version);
const WidgetClass = exports[className];
if (!WidgetClass) {
throw new Error(`Widget class not found: ${className} in ${name}@${version}`);
}
return WidgetClass;
}
listRegisteredModules(): string[] {
return Array.from(this._modules.keys());
}
}Integration between registry and widget manager for dynamic loading:
class ExtensibleWidgetManager implements IWidgetManager {
constructor(private registry: IJupyterWidgetRegistry) {}
async new_model(options: IModelOptions, state?: JSONObject): Promise<WidgetModel> {
try {
// Try to get model class from registry
const ModelClass = await this.registry.getWidgetClass(
options.model_module,
options.model_module_version,
options.model_name
) as typeof WidgetModel;
// Create model instance
const model = new ModelClass(state || {}, {
model_id: options.model_id || generateId(),
widget_manager: this,
comm: options.comm
});
this.register_model(model.model_id, Promise.resolve(model));
return model;
} catch (error) {
console.error('Failed to create model:', error);
throw error;
}
}
async create_view<VT extends WidgetView>(
model: WidgetModel,
options?: unknown
): Promise<VT> {
const viewName = model.get('_view_name');
const viewModule = model.get('_view_module');
const viewModuleVersion = model.get('_view_module_version');
if (!viewName || !viewModule) {
throw new Error('Model missing view information');
}
try {
// Get view class from registry
const ViewClass = await this.registry.getWidgetClass(
viewModule,
viewModuleVersion,
viewName
) as typeof WidgetView;
// Create view instance
const view = new ViewClass({
model: model,
options: options
});
return view as VT;
} catch (error) {
console.error('Failed to create view:', error);
throw error;
}
}
// ... other IWidgetManager methods
}Pattern for plugin-style widget registration:
// Plugin interface
interface IWidgetPlugin {
id: string;
activate(app: Application, registry: IJupyterWidgetRegistry): void;
deactivate?(): void;
}
// Example plugin
class ChartWidgetsPlugin implements IWidgetPlugin {
id = '@my-org/chart-widgets';
activate(app: Application, registry: IJupyterWidgetRegistry): void {
// Register widget classes
registry.registerWidget({
name: this.id,
version: '1.0.0',
exports: {
BarChartModel: BarChartModel,
BarChartView: BarChartView,
LineChartModel: LineChartModel,
LineChartView: LineChartView,
PieChartModel: PieChartModel,
PieChartView: PieChartView
}
});
console.log('Chart widgets plugin activated');
}
deactivate(): void {
console.log('Chart widgets plugin deactivated');
}
}
// Plugin management
class PluginManager {
private plugins = new Map<string, IWidgetPlugin>();
registerPlugin(plugin: IWidgetPlugin): void {
this.plugins.set(plugin.id, plugin);
}
activatePlugin(
pluginId: string,
app: Application,
registry: IJupyterWidgetRegistry
): void {
const plugin = this.plugins.get(pluginId);
if (plugin) {
plugin.activate(app, registry);
}
}
deactivatePlugin(pluginId: string): void {
const plugin = this.plugins.get(pluginId);
if (plugin && plugin.deactivate) {
plugin.deactivate();
}
}
}
// Usage
const pluginManager = new PluginManager();
const chartPlugin = new ChartWidgetsPlugin();
pluginManager.registerPlugin(chartPlugin);
pluginManager.activatePlugin(chartPlugin.id, app, registry);class VersionedRegistry implements IJupyterWidgetRegistry {
private modules = new Map<string, Map<string, IWidgetRegistryData>>();
registerWidget(data: IWidgetRegistryData): void {
if (!this.modules.has(data.name)) {
this.modules.set(data.name, new Map());
}
this.modules.get(data.name)!.set(data.version, data);
}
findCompatibleVersion(name: string, versionRange: string): string | null {
const moduleVersions = this.modules.get(name);
if (!moduleVersions) return null;
// Simple version matching (in real implementation, use semver)
const availableVersions = Array.from(moduleVersions.keys());
// Find highest compatible version
return availableVersions
.filter(version => this.isCompatible(version, versionRange))
.sort(this.compareVersions)
.pop() || null;
}
private isCompatible(version: string, range: string): boolean {
// Simplified version compatibility check
// Real implementation would use semver library
return version.startsWith(range.replace('~', '').replace('^', ''));
}
private compareVersions(a: string, b: string): number {
// Simplified version comparison
return a.localeCompare(b, undefined, { numeric: true });
}
}// Registry with conditional widget loading
class ConditionalRegistry implements IJupyterWidgetRegistry {
registerWidget(data: IWidgetRegistryData): void {
// Enhance registration data with conditions
const enhancedData = {
...data,
exports: this.wrapConditionalExports(data.exports)
};
this.doRegister(enhancedData);
}
private wrapConditionalExports(exports: ExportData): ExportData {
return async (): Promise<ExportMap> => {
// Check environment conditions
if (!this.checkEnvironment()) {
throw new Error('Widget not supported in current environment');
}
// Check feature detection
if (!this.checkFeatures()) {
throw new Error('Required features not available');
}
// Resolve actual exports
let actualExports: ExportMap;
if (typeof exports === 'function') {
actualExports = await exports();
} else if (exports instanceof Promise) {
actualExports = await exports;
} else {
actualExports = exports;
}
return actualExports;
};
}
private checkEnvironment(): boolean {
// Check browser capabilities, Jupyter environment, etc.
return typeof window !== 'undefined' &&
'requestAnimationFrame' in window;
}
private checkFeatures(): boolean {
// Check for required features
return 'WebGL' in window ||
'WebGL2RenderingContext' in window;
}
}// Registry with event system
interface IRegistryEvents {
'widget-registered': (data: IWidgetRegistryData) => void;
'widget-loaded': (name: string, version: string, exports: ExportMap) => void;
'widget-error': (name: string, version: string, error: Error) => void;
}
class EventfulRegistry extends EventTarget implements IJupyterWidgetRegistry {
registerWidget(data: IWidgetRegistryData): void {
// Perform registration
this.doRegister(data);
// Emit event
this.dispatchEvent(new CustomEvent('widget-registered', {
detail: data
}));
}
async loadWidget(name: string, version: string): Promise<ExportMap> {
try {
const exports = await this.resolveExports(name, version);
this.dispatchEvent(new CustomEvent('widget-loaded', {
detail: { name, version, exports }
}));
return exports;
} catch (error) {
this.dispatchEvent(new CustomEvent('widget-error', {
detail: { name, version, error }
}));
throw error;
}
}
// Type-safe event listening
on<K extends keyof IRegistryEvents>(
type: K,
listener: IRegistryEvents[K]
): void {
this.addEventListener(type, listener as EventListener);
}
}
// Usage with events
const registry = new EventfulRegistry();
registry.on('widget-registered', (data) => {
console.log(`New widget module registered: ${data.name}@${data.version}`);
});
registry.on('widget-loaded', ({ name, version, exports }) => {
console.log(`Widget loaded: ${name}@${version}, exports:`, Object.keys(exports));
});
registry.on('widget-error', ({ name, version, error }) => {
console.error(`Failed to load widget ${name}@${version}:`, error);
});// JupyterLab extension using registry
const widgetExtension: JupyterFrontEndPlugin<void> = {
id: '@my-org/widget-extension',
autoStart: true,
requires: [IJupyterWidgetRegistry],
activate: (app: JupyterFrontEnd, registry: IJupyterWidgetRegistry) => {
// Register widgets when extension loads
registry.registerWidget({
name: '@my-org/widgets',
version: '1.0.0',
exports: async () => {
const module = await import('./widgets');
return {
MyModel: module.MyModel,
MyView: module.MyView
};
}
});
}
};
export default widgetExtension;// Discover and register widgets at runtime
class RuntimeDiscovery {
constructor(private registry: IJupyterWidgetRegistry) {}
async discoverWidgets(searchPaths: string[]): Promise<void> {
for (const path of searchPaths) {
try {
const packageJson = await this.loadPackageJson(path);
if (packageJson.keywords?.includes('jupyter-widget')) {
await this.registerFromPackage(path, packageJson);
}
} catch (error) {
console.warn(`Failed to discover widgets in ${path}:`, error);
}
}
}
private async registerFromPackage(path: string, packageJson: any): Promise<void> {
this.registry.registerWidget({
name: packageJson.name,
version: packageJson.version,
exports: () => import(path)
});
}
}