diff --git a/ipylab/connection.py b/ipylab/connection.py index 01e1779..dd51917 100644 --- a/ipylab/connection.py +++ b/ipylab/connection.py @@ -6,10 +6,10 @@ import uuid from typing import TYPE_CHECKING, Any, ClassVar -from ipywidgets import register +from ipywidgets import Widget, register from traitlets import Bool, Dict, Unicode -from ipylab.asyncwidget import AsyncWidgetBase +from ipylab.asyncwidget import AsyncWidgetBase, pack if TYPE_CHECKING: from collections.abc import Generator @@ -49,7 +49,7 @@ class Connection(AsyncWidgetBase): """ CID_PREFIX = "" # Required in subclassess to discriminate when creating. - _CLASS_DEFINITIONS: ClassVar[dict[str, type[Connection]]] = {} + _CLASS_DEFINITIONS: ClassVar[dict[str, type[Self]]] = {} _connections: dict[str, Connection] = {} # noqa RUF012 _model_name = Unicode("ConnectionModel").tag(sync=True) cid = Unicode(read_only=True, help="connection id").tag(sync=True) @@ -63,7 +63,7 @@ def __init_subclass__(cls, **kwargs) -> None: cls._CLASS_DEFINITIONS[cls.CID_PREFIX] = cls # type: ignore super().__init_subclass__(**kwargs) - def __new__(cls, *, cid: str, id: str | None = None, **kwgs) -> Self: # noqa: A002, ARG003 + def __new__(cls, *, cid: str, id: str = "", info: dict | None = None, **kwgs) -> Self: # noqa: A002 if cid not in cls._connections: if cls.CID_PREFIX and not cid.startswith(cls.CID_PREFIX): msg = f"Expected prefix '{cls.CID_PREFIX}' not found for {cid=}" @@ -71,18 +71,17 @@ def __new__(cls, *, cid: str, id: str | None = None, **kwgs) -> Self: # noqa: A # Check if a subclass is registered with 'CID_PREFIX' cls_ = cls._CLASS_DEFINITIONS.get(cid.split(":")[0], cls) if ":" in cid else cls kwgs.pop("info", None) - cls._connections[cid] = super().__new__(cls_, **kwgs) # type: ignore + cls._connections[cid] = inst = super().__new__(cls_, **kwgs) + inst.set_trait("cid", cid) + inst.set_trait("id", id) + inst.set_trait("info", info or {}) + return cls._connections[cid] # type: ignore - def __init__(self, *, cid: str, model_id=None, id: str | None = None, **kwgs): # noqa: A002 + def __init__(self, cid: str, id: str = "", info: dict | None = None, **kwgs): # noqa: A002, ARG002 if self._async_widget_base_init_complete: return - self.set_trait("cid", cid) - self.set_trait("id", id or "") - info = kwgs.pop("info", None) - if info: - self.set_trait("info", info) - super().__init__(model_id=model_id, **kwgs) + super().__init__(**kwgs) def __str__(self): return self.cid @@ -153,3 +152,12 @@ async def activate_(): return self return self.to_task(activate_()) + + +class ModelConnection(Connection): + """A connection to the model of a widget.""" + + CID_PREFIX = "ipylab widget connection" + + def __new__(cls, widget: Widget, **kwgs) -> Self: + return super().__new__(cls, cid=cls.new_cid(), id=pack(widget), **kwgs) # type: ignore diff --git a/ipylab/menu.py b/ipylab/menu.py index 52a6f70..004a483 100644 --- a/ipylab/menu.py +++ b/ipylab/menu.py @@ -52,10 +52,10 @@ def __new__(cls, *, model_id=None, **kwgs): kwgs.pop("basename", None) return super().__new__(cls, model_id=model_id, **kwgs) - def __init__(self, *, model_id=None, **kwgs): + def __init__(self, *, model_id=None, basename="", **kwgs): if self._async_widget_base_init_complete: return - if basename := kwgs.pop("basename", None): + if basename: self.set_trait("_basename", basename) super().__init__(model_id=model_id, **kwgs) @@ -106,6 +106,9 @@ def add_item( ) return self.to_task(self._add_to_tuple_trait("items", task)) + def activate(self): + return self.execute_method("open") + class MenuConnection(RankedMenu, Connection): """A connection to a custom menu""" diff --git a/src/widgets/commands.ts b/src/widgets/commands.ts index ae144c8..d617059 100644 --- a/src/widgets/commands.ts +++ b/src/widgets/commands.ts @@ -13,8 +13,8 @@ export class CommandRegistryModel extends IpylabModel { * @param attributes The base attributes. * @param options The initialization options. */ - initialize(attributes: any, options: any): void { - super.initialize(attributes, options); + async initialize(attributes: any, options: any): Promise { + await super.initialize(attributes, options); this.commands.commandChanged.connect(this._sendCommandList, this); this._sendCommandList(); } diff --git a/src/widgets/connection.ts b/src/widgets/connection.ts index 1309bb7..06ef456 100644 --- a/src/widgets/connection.ts +++ b/src/widgets/connection.ts @@ -16,22 +16,9 @@ import { IpylabModel } from './ipylab'; */ export class ConnectionModel extends IpylabModel { async initialize(attributes: ObjectHash, options: any): Promise { - let base; - const cid = this.get('cid'); - const id = this.get('id'); - try { - base = this.getConnection(cid, id); - } catch {} - super.initialize(attributes, { ...options, base }); - if (base) { - this.base.disposed.connect(() => this.close()); - this.on('change:_dispose', this.dispose, this); - } else { - console.log( - `Failed to get connection for cid='${cid}' id='${id}' so closing...` - ); - this.close(); - } + await super.initialize(attributes, options); + this.base.disposed.connect(() => this.close()); + this.on('change:_dispose', this.dispose, this); } close(comm_closed?: boolean): Promise { diff --git a/src/widgets/frontend.ts b/src/widgets/frontend.ts index e87f42a..43a9980 100644 --- a/src/widgets/frontend.ts +++ b/src/widgets/frontend.ts @@ -36,8 +36,8 @@ export class JupyterFrontEndModel extends IpylabModel { * @param attributes The base attributes. * @param options The initialization options. */ - initialize(attributes: any, options: any): void { - super.initialize(attributes, options); + async initialize(attributes: any, options: any): Promise { + await super.initialize(attributes, options); this.set('version', this.app.version); Private.jupyterFrontEndModels.set(this.kernelId, this); diff --git a/src/widgets/ipylab.ts b/src/widgets/ipylab.ts index 46ee518..363d17a 100644 --- a/src/widgets/ipylab.ts +++ b/src/widgets/ipylab.ts @@ -55,26 +55,31 @@ export { * Base model for common features */ export class IpylabModel extends WidgetModel { - initialize(attributes: ObjectHash, options: any): void { + async initialize(attributes: ObjectHash, options: any): Promise { super.initialize(attributes, options); + try { + if (this.get('cid')) { + this._base = await this.getConnection(this.get('cid'), this.get('id')); + } else { + const basename = this.get('_basename'); + this._base = basename + ? getNestedObject({ + base: this, + path: basename, + basename: `model_name= '${this.defaults()._model_name}` + }) + : this; + } + } catch { + this.close(); + throw new Error(`Failed to set the base so closing...`); + } this.set('kernelId', this.kernelId); this.on('msg:custom', this._onCustomMessage, this); this.save_changes(); const msg = `ipylab ${this.get('_model_name')} ready for operations`; this.send({ init: msg }); onKernelLost(this.kernel, this.close, this); - if (typeof options.base === 'object') { - this._base = options.base; - } else { - const basename = this.get('_basename'); - this._base = basename - ? getNestedObject({ - base: this, - path: basename, - basename: `model_name= '${this.defaults()._model_name}` - }) - : this; - } } get base(): any { @@ -272,7 +277,11 @@ export class IpylabModel extends WidgetModel { onKernelLost(this.kernel, lw.dispose, lw); return lw; } - return this.getConnection(value); + const obj = await this.getConnection(value); + if (!(obj instanceof Widget)) { + throw new Error(`Not a widget '${value}'`); + } + return obj; } /** * Returns the object for the dotted path 'value'. @@ -438,11 +447,19 @@ export class IpylabModel extends WidgetModel { * @param cid Get an object that has been registered as a connection. * @returns */ - getConnection(cid: string, id: string | null = null): any { + async getConnection(cid: string, id: string | null = null): Promise { if (Private.connection.has(cid)) { return Private.connection.get(cid); } - const obj = this._getLuminoWidgetFromShell(id || cid); + let obj; + if (id.slice(0, 10) === 'IPY_MODEL_') { + obj = await unpack_models(id, this.widget_manager); + if (!(obj instanceof WidgetModel)) { + throw new Error(`Failed to get model ${id}`); + } + } else { + obj = this._getLuminoWidgetFromShell(id || cid); + } IpylabModel.registerConnection(obj, cid); return obj; } diff --git a/src/widgets/notification.ts b/src/widgets/notification.ts index 378f2c6..58305a7 100644 --- a/src/widgets/notification.ts +++ b/src/widgets/notification.ts @@ -3,13 +3,14 @@ import { Notification } from '@jupyterlab/apputils'; import { ObservableDisposableDelegate } from '@lumino/disposable'; +import { ObjectHash } from 'backbone'; import { IpylabModel } from './ipylab'; /** * The model for a notification. */ export class NotificationManagerModel extends IpylabModel { - initialize(attributes: Backbone.ObjectHash, options: any): void { - super.initialize(attributes, options); + async initialize(attributes: ObjectHash, options: any): Promise { + await super.initialize(attributes, options); Notification.manager.changed.connect(this.update, this); }