Skip to content

Commit

Permalink
Switched default transform from 'done' to 'raw'
Browse files Browse the repository at this point in the history
added 'utils.ts' - a place to put useful functions.
added 'newSession' - now provides comms for widgets without needing a running notebook.
Works okay, but needs further refinement.
Now obtaining kernelId from widget_manager rather than from kernel side.
  • Loading branch information
Alan Fleming committed Jan 30, 2024
1 parent a7a0c3d commit bf55c5c
Show file tree
Hide file tree
Showing 15 changed files with 602 additions and 557 deletions.
12 changes: 7 additions & 5 deletions .pre-commit-config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -11,9 +11,11 @@ repos:
rev: 'v0.8.1'
hooks:
- id: taplo
- repo: https://github.com/astral-sh/ruff-pre-commit
rev: v0.1.11
- repo: local
hooks:
- id: ruff
args: ["--fix"]
- id: ruff-format
- id: ruff-format-hatch-settings
name: hatch-ruff
language: system
entry: hatch fmt
pass_filenames: false
verbose: true
44 changes: 28 additions & 16 deletions examples/autostart.ipynb
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,9 @@
"source": [
"# Autostart\n",
"\n",
"Auto start-up is enabled via a the `pluggy` framework and works by loading `ipylab` in a kernel named `Ipylab backend` that automatically starts when `ipylab` is activated. Plugins are found using the entry point: \"ipylab-python-backend\". Autostart simply calls `Python code`. \n",
"Autostart is a feature that will call all registered Python functions When `ipylab` is activated. Normally this will occur when Jupyterlab is started. The feature is implemented using the `pluggy`.\n",
"\n",
"There are no limitations to what can be done.\n",
"\n",
"## Entry points\n",
"\n",
Expand All @@ -42,19 +44,20 @@
"source": [
"# @my_module.autostart.py\n",
"\n",
"import asyncio\n",
"\n",
"import ipylab\n",
"\n",
"app = ipylab.JupyterFrontEnd()\n",
"\n",
"\n",
"def create_app():\n",
"async def create_app():\n",
" # Ensure this function provides all the imports.\n",
" global ma\n",
" import ipywidgets as ipw\n",
"\n",
" import ipylab\n",
"\n",
" # app = ipylab.JupyterFrontEnd()\n",
" # await app.wait_ready()\n",
" ma = ipylab.MainArea(name=\"My demo app\")\n",
" console_button = ipw.Button(description=\"Toggle console\")\n",
" console_button.on_click(\n",
Expand All @@ -64,24 +67,15 @@
" ipw.HTML(f\"<h3>My simple app</h3> Welcome to my app.<br> kernel id: {ma.kernelId}\"),\n",
" console_button,\n",
" ]\n",
" ma.content.label = \"This is my app\"\n",
" ma.load()\n",
" print(\"Finshed creating my app\")\n",
"\n",
" # Retun ma so it doesn't accidentally get garbage collected.\n",
" return ma\n",
"\n",
"\n",
"async def load_app_async():\n",
" session = await app.newNotebook(path=\"my app\")\n",
" kernelId = session[\"kernel\"][\"id\"]\n",
" await app.injectCode(kernelId, create_app)\n",
" print(\"Done\")\n",
"\n",
"\n",
"class MyPlugins:\n",
" @ipylab.hookspecs.hookimpl()\n",
" def run_once_at_startup(self):\n",
" asyncio.create_task(load_app_async())\n",
" app.newSession(path=\"my app\", code=create_app)\n",
"\n",
"\n",
"MyPlugins()"
Expand All @@ -91,7 +85,25 @@
"cell_type": "markdown",
"metadata": {},
"source": [
"### Lauch app\n",
"### Launch the app manually\n",
"\n",
"We can 'launch' the app in a new kernel."
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"app.newSession(path=\"my app\", code=create_app)"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"### Auto Lauch app\n",
"Simulate code launch in the as it happens in `Ipylab backend`"
]
},
Expand Down
25 changes: 25 additions & 0 deletions examples/sessions.ipynb
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,31 @@
"source": [
"app.commands.execute(\"notebook:create-console\")"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"## Example: Create New Session (Python with comms)"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"t = app.newSession(\"second\", code=\"import ipylab;app = ipylab.JupyterFrontEnd()\")"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"t.result()"
]
}
],
"metadata": {
Expand Down
49 changes: 30 additions & 19 deletions ipylab/asyncwidget.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,26 +4,21 @@

import asyncio
import enum
import inspect
import textwrap
import types
import uuid
from collections.abc import Callable, Coroutine
from typing import Any

from IPython.core.getipython import get_ipython
from ipywidgets import Widget, register, widget_serialization
from traitlets import Bool, Dict, Instance, Set, Unicode

import ipylab._frontend as _fe
from ipylab.hookspecs import pm

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

# Currently only checks for an IPython kernel. A better way of getting the kernelId would be useful.
ip = get_ipython()
KERNEL_ID = (
ip.kernel.config["IPKernelApp"]["connection_file"].split("kernel-", 1)[1].removesuffix(".json")
if ip
else "NO KERNEL"
)


def pack(obj: Widget):
"""Return serialized obj if it is a Widget otherwise return it unchanged."""
Expand All @@ -32,6 +27,27 @@ def pack(obj: Widget):
return obj


def pack_code(code: str | types.ModuleType) -> str:
"""Convert code to a string suitable to run in a kernel."""
if not isinstance(code, str):
should_call = callable(code)
func_name = code.__name__
code = inspect.getsource(code)
if should_call:
code = textwrap.dedent(
f"""
import asyncio
{{code}}
result = {func_name}()
if asyncio.iscoroutine(result):
task = asyncio.create_task(result)
"""
).format(code=code)
return code


class TransformMode(enum.StrEnum):
"""The transformation to apply to the result of frontend operations prior to sending.
Expand Down Expand Up @@ -102,7 +118,7 @@ class WidgetBase(Widget):
class AsyncWidgetBase(WidgetBase):
"""The base for all widgets that need async comms with the frontend model."""

kernelId = Unicode(KERNEL_ID, read_only=True).tag(sync=True)
kernelId = Unicode(read_only=True).tag(sync=True)
_ipylab_model_register: dict[str, AsyncWidgetBase] = {}
_singleton_register: dict[type, str] = {}
SINGLETON = False
Expand All @@ -127,11 +143,6 @@ def __new__(cls, *, model_id=None, **kwgs):
def __init__(self, *, model_id=None, **kwgs):
if self._model_id:
return
if not self.kernelId:
raise RuntimeError(
f"{self.__class__.__name__} requries a running kernel."
"kernelId is not set meaning that a kernel is not running."
)
super().__init__(model_id=model_id, **kwgs)
self._ipylab_model_register[self.model_id] = self
if self.SINGLETON:
Expand Down Expand Up @@ -256,7 +267,7 @@ def schedule_operation(
operation: str,
*,
callback: Callable[[any, any], None | Coroutine] = None,
transform: TransformMode | dict[str, str] = TransformMode.done,
transform: TransformMode | dict[str, str] = TransformMode.raw,
**kwgs,
) -> asyncio.Task:
"""
Expand Down Expand Up @@ -300,7 +311,7 @@ def execute_method(
method: str,
*args,
callback: Callable[[any, any], None | Coroutine] = None,
transform: TransformMode | dict[str, str] = TransformMode.done,
transform: TransformMode | dict[str, str] = TransformMode.raw,
) -> asyncio.Task:
"""Call a method on the corresponding frontend object.
Expand Down Expand Up @@ -334,7 +345,7 @@ def get_attribute(
name: str,
*,
callback: Callable[[any, any], None | Coroutine] = None,
transform: TransformMode | dict[str, str] = TransformMode.done,
transform: TransformMode | dict[str, str] = TransformMode.raw,
):
"""A serialized verison of the attribute relative to this object."""
raise NotImplementedError("TODO")
Expand All @@ -355,7 +366,7 @@ def list_attributes(
base: str = "",
*,
callback: Callable[[any, any], None | Coroutine] = None,
transform: TransformMode | dict[str, str] = TransformMode.done,
transform: TransformMode | dict[str, str] = TransformMode.raw,
):
"""A serialized verison of the attribute relative to this object."""
raise NotImplementedError("TODO")
Expand Down
2 changes: 1 addition & 1 deletion ipylab/commands.py
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,7 @@ async def _do_operation_for_frontend(
def execute(
self,
command_id: str,
transform: TransformMode | dict[str, str] = TransformMode.done,
transform: TransformMode | dict[str, str] = TransformMode.raw,
**args,
) -> asyncio.Task:
"""Execute command_id.
Expand Down
84 changes: 56 additions & 28 deletions ipylab/jupyterfrontend.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,14 +3,18 @@
from __future__ import annotations

import asyncio
import inspect
import textwrap
import types
from typing import NotRequired, Self, TypedDict

from traitlets import Dict, Instance, Tuple, Unicode

from ipylab.asyncwidget import AsyncWidgetBase, register, widget_serialization
from ipylab.asyncwidget import (
AsyncWidgetBase,
TransformMode,
pack_code,
register,
widget_serialization,
)
from ipylab.commands import CommandPalette, CommandRegistry
from ipylab.dialog import Dialog, FileDialog
from ipylab.sessions import SessionManager
Expand All @@ -24,27 +28,6 @@ class LauncherOptions(TypedDict):
icon: NotRequired[str]


def pack_code(code: str | types.ModuleType) -> str:
"""Convert code to a string suitable to run in a kernel."""
if not isinstance(code, str):
should_call = callable(code)
func_name = code.__name__
code = inspect.getsource(code)
if should_call:
code = textwrap.dedent(
f"""
import asyncio
{{code}}
result = {func_name}()
if asyncio.iscoroutine(result):
task = asyncio.create_task(result)
"""
).format(code=code)
return code


@register
class JupyterFrontEnd(AsyncWidgetBase):
_model_name = Unicode("JupyterFrontEndModel").tag(sync=True)
Expand Down Expand Up @@ -102,18 +85,57 @@ def _init_python_backend(self) -> str:
pm.load_setuptools_entrypoints("ipylab-python-backend")
result = pm.hook.run_once_at_startup()

def newNotebook(
def newSession(
self,
path: str = "",
*,
name: str = "",
kernelId="",
kernelName="python3",
code: str | types.ModuleType = "",
) -> asyncio.Task:
"""
Create a new kernel and execute code in it or execute code in an existing kernel.
path: The session path.
type: The type of session.
name: The name of the session.
kernel: The kernel details.
code: A string, module or function.
If passing a function, the function will be executed. It is important
that objects that must stay alive outside the function must be kept alive.
So it is advised to use a code.
"""
return self.schedule_operation(
"newSession",
path=path,
name=name or path,
kernelId=kernelId,
kernelName=kernelName,
code=pack_code(code),
transform=TransformMode.raw,
)

def newNotebook(
self,
path: str = "",
*,
name: str = "",
kernelId="",
kernelName="python3",
code: str | types.ModuleType = "",
) -> asyncio.Task:
"""Create a new notebook."""
return self.execute_method(
"newNotebook", name, path, kernelId, kernelName, pack_code(code), transform="raw"
return self.schedule_operation(
"newNotebook",
path=path,
name=name or path,
kernelId=kernelId,
kernelName=kernelName,
code=pack_code(code),
transform=TransformMode.raw,
)

def injectCode(self, kernelId: str, code: str | types.ModuleType) -> asyncio.Task:
Expand All @@ -127,4 +149,10 @@ def injectCode(self, kernelId: str, code: str | types.ModuleType) -> asyncio.Tas
Return objects from the function to should be retained.
"""

return self.execute_method("injectCode", kernelId, pack_code(code), transform="raw")
return self.schedule_operation(
"injectCode", kernelId=kernelId, code=pack_code(code), transform=TransformMode.raw
)

def startIyplabPythonBackend(self) -> asyncio.Task:
"""Checks backend is running and starts it if it isn't returning the session model."""
return self.schedule_operation("startIyplabPythonBackend")
Loading

0 comments on commit bf55c5c

Please sign in to comment.