-
Notifications
You must be signed in to change notification settings - Fork 948
/
Copy pathmanager.ts
601 lines (524 loc) · 15.3 KB
/
manager.ts
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
// Copyright (c) Jupyter Development Team.
// Distributed under the terms of the Modified BSD License.
import {
shims,
IClassicComm,
IWidgetRegistryData,
ExportMap,
ExportData,
WidgetModel,
WidgetView,
ICallbacks,
} from '@jupyter-widgets/base';
import {
ManagerBase,
serialize_state,
IStateOptions,
} from '@jupyter-widgets/base-manager';
import { IDisposable } from '@lumino/disposable';
import { ReadonlyPartialJSONValue } from '@lumino/coreutils';
import { INotebookModel } from '@jupyterlab/notebook';
import { IRenderMimeRegistry } from '@jupyterlab/rendermime';
import { Kernel, KernelMessage, Session } from '@jupyterlab/services';
import { DocumentRegistry } from '@jupyterlab/docregistry';
import { ISignal, Signal } from '@lumino/signaling';
import { valid } from 'semver';
import { SemVerCache } from './semvercache';
/**
* The mime type for a widget view.
*/
export const WIDGET_VIEW_MIMETYPE = 'application/vnd.jupyter.widget-view+json';
/**
* The mime type for widget state data.
*/
export const WIDGET_STATE_MIMETYPE =
'application/vnd.jupyter.widget-state+json';
/**
* A widget manager that returns Lumino widgets.
*/
export abstract class LabWidgetManager
extends ManagerBase
implements IDisposable
{
constructor(rendermime: IRenderMimeRegistry) {
super();
this._rendermime = rendermime;
}
/**
* Default callback handler to emit unhandled kernel messages.
*/
callbacks(view?: WidgetView): ICallbacks {
return {
iopub: {
output: (msg: KernelMessage.IIOPubMessage): void => {
this._onUnhandledIOPubMessage.emit(msg);
},
},
};
}
/**
* Register a new kernel
*/
protected _handleKernelChanged({
oldValue,
newValue,
}: Session.ISessionConnection.IKernelChangedArgs): void {
if (oldValue) {
oldValue.removeCommTarget(this.comm_target_name, this._handleCommOpen);
}
if (newValue) {
newValue.registerCommTarget(this.comm_target_name, this._handleCommOpen);
}
}
/**
* Disconnect the widget manager from the kernel, setting each model's comm
* as dead.
*/
disconnect(): void {
super.disconnect();
this._restoredStatus = false;
}
protected async _loadFromKernel(): Promise<void> {
if (!this.kernel) {
throw new Error('Kernel not set');
}
if (this.kernel?.handleComms === false) {
// A "load" for a kernel that does not handle comms does nothing.
return;
}
return super._loadFromKernel();
}
/**
* Create a comm.
*/
async _create_comm(
target_name: string,
model_id: string,
data?: any,
metadata?: any,
buffers?: ArrayBuffer[] | ArrayBufferView[]
): Promise<IClassicComm> {
const kernel = this.kernel;
if (!kernel) {
throw new Error('No current kernel');
}
const comm = kernel.createComm(target_name, model_id);
if (data || metadata) {
comm.open(data, metadata, buffers);
}
return new shims.services.Comm(comm);
}
/**
* Get the currently-registered comms.
*/
async _get_comm_info(): Promise<any> {
const kernel = this.kernel;
if (!kernel) {
throw new Error('No current kernel');
}
const reply = await kernel.requestCommInfo({
target_name: this.comm_target_name,
});
if (reply.content.status === 'ok') {
return (reply.content as any).comms;
} else {
return {};
}
}
/**
* Get whether the manager is disposed.
*
* #### Notes
* This is a read-only property.
*/
get isDisposed(): boolean {
return this._isDisposed;
}
/**
* Dispose the resources held by the manager.
*/
dispose(): void {
if (this.isDisposed) {
return;
}
this._isDisposed = true;
if (this._commRegistration) {
this._commRegistration.dispose();
}
}
/**
* Resolve a URL relative to the current notebook location.
*/
async resolveUrl(url: string): Promise<string> {
return url;
}
/**
* Load a class and return a promise to the loaded object.
*/
protected async loadClass(
className: string,
moduleName: string,
moduleVersion: string
): Promise<typeof WidgetModel | typeof WidgetView> {
// Special-case the Jupyter base and controls packages. If we have just a
// plain version, with no indication of the compatible range, prepend a ^ to
// get all compatible versions. We may eventually apply this logic to all
// widget modules. See issues #2006 and #2017 for more discussion.
if (
(moduleName === '@jupyter-widgets/base' ||
moduleName === '@jupyter-widgets/controls') &&
valid(moduleVersion)
) {
moduleVersion = `^${moduleVersion}`;
}
const allVersions = this._registry.getAllVersions(moduleName);
if (!allVersions) {
throw new Error(`No version of module ${moduleName} is registered`);
}
const mod = this._registry.get(moduleName, moduleVersion);
if (!mod) {
const registeredVersionList = Object.keys(allVersions);
throw new Error(
`Module ${moduleName}, version ${moduleVersion} is not registered, however, \
${registeredVersionList.join(',')} ${
registeredVersionList.length > 1 ? 'are' : 'is'
}`
);
}
let module: ExportMap;
if (typeof mod === 'function') {
module = await mod();
} else {
module = await mod;
}
const cls: any = module[className];
if (!cls) {
throw new Error(`Class ${className} not found in module ${moduleName}`);
}
return cls;
}
abstract get kernel(): Kernel.IKernelConnection | null;
get rendermime(): IRenderMimeRegistry {
return this._rendermime;
}
/**
* A signal emitted when state is restored to the widget manager.
*
* #### Notes
* This indicates that previously-unavailable widget models might be available now.
*/
get restored(): ISignal<this, void> {
return this._restored;
}
/**
* Whether the state has been restored yet or not.
*/
get restoredStatus(): boolean {
return this._restoredStatus;
}
/**
* A signal emitted for unhandled iopub kernel messages.
*
*/
get onUnhandledIOPubMessage(): ISignal<this, KernelMessage.IIOPubMessage> {
return this._onUnhandledIOPubMessage;
}
register(data: IWidgetRegistryData): void {
this._registry.set(data.name, data.version, data.exports);
}
/**
* Register a widget model.
*/
register_model(model_id: string, modelPromise: Promise<WidgetModel>): void {
super.register_model(model_id, modelPromise);
// Update the synchronous model map
modelPromise.then((model) => {
this._modelsSync.set(model_id, model);
model.once('comm:close', () => {
this._modelsSync.delete(model_id);
});
});
}
/**
* Close all widgets and empty the widget state.
* @return Promise that resolves when the widget state is cleared.
*/
async clear_state(): Promise<void> {
await super.clear_state();
this._modelsSync = new Map();
}
/**
* Synchronously get the state of the live widgets in the widget manager.
*
* This includes all of the live widget models, and follows the format given in
* the @jupyter-widgets/schema package.
*
* @param options - The options for what state to return.
* @returns A state dictionary
*/
get_state_sync(options: IStateOptions = {}): ReadonlyPartialJSONValue {
const models = [];
for (const model of this._modelsSync.values()) {
if (model.comm_live) {
models.push(model);
}
}
return serialize_state(models, options);
}
// _handleCommOpen is an attribute, not a method, so `this` is captured in a
// single object that can be registered and removed
protected _handleCommOpen = async (
comm: Kernel.IComm,
msg: KernelMessage.ICommOpenMsg
): Promise<void> => {
const oldComm = new shims.services.Comm(comm);
await this.handle_comm_open(oldComm, msg);
};
protected _restored = new Signal<this, void>(this);
protected _restoredStatus = false;
protected _kernelRestoreInProgress = false;
private _isDisposed = false;
private _registry: SemVerCache<ExportData> = new SemVerCache<ExportData>();
private _rendermime: IRenderMimeRegistry;
private _commRegistration: IDisposable;
private _modelsSync = new Map<string, WidgetModel>();
private _onUnhandledIOPubMessage = new Signal<
this,
KernelMessage.IIOPubMessage
>(this);
}
/**
* A widget manager that returns Lumino widgets.
*/
export class KernelWidgetManager extends LabWidgetManager {
constructor(
kernel: Kernel.IKernelConnection,
rendermime: IRenderMimeRegistry
) {
super(rendermime);
this._kernel = kernel;
kernel.statusChanged.connect((sender, args) => {
this._handleKernelStatusChange(args);
});
kernel.connectionStatusChanged.connect((sender, args) => {
this._handleKernelConnectionStatusChange(args);
});
this._handleKernelChanged({
name: 'kernel',
oldValue: null,
newValue: kernel,
});
this.restoreWidgets();
}
_handleKernelConnectionStatusChange(status: Kernel.ConnectionStatus): void {
if (status === 'connected') {
// Only restore if we aren't currently trying to restore from the kernel
// (for example, in our initial restore from the constructor).
if (!this._kernelRestoreInProgress) {
this.restoreWidgets();
}
}
}
_handleKernelStatusChange(status: Kernel.Status): void {
if (status === 'restarting') {
this.disconnect();
}
}
/**
* Restore widgets from kernel and saved state.
*/
async restoreWidgets(): Promise<void> {
try {
this._kernelRestoreInProgress = true;
await this._loadFromKernel();
this._restoredStatus = true;
this._restored.emit();
} catch (err) {
// Do nothing
}
this._kernelRestoreInProgress = false;
}
/**
* Dispose the resources held by the manager.
*/
dispose(): void {
if (this.isDisposed) {
return;
}
this._kernel = null!;
super.dispose();
}
get kernel(): Kernel.IKernelConnection {
return this._kernel;
}
private _kernel: Kernel.IKernelConnection;
}
/**
* A widget manager that returns phosphor widgets.
*/
export class WidgetManager extends LabWidgetManager {
constructor(
context: DocumentRegistry.IContext<INotebookModel>,
rendermime: IRenderMimeRegistry,
settings: WidgetManager.Settings
) {
super(rendermime);
this._context = context;
context.sessionContext.kernelChanged.connect((sender, args) => {
this._handleKernelChanged(args);
});
context.sessionContext.statusChanged.connect((sender, args) => {
this._handleKernelStatusChange(args);
});
context.sessionContext.connectionStatusChanged.connect((sender, args) => {
this._handleKernelConnectionStatusChange(args);
});
if (context.sessionContext.session?.kernel) {
this._handleKernelChanged({
name: 'kernel',
oldValue: null,
newValue: context.sessionContext.session?.kernel,
});
}
this.restoreWidgets(this._context!.model);
this._settings = settings;
context.saveState.connect((sender, saveState) => {
if (saveState === 'started' && settings.saveState) {
this._saveState();
}
});
}
/**
* Save the widget state to the context model.
*/
private _saveState(): void {
const state = this.get_state_sync({ drop_defaults: true });
if (this._context.model.setMetadata) {
this._context.model.setMetadata('widgets', {
'application/vnd.jupyter.widget-state+json': state,
});
} else {
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore JupyterLab 3 support
this._context.model.metadata.set('widgets', {
'application/vnd.jupyter.widget-state+json': state,
});
}
}
_handleKernelConnectionStatusChange(status: Kernel.ConnectionStatus): void {
if (status === 'connected') {
// Only restore if we aren't currently trying to restore from the kernel
// (for example, in our initial restore from the constructor).
if (!this._kernelRestoreInProgress) {
// We only want to restore widgets from the kernel, not ones saved in the notebook.
this.restoreWidgets(this._context!.model, {
loadKernel: true,
loadNotebook: false,
});
}
}
}
_handleKernelStatusChange(status: Kernel.Status): void {
if (status === 'restarting') {
this.disconnect();
}
}
/**
* Restore widgets from kernel and saved state.
*/
async restoreWidgets(
notebook: INotebookModel,
{ loadKernel, loadNotebook } = { loadKernel: true, loadNotebook: true }
): Promise<void> {
try {
await this.context.sessionContext.ready;
if (loadKernel) {
try {
this._kernelRestoreInProgress = true;
await this._loadFromKernel();
} finally {
this._kernelRestoreInProgress = false;
}
}
if (loadNotebook) {
await this._loadFromNotebook(notebook);
}
// If the restore worked above, then update our state.
this._restoredStatus = true;
this._restored.emit();
} catch (err) {
// Do nothing if the restore did not work.
}
}
/**
* Load widget state from notebook metadata
*/
async _loadFromNotebook(notebook: INotebookModel): Promise<void> {
const widget_md = notebook.getMetadata
? (notebook.getMetadata('widgets') as any)
: // eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore JupyterLab 3 support
notebook.metadata.get('widgets');
// Restore any widgets from saved state that are not live
if (widget_md && widget_md[WIDGET_STATE_MIMETYPE]) {
let state = widget_md[WIDGET_STATE_MIMETYPE];
state = this.filterExistingModelState(state);
await this.set_state(state);
}
}
/**
* Dispose the resources held by the manager.
*/
dispose(): void {
if (this.isDisposed) {
return;
}
this._context = null!;
super.dispose();
}
/**
* Resolve a URL relative to the current notebook location.
*/
async resolveUrl(url: string): Promise<string> {
const partial = await this.context.urlResolver.resolveUrl(url);
return this.context.urlResolver.getDownloadUrl(partial);
}
get context(): DocumentRegistry.IContext<INotebookModel> {
return this._context;
}
get kernel(): Kernel.IKernelConnection | null {
return this._context.sessionContext?.session?.kernel ?? null;
}
/**
* Register a widget model.
*/
register_model(model_id: string, modelPromise: Promise<WidgetModel>): void {
super.register_model(model_id, modelPromise);
this.setDirty();
}
/**
* Close all widgets and empty the widget state.
* @return Promise that resolves when the widget state is cleared.
*/
async clear_state(): Promise<void> {
await super.clear_state();
this.setDirty();
}
/**
* Set the dirty state of the notebook model if applicable.
*
* TODO: perhaps should also set dirty when any model changes any data
*/
setDirty(): void {
if (this._settings.saveState) {
this._context!.model.dirty = true;
}
}
private _context: DocumentRegistry.IContext<INotebookModel>;
private _settings: WidgetManager.Settings;
}
export namespace WidgetManager {
export type Settings = {
saveState: boolean;
};
}