diff --git a/ipylab/connection.py b/ipylab/connection.py index c679c51..a59a5e7 100644 --- a/ipylab/connection.py +++ b/ipylab/connection.py @@ -4,13 +4,18 @@ from __future__ import annotations import uuid -from typing import Generic, TypeVar +from typing import TYPE_CHECKING, Generic, TypeVar from ipywidgets import register from traitlets import Bool, Unicode from ipylab.asyncwidget import AsyncWidgetBase +if TYPE_CHECKING: + from asyncio import Task + + from ipylab._compat.typing import Self + T = TypeVar("T", bound="Connection") @@ -57,7 +62,7 @@ class Connection(AsyncWidgetBase, Generic[T]): _connections: dict[str, T] = {} # noqa RUF012 _model_name = Unicode("ConnectionModel").tag(sync=True) cid = Unicode(read_only=True, help="connection id").tag(sync=True) - id = Unicode(allow_none=True, read_only=True, help="id of the disposable if it has one") + id = Unicode("", read_only=True, help="id of the object if it has one").tag(sync=True) _dispose = Bool(read_only=True).tag(sync=True) _basename = None @@ -80,7 +85,7 @@ def __init__(self, *, cid: str, model_id=None, id: str | None = None, **kwgs): if self._async_widget_base_init_complete: return self.set_trait("cid", cid) - self.set_trait("id", id) + self.set_trait("id", id or "") super().__init__(model_id=model_id, **kwgs) def __str__(self): @@ -130,6 +135,11 @@ def get_existing_connection(cls, *name_or_id: str, quiet=False): class MainAreaConnection(Connection): CID_PREFIX = "ipylab MainArea" - def activate(self): + def activate(self) -> Task[Self]: self._check_closed() - return self.app.shell.execute_method("activateById", self.id) + + async def activate_(): + self.app.shell.execute_method("activateById", self.id) + return self + + return self.to_task(activate_()) diff --git a/ipylab/jupyterfrontend.py b/ipylab/jupyterfrontend.py index 71493b8..e5dc811 100644 --- a/ipylab/jupyterfrontend.py +++ b/ipylab/jupyterfrontend.py @@ -7,6 +7,7 @@ from traitlets import Dict, Instance, Tuple, Unicode +from ipylab import MainAreaConnection from ipylab.asyncwidget import AsyncWidgetBase, Transform, pack, pack_code, register, widget_serialization from ipylab.commands import CommandRegistry from ipylab.dialog import Dialog, FileDialog @@ -38,6 +39,12 @@ class JupyterFrontEnd(AsyncWidgetBase): shell = Instance(Shell, (), read_only=True) session_manager = Instance(SessionManager, (), read_only=True) + @property + def current_widget(self): + """A connection to the current widget in the shell.""" + id_ = self.current_widget_id + return MainAreaConnection(cid=MainAreaConnection.to_cid(id_), id=id_) + def _init_python_backend(self): "Run by the Ipylab python backend." # This is called in a separate kernel started by the JavaScript frontend diff --git a/src/widgets/connection.ts b/src/widgets/connection.ts index 8de11bf..1309bb7 100644 --- a/src/widgets/connection.ts +++ b/src/widgets/connection.ts @@ -6,28 +6,44 @@ import { ObjectHash } from 'backbone'; import { IpylabModel } from './ipylab'; /** - * Maintain a connection to any object by using a cid. + * Provides a connection from any object reachable here to one or more Python backends. + * + * Typically the object is registered first via the method `registerConnection` with a cid + * In Python The `cid` is passed when creating a new Connection. + * + * The object is set as the base. If the object is disposable, the ConnectionModel will + * also close when the object is disposed. */ export class ConnectionModel extends IpylabModel { async initialize(attributes: ObjectHash, options: any): Promise { - super.initialize(attributes, { - ...options, - base: this.getConnection(this.get('cid')) - }); - this.base.disposed.connect(() => { - if (!this.get('_dispose')) { - this.close(); - } - }); - this.listenTo(this, 'change:_dispose', () => this.base.dispose()); + 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(); + } } close(comm_closed?: boolean): Promise { if (this.base?.ipylabDisposeOnClose) { delete this.base.ipylabDisposeOnClose; - this.base.dispose(); + this.dispose(); } - return super.close(comm_closed); + return super.close((comm_closed || this.get('_dispose')) ?? false); + } + + dispose() { + this.base?.dispose(); } /** diff --git a/src/widgets/ipylab.ts b/src/widgets/ipylab.ts index 513955e..c2ac9b7 100644 --- a/src/widgets/ipylab.ts +++ b/src/widgets/ipylab.ts @@ -57,14 +57,13 @@ export class IpylabModel extends WidgetModel { initialize(attributes: ObjectHash, options: any): void { super.initialize(attributes, options); this.set('kernelId', this.kernelId); - this.on('msg:custom', this._onCustomMessage.bind(this)); + 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; - delete options.base; } else { const basename = this.get('_basename'); this._base = basename @@ -240,7 +239,7 @@ export class IpylabModel extends WidgetModel { if (value.slice(0, 10) === 'IPY_MODEL_') { const model = await unpack_models(value, this.widget_manager); if (model.model_name === IpylabModel.connection_model_name) { - const widget = this.getConnection(model.cid); + const widget = this.getConnection(model.get('cid'), model.get('id')); if (!(widget instanceof Widget)) { throw new Error(`Failed to get a lumio widget for: ${value}`); } @@ -390,11 +389,11 @@ export class IpylabModel extends WidgetModel { * @param cid Get an object that has been registered as a connection. * @returns */ - getConnection(cid: string): any { + getConnection(cid: string, id: string | null = null): any { if (Private.connection.has(cid)) { return Private.connection.get(cid); } - const obj = this._getLuminoWidgetFromShell(cid); + const obj = this._getLuminoWidgetFromShell(id || cid); IpylabModel.registerConnection(obj, cid); return obj; }