Skip to content

Commit

Permalink
DisposableConnection - use cid instead of id
Browse files Browse the repository at this point in the history
  • Loading branch information
Alan Fleming committed Aug 28, 2024
1 parent 11d53ea commit 0c96d6c
Show file tree
Hide file tree
Showing 8 changed files with 87 additions and 95 deletions.
2 changes: 1 addition & 1 deletion examples/autostart.ipynb
Original file line number Diff line number Diff line change
Expand Up @@ -112,7 +112,7 @@
" start_my_app.callcount = n\n",
" path = f\"my app {n}\"\n",
" launcher = (\n",
" ipylab.DisposableConnection(id=app.current_widget_id) if app.current_widget_id.startswith(\"launcher\") else None\n",
" ipylab.DisposableConnection(cid=app.current_widget_id) if app.current_widget_id.startswith(\"launcher\") else None\n",
" )\n",
" await app.exec_eval(execute=create_app, evaluate={\"path\": f\"'{path}'\", \"payload\": \"create_app(path)\"}, path=path)\n",
" if launcher:\n",
Expand Down
32 changes: 25 additions & 7 deletions examples/commands.ipynb
Original file line number Diff line number Diff line change
Expand Up @@ -111,6 +111,24 @@
"w = t.result()"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"t = w.list_attributes()"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"t.result()"
]
},
{
"cell_type": "code",
"execution_count": null,
Expand Down Expand Up @@ -464,19 +482,19 @@
]
},
{
"cell_type": "markdown",
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"Notice the command is not in the pallet."
"show_command_pallet()"
]
},
{
"cell_type": "code",
"execution_count": null,
"cell_type": "markdown",
"metadata": {},
"outputs": [],
"source": [
"show_command_pallet()"
"Notice the command is not in the pallet."
]
},
{
Expand Down Expand Up @@ -539,7 +557,7 @@
"metadata": {},
"outputs": [],
"source": [
"assert str(cmd) not in app.commands.all_commands # noqa: S101"
"assert str(cmd) in app.commands.all_commands # noqa: S101"
]
},
{
Expand Down
34 changes: 13 additions & 21 deletions ipylab/commands.py
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,7 @@ def configure(self, *, emit=True, **kwgs: Unpack[CommandOptions]) -> Task[Comman
async def configure_():
config = await self.update_values("config", kwgs) # type: ignore
if emit:
await self.app.commands.execute_method("commandChanged.emit", {"id": self.id})
await self.app.commands.execute_method("commandChanged.emit", {"id": self.cid})
return config

return self.to_task(configure_())
Expand Down Expand Up @@ -117,10 +117,6 @@ class CommandPalette(AsyncWidgetBase):
SINGLETON = True
items: Container[tuple[CommandPalletConnection, ...]] = TypedTuple(trait=Instance(CommandPalletConnection))

def _to_pallet_command_category_id(self, command_id: str | CommandConnection, category: str):
cmd = str(CommandConnection.get_existing_connection(command_id))
return f"{CommandPalletConnection.to_id(str(cmd))} | {category}"

def add(
self,
command: str | CommandConnection,
Expand All @@ -130,8 +126,8 @@ def add(
args: dict | None = None,
) -> Task[CommandPalletConnection]:
"""Add a command to the command pallet (must be registered in this kernel)."""
id_ = self._to_pallet_command_category_id(command, category)
conn = CommandPalletConnection.get_existing_connection(id_, quiet=True)
cid = CommandPalletConnection.to_cid(str(command), category)
conn = CommandPalletConnection.get_existing_connection(cid, quiet=True)
if conn:
conn.dispose()
return self.execute_method(
Expand All @@ -142,14 +138,12 @@ def add(
"command": str(command),
"rank": rank,
},
id=id_,
cid=cid,
transform=TransformMode.connection,
)

def remove(self, command_id: str | CommandConnection, category: str):
conn = CommandPalletConnection.get_existing_connection(
self._to_pallet_command_category_id(command_id, category), quiet=True
)
def remove(self, commandID: str | CommandConnection, category: str):
conn = CommandPalletConnection.get_existing_connection(str(commandID), category, quiet=True)
if conn:
conn.dispose()

Expand All @@ -159,17 +153,13 @@ class Launcher(AsyncWidgetBase):
SINGLETON = True
items: Container[tuple[LauncherConnection, ...]] = TypedTuple(trait=Instance(LauncherConnection))

def _to_launcher_connection_id(self, command_id: str | CommandConnection, category: str):
cmd = str(CommandConnection.get_existing_connection(command_id))
return f"{LauncherConnection.to_id(str(cmd))} | {category}"

def add(self, command: str | CommandConnection, category: str, *, rank=None, **args) -> Task[LauncherConnection]:
"""Add a launcher for the command (must be registered in this kerenel).
ref: https://jupyterlab.readthedocs.io/en/latest/api/interfaces/launcher.ILauncher.IItemOptions.html
"""
id_ = self._to_launcher_connection_id(command, category)
conn = LauncherConnection.get_existing_connection(id_, quiet=True)
cid = LauncherConnection.to_cid(str(command), category)
conn = LauncherConnection.get_existing_connection(cid, quiet=True)
if conn:
conn.dispose()
return self.execute_method(
Expand All @@ -180,12 +170,12 @@ def add(self, command: str | CommandConnection, category: str, *, rank=None, **a
"rank": rank,
"args": args,
},
id=id_,
cid=cid,
transform=TransformMode.connection,
)

def remove(self, command_id: str | CommandConnection, category: str):
conn = LauncherConnection.get_existing_connection(self._to_launcher_connection_id(command_id, category))
conn = LauncherConnection.get_existing_connection(str(command_id), category)
if conn:
conn.dispose()

Expand Down Expand Up @@ -241,9 +231,11 @@ def add(
"""
tfm = dict(frontend_transform_kwgs) if frontend_transform_kwgs else {}
tfm["transform"] = TransformMode(frontend_transform)
cid = CommandConnection.to_cid(name)
task = self.schedule_operation(
"addCommand",
id=CommandConnection.to_id(name),
id=cid,
cid=cid,
caption=caption,
label=label,
iconClass=icon_class,
Expand Down
58 changes: 30 additions & 28 deletions ipylab/disposable_connection.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@

from __future__ import annotations

import uuid
from typing import Generic, TypeVar

from ipywidgets import register
Expand All @@ -20,62 +21,63 @@ class DisposableConnection(AsyncWidgetBase, Generic[T]):
This defines the 'base' as the disposable object meaning the frontend attribute methods
are associated directly with the object on the frontend.
The 'dispose' method will call the dispose method on the frontend object and close.
The 'dispose' method will call the dispose method on the frontend object and close
see: https://lumino.readthedocs.io/en/latest/api/modules/disposable.html
Subclasses that are inherited with and ID_PREFIX
Subclasses that are inherited with and ID_PREFIX.
In some cases it may be necessary use the keyword argument `cid` to ensure
the subclass instance is returned. The class methods `to_cid` and `new_cid`
will generate an appropriate id.
"""

ID_PREFIX = ""
_CLASS_DEFINITIONS: dict[str, type[T]] = {} # noqa RUF012
_connections: dict[str, T] = {} # noqa RUF012
_model_name = Unicode("DisposableConnectionModel").tag(sync=True)
id = Unicode(read_only=True).tag(sync=True)
cid = Unicode(read_only=True).tag(sync=True)
_dispose = Bool(read_only=True).tag(sync=True)

def __init_subclass__(cls, **kwargs) -> None:
if cls.ID_PREFIX:
cls._CLASS_DEFINITIONS[cls.ID_PREFIX] = cls # type: ignore
super().__init_subclass__(**kwargs)

def __new__(cls, *, id: str, **kwgs): # noqa A002
if id not in cls._connections:
if cls.ID_PREFIX and not id.startswith(cls.ID_PREFIX):
msg = f"Expected prefix '{cls.ID_PREFIX}' not found for {id=}"
def __new__(cls, *, cid: str, **kwgs):
if cid not in cls._connections:
if cls.ID_PREFIX and not cid.startswith(cls.ID_PREFIX):
msg = f"Expected prefix '{cls.ID_PREFIX}' not found for {cid=}"
raise ValueError(msg)
# Check if a subclass is registered with 'ID_PREFIX'
cls_ = cls._CLASS_DEFINITIONS.get(id.split(":")[0], cls) if ":" in id else cls
cls._connections[id] = super().__new__(cls_, **kwgs) # type: ignore
return cls._connections[id]
cls_ = cls._CLASS_DEFINITIONS.get(cid.split(":")[0], cls) if ":" in cid else cls
cls._connections[cid] = super().__new__(cls_, **kwgs) # type: ignore
return cls._connections[cid]

def __str__(self):
return self.id
return self.cid

@classmethod
def to_cid(cls, *args: str) -> str:
"""Generate an id for the args"""
return " | ".join([f"{cls.ID_PREFIX}:{args[0].removeprefix(cls.ID_PREFIX).strip(':')}", *args[1:]]).strip(": ")

@classmethod
def to_id(cls, name_or_id: str | T) -> str:
"""Generate an id for the given name."""
if isinstance(name_or_id, DisposableConnection):
if not isinstance(name_or_id, cls):
msg = f"{name_or_id} is not a subclass of {cls}"
raise TypeError(msg)
return name_or_id.id
if not cls.ID_PREFIX:
return name_or_id
return f"{cls.ID_PREFIX}:{name_or_id.removeprefix(cls.ID_PREFIX).strip(':')}"
def new_cid(cls, *args):
return cls.to_cid(str(uuid.uuid4()), *args)

@classmethod
def get_instances(cls) -> tuple[T]:
return tuple(item for item in cls._connections.values() if item.__class__ is cls) # type: ignore

def __init__(self, *, id: str, model_id=None, **kwgs): # noqa: A002
def __init__(self, *, cid: str, model_id=None, **kwgs):
if self._async_widget_base_init_complete:
return
self.set_trait("id", id)
self.set_trait("cid", cid)
super().__init__(model_id=model_id, **kwgs)

def close(self):
self._connections.pop(self.id, None)
self._connections.pop(self.cid, None)
super().close()

def dispose(self):
Expand All @@ -84,17 +86,17 @@ def dispose(self):
self.close()

@classmethod
def get_existing_connection(cls, name_or_id: str | T, *, quiet=False):
def get_existing_connection(cls, *name_or_id: str, quiet=False):
"""Get an existing connection.
quiet: bool
If the connection does exist:
* False -> Will raise an error.
* True -> Will return None.
"""
id_ = cls.to_id(name_or_id)
conn = cls._connections.get(id_)
cid = cls.to_cid(*name_or_id)
conn = cls._connections.get(cid)
if not conn and not quiet:
msg = f"A connection does not exist with id='{id_}'"
msg = f"A connection does not exist with id='{cid}'"
raise ValueError(msg)
return conn
5 changes: 3 additions & 2 deletions ipylab/shell.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,13 +7,14 @@
from ipylab import pack
from ipylab._compat.enum import StrEnum
from ipylab.asyncwidget import AsyncWidgetBase, TransformMode, Unicode
from ipylab.disposable_connection import DisposableConnection

if t.TYPE_CHECKING:
from asyncio import Task

from ipywidgets import Widget

from ipylab.disposable_connection import DisposableConnection

__all__ = ["Area", "InsertMode", "Shell"]


Expand Down Expand Up @@ -80,7 +81,7 @@ def addToShell(
"activate": activate,
"mode": InsertMode(mode),
"rank": int(rank) if rank else None,
"ref": ref.id if isinstance(ref, DisposableConnection) else ref or None,
"ref": pack(ref),
}
return self.app.schedule_operation(
"addToShell",
Expand Down
12 changes: 1 addition & 11 deletions src/widgets/commands.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,7 @@

import { unpack_models } from '@jupyter-widgets/base';
import { CommandRegistry } from '@lumino/commands';
import {
IDisposable,
ISerializers,
IpylabModel,
JSONValue,
onKernelLost
} from './ipylab';
import { IDisposable, ISerializers, IpylabModel, JSONValue } from './ipylab';
/**
* The model for a command registry.
*/
Expand Down Expand Up @@ -108,11 +102,7 @@ export class CommandRegistryModel extends IpylabModel {
usage: () => config?.usage
};
const command = this.commands.addCommand(id, options_ as any);
(command as any).id = id;
(command as any).config = config;

IpylabModel.trackDisposable(command);
onKernelLost(this.kernel, command.dispose, command);
return command;
}

Expand Down
6 changes: 2 additions & 4 deletions src/widgets/disposable_connection.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,7 @@ import { ObjectHash } from 'backbone';
import { IpylabModel } from './ipylab';

/**
* Maintain a connection to a Lumino widget such as a MainAreaWidget, Console, TextEditor, etc...
* The widget must exist in the shell or have already been added to the tracker.
*
* Maintain a connection to any object with a dispose method.
*/
export class DisposableConnectionModel extends IpylabModel {
async initialize(
Expand All @@ -26,7 +24,7 @@ export class DisposableConnectionModel extends IpylabModel {

get base() {
try {
return this.getDisposable(this.get('id'));
return this.getDisposable(this.get('cid'));
} catch {
this.close();
}
Expand Down
Loading

0 comments on commit 0c96d6c

Please sign in to comment.