Skip to content

Commit

Permalink
Change LuminoWidgetConnection to DisposableConnection
Browse files Browse the repository at this point in the history
  • Loading branch information
Alan Fleming committed Aug 19, 2024
1 parent 6b20884 commit 6f523ba
Show file tree
Hide file tree
Showing 16 changed files with 121 additions and 105 deletions.
46 changes: 24 additions & 22 deletions ipylab/asyncwidget.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,18 +26,15 @@
from collections.abc import Callable, Iterable
from typing import ClassVar

from ipylab.luminowidget_connection import LuminoWidgetConnection
from ipylab.disposable_connection import DisposableConnection


__all__ = ["AsyncWidgetBase", "WidgetBase", "register", "pack", "Widget"]


def pack(obj: Widget | LuminoWidgetConnection | Any):
def pack(obj: Widget | Any):
"""Return serialized obj if it is a Widget otherwise return it unchanged."""
from ipylab.luminowidget_connection import LuminoWidgetConnection

if isinstance(obj, LuminoWidgetConnection):
return obj.id
if isinstance(obj, Widget):
return widget_serialization["to_json"](obj, None)
return obj
Expand All @@ -55,9 +52,9 @@ class TransformMode(StrEnum):
- done: [default] A string '--DONE--'
- raw: No conversion. Note: data is serialized when sending, some objects shouldn't be serialized.
- string: Result is converted to a string.
- attribute: A dotted attribute of the returned object is returned. ['path']='dotted.path.name'
- function: Use a function to calculate the return value. ['code'] = 'function...'
- connection: Hopefully return a connection to a disposeable.
`attribute`
---------
Expand Down Expand Up @@ -185,8 +182,11 @@ def _check_closed(self):
msg = f"This widget is closed {self!r}"
raise RuntimeError(msg)

def new_task(self, coro: asyncio._CoroutineLike):
"""Start a task"""
def start_maybe(self, coro: asyncio._CoroutineLike, *, start: bool):
"Run the coro in a task only if start is True returning the coro or task."
self._check_closed()
if not start:
return coro
task = asyncio.create_task(coro)
self._tasks.add(task)
task.add_done_callback(self._tasks.discard)
Expand Down Expand Up @@ -236,9 +236,9 @@ async def _send_receive(self, content: dict):
async def _wait_response_check_error(self, response: Response, content: dict) -> Any:
payload = await response.wait()
if content["transform"] is TransformMode.connection:
from ipylab.luminowidget_connection import LuminoWidgetConnection
from ipylab.disposable_connection import DisposableConnection

return LuminoWidgetConnection(id=payload)
return DisposableConnection(id=payload)
return payload

def _on_frontend_msg(self, _, content: dict, buffers: list):
Expand All @@ -251,7 +251,9 @@ def _on_frontend_msg(self, _, content: dict, buffers: list):
if ipylab_backend:
self._pending_operations.pop(ipylab_backend).set(payload, error)
if "ipylab_FE" in content:
self.new_task(self._handle_frontend_operation(content["ipylab_FE"], operation, payload, buffers))
self.start_maybe(
self._handle_frontend_operation(content["ipylab_FE"], operation, payload, buffers), start=True
)
elif "init" in content:
self._ready_response.set(content)
elif "closed" in content:
Expand Down Expand Up @@ -302,7 +304,7 @@ def schedule_operation(
toLuminoWidget: Iterable[str] | None = None,
start=True,
**kwgs,
) -> asyncio._AwaitableLike[Dict | str | list | float | int | None | LuminoWidgetConnection]:
) -> asyncio._AwaitableLike[Dict | str | list | float | int | None | DisposableConnection]:
"""
operation: str
Expand Down Expand Up @@ -334,8 +336,7 @@ def schedule_operation(
content = {"ipylab_BE": ipylab_BE, "operation": operation, "kwgs": kwgs, "transform": TransformMode(transform)}
if toLuminoWidget:
content["toLuminoWidget"] = list(map(str, toLuminoWidget))
coro = self._send_receive(content)
return self.new_task(coro) if start else coro
return self.start_maybe(self._send_receive(content), start=start)

def execute_method(
self,
Expand Down Expand Up @@ -388,8 +389,7 @@ async def _list_methods():
return [n for n in payload if not n.startswith("_")]
return payload

coro = _list_methods()
return self.new_task(coro) if start else coro
return self.start_maybe(_list_methods(), start=start)

def list_attributes(
self,
Expand Down Expand Up @@ -421,15 +421,17 @@ async def list_attributes_():
return groups
return payload

coro = list_attributes_()
return self.new_task(coro) if start else coro
return self.start_maybe(list_attributes_(), start=start)

def execute_command(self, command_id: str, *, execute_settings: dict | None = None, **kwgs):
"""Execute command_id.
def execute_command(self, command_id: str, *, execute_kwgs: dict | None = None, **kwgs):
"""Execute the command_id registered with Jupyterlab.
`kwgs` correspond to `args` in JupyterLab.
Finding what the `args` are remains an outstanding issue in JupyterLab.
execute_kwgs: dict | None
Passed to execute_method (we use a dict to avoid any potential of argument clash).
Finding what `args` can be used remains an outstanding issue in JupyterLab.
see: https://github.com/jtpio/ipylab/issues/128#issuecomment-1683097383 for hints
about how args can be found.
Expand All @@ -438,5 +440,5 @@ def execute_command(self, command_id: str, *, execute_settings: dict | None = No
"app.commands.execute",
command_id,
kwgs, # -> used as 'args' in Jupyter
**execute_settings or {},
**execute_kwgs or {},
)
4 changes: 2 additions & 2 deletions ipylab/commands.py
Original file line number Diff line number Diff line change
Expand Up @@ -89,7 +89,7 @@ def addPythonCommand(
**kwgs,
)

def removePythonCommand(self, command_id: str):
def removePythonCommand(self, command_id: str, *, start=True):
# TODO: check whether to keep this method, or return disposables like in lab
if command_id not in self._execute_callbacks:
msg = f"{command_id=} is not a registered command!"
Expand All @@ -106,4 +106,4 @@ async def removePythonCommand_():
await coro_
self._execute_callbacks.pop(command_id, None)

return self.new_task(removePythonCommand_())
return self.start_maybe(removePythonCommand_(), start=start)
Original file line number Diff line number Diff line change
Expand Up @@ -3,28 +3,32 @@

from __future__ import annotations

import asyncio
import contextlib
from typing import ClassVar

from ipywidgets import register
from traitlets import Unicode

from ipylab.asyncwidget import AsyncWidgetBase
from ipylab.hasapp import HasApp
from ipylab.jupyterfrontend_subsection import FrontEndSubsection


@register
class LuminoWidgetConnection(AsyncWidgetBase, HasApp):
"""A connection to a single Lumino widget in the Jupyterlab shell.
class DisposableConnection(FrontEndSubsection, AsyncWidgetBase):
"""A connection to a disposable object in the Frontend.
The dispose method is directly accesssable, but
The comm trait can be observed for when the lumino widget in Jupyterlab is closed.
There is no direct connection to the widget on the frontend, rather, it
can be accessed using the prefix 'widget.' in the method calls:
* execute_method
see: https://lumino.readthedocs.io/en/latest/api/modules/disposable.html
"""

_connections: ClassVar[dict[str, LuminoWidgetConnection]] = {}
_model_name = Unicode("LuminoWidgetConnectionModel").tag(sync=True)
SUB_PATH_BASE = "obj"
_connections: ClassVar[dict[str, DisposableConnection]] = {}
_model_name = Unicode("DisposableConnectionModel").tag(sync=True)
id = Unicode(read_only=True).tag(sync=True)

def __new__(cls, *, id: str, **kwgs): # noqa: A002
Expand All @@ -42,8 +46,12 @@ def close(self):
self._connections.pop(self.id, None)
super().close()

def dispose(self):
"""Close the Lumino widget at the frontend.
def dispose(self, *, start=True) -> asyncio._AwaitableLike[None]:
"Close the disposable on the frontend."

async def dispose_():
if self.comm:
with contextlib.suppress(asyncio.CancelledError):
await self.execute_method("dispose", start=start)

Note: The task is cancelled when the connection is closed."""
return self.execute_method("widget.dispose")
return self.start_maybe(dispose_(), start=start)
23 changes: 12 additions & 11 deletions ipylab/jupyterfrontend_subsection.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,21 +5,21 @@
from typing import TYPE_CHECKING

from ipylab import TransformMode
from ipylab.hasapp import HasApp
from ipylab.asyncwidget import AsyncWidgetBase

if TYPE_CHECKING:
from collections.abc import Iterable

from ipylab.asyncwidget import TransformType


class JupyterFrontEndSubsection(HasApp):
"""Use as a sub section in the JupyterFrontEnd class"""
class FrontEndSubsection(AsyncWidgetBase):
"""Direct access to methods on an object relative to the frontend Model."""

# Point to the attribute on the JupyterFrontEndModel for which this class represents.
# Point to the attribute on the model to which this model corresponds.
# Nested attributes are support such as "app.sessionManager"
# see ipylab/src/widgets/frontend.ts -> JupyterFrontEndModel
JFE_JS_SUB_PATH = ""
# see ipylab/src/widgets/ipylab.ts -> IpylabModel
SUB_PATH_BASE = "app"

def execute_method(
self,
Expand All @@ -32,8 +32,9 @@ def execute_method(
"""Execute a nested method on this objects JFE_SUB_PATH relative to the instance of the
JupyterFrontEndModel in the JS frontend.
"""
return self.app.execute_method(
f"{self.JFE_JS_SUB_PATH}.{method}",

return super().execute_method(
f"{self.SUB_PATH_BASE}.{method}",
*args,
transform=transform,
toLuminoWidget=toLuminoWidget,
Expand All @@ -42,12 +43,12 @@ def execute_method(

def get_attribute(self, path: str, *, transform: TransformType = TransformMode.raw, start=True):
"""Get an attribute by name from the front end."""
return self.app.get_attribute(f"{self.JFE_JS_SUB_PATH}.{path}", transform=transform, start=start)
return super().get_attribute(f"{self.SUB_PATH_BASE}.{path}", transform=transform, start=start)

def list_attributes(self, path: str = "", *, transform: TransformType = TransformMode.raw, start=True):
"""Get a list of all attributes."""
return self.app.list_attributes(
f"{self.JFE_JS_SUB_PATH}.{path}",
return super().list_attributes(
f"{self.SUB_PATH_BASE}.{path}",
transform=transform,
start=start,
)
6 changes: 3 additions & 3 deletions ipylab/sessions.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,15 +3,15 @@

import asyncio

from ipylab.jupyterfrontend_subsection import JupyterFrontEndSubsection
from ipylab.jupyterfrontend_subsection import FrontEndSubsection


class SessionManager(JupyterFrontEndSubsection):
class SessionManager(FrontEndSubsection):
"""
https://jupyterlab.readthedocs.io/en/latest/api/interfaces/services.Session.IManager.html
"""

JFE_JS_SUB_PATH = "sessionManager"
SUB_PATH_BASE = "app.sessionManager"

def refreshRunning(self) -> asyncio.Task:
"""Force a call to refresh running sessions."""
Expand Down
12 changes: 6 additions & 6 deletions ipylab/shell.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,8 @@

from ipylab import pack
from ipylab.asyncwidget import TransformMode
from ipylab.jupyterfrontend_subsection import JupyterFrontEndSubsection
from ipylab.luminowidget_connection import LuminoWidgetConnection
from ipylab.disposable_connection import DisposableConnection
from ipylab.jupyterfrontend_subsection import FrontEndSubsection

if sys.version_info >= (3, 11):
from enum import StrEnum
Expand Down Expand Up @@ -48,7 +48,7 @@ class InsertMode(StrEnum):
tab_after = "tab-after"


class Shell(JupyterFrontEndSubsection):
class Shell(FrontEndSubsection):
"""
Provides access to the shell.
The minimal interface is:
Expand All @@ -59,7 +59,7 @@ class Shell(JupyterFrontEndSubsection):
ref: https://jupyterlab.readthedocs.io/en/latest/api/interfaces/application.JupyterFrontEnd.IShell.html#add
"""

JFE_JS_SUB_PATH = "shell"
SUB_PATH_BASE = "app.shell"

def addToShell(
self,
Expand All @@ -69,7 +69,7 @@ def addToShell(
activate: bool = True,
mode: InsertMode = InsertMode.split_right,
rank: int | None = None,
ref: LuminoWidgetConnection | str = "",
ref: DisposableConnection | str = "",
start=True,
**options,
):
Expand All @@ -86,7 +86,7 @@ def addToShell(
"activate": activate,
"mode": InsertMode(mode),
"rank": int(rank) if rank else None,
"ref": ref.id if isinstance(ref, LuminoWidgetConnection) else ref or None,
"ref": ref.id if isinstance(ref, DisposableConnection) else ref or None,
}
return self.app.schedule_operation(
"addToShell",
Expand Down
6 changes: 3 additions & 3 deletions ipylab/widgets.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@
if TYPE_CHECKING:
import asyncio

from ipylab.luminowidget_connection import LuminoWidgetConnection
from ipylab.disposable_connection import DisposableConnection


@register
Expand Down Expand Up @@ -64,10 +64,10 @@ def addToShell(
activate: bool = True,
mode: InsertMode = InsertMode.split_right,
rank: int | None = None,
ref: LuminoWidgetConnection | str = "",
ref: DisposableConnection | str = "",
start=True,
**options,
) -> asyncio._AwaitableLike[LuminoWidgetConnection]:
) -> asyncio._AwaitableLike[DisposableConnection]:
"""Add this panel to the shell."""
return self.app.shell.addToShell(
self,
Expand Down
4 changes: 2 additions & 2 deletions src/widget.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import { CommandRegistryModel } from './widgets/commands';
import { JupyterFrontEndModel } from './widgets/frontend';
import { IconModel, IconView } from './widgets/icon';
import { IpylabModel } from './widgets/ipylab';
import { LuminoWidgetConnectionModel } from './widgets/luminowidget_connection';
import { DisposableConnectionModel } from './widgets/disposable_connection';
import { MainAreaModel } from './widgets/main_area';
import { CommandPaletteModel, LauncherModel } from './widgets/palette';
import { PanelModel, PanelView } from './widgets/panel';
Expand All @@ -20,7 +20,7 @@ export {
IpylabModel,
JupyterFrontEndModel,
LauncherModel,
LuminoWidgetConnectionModel,
DisposableConnectionModel,
MainAreaModel,
PanelModel,
PanelView,
Expand Down
Loading

0 comments on commit 6f523ba

Please sign in to comment.