Skip to content

Commit

Permalink
Refactoring + remove reliance on _model_id.
Browse files Browse the repository at this point in the history
  • Loading branch information
Alan Fleming committed Jun 7, 2024
1 parent 8ea26ff commit b2503f6
Show file tree
Hide file tree
Showing 11 changed files with 67 additions and 91 deletions.
29 changes: 12 additions & 17 deletions examples/autostart.ipynb
Original file line number Diff line number Diff line change
Expand Up @@ -72,7 +72,7 @@
"import ipylab\n",
"\n",
"\n",
"async def create_app():\n",
"async def create_app(path):\n",
" # The code in this function is called in the new kernel.\n",
" # Ensure imports are performed inside the function.\n",
" import ipywidgets as ipw\n",
Expand All @@ -81,7 +81,7 @@
"\n",
" global ma # noqa: PLW0603\n",
"\n",
" ma = ipylab.MainArea(name=\"My demo app\")\n",
" ma = ipylab.MainArea(name=\"My demo app\", path=path)\n",
" await ma.wait_ready()\n",
" ma.content.title.label = \"Simple app\"\n",
" ma.content.title.caption = ma.kernelId\n",
Expand All @@ -92,7 +92,7 @@
" \"The dialog demonstrates the use of the `on_frontend_error` plugin.\",\n",
" )\n",
" console_button.on_click(\n",
" lambda _: ma.load_console(path=\"console\") if ma.console_status == \"unloaded\" else ma.unload_console()\n",
" lambda _: ma.load_console(name=\"console\") if ma.console_status == \"unloaded\" else ma.unload_console()\n",
" )\n",
" error_button.on_click(lambda _: ma.executeCommand(\"Not a command\"))\n",
" console_status = ipw.HTML()\n",
Expand All @@ -118,7 +118,12 @@
"\n",
" # Register plugin for this kernel.\n",
" ipylab.hookspecs.pm.register(IpylabPlugins()) # type: ignore\n",
"\n",
" # Close the launcher if it still has the focus.\n",
" if ma.app.current_widget_id.startswith(\"launcher\"):\n",
" await ma.app.executeMethod(\"app.shell.currentWidget.dispose\")\n",
" await ma.load()\n",
" return ipylab.pack(ma)\n",
"\n",
"\n",
"n = 0\n",
Expand All @@ -128,13 +133,11 @@
"async def start_my_app(cwd): # noqa: ARG001\n",
" global n # noqa: PLW0603\n",
" n += 1\n",
" # Currently we need to use notebooks for widgets to in a kernel.\n",
" session = await app.newNotebook(f\"test{n}\")\n",
" app.execEval(\n",
" path = f\"my app {n}\"\n",
" return await app.execEval(\n",
" code=create_app,\n",
" user_expressions={\"main_area_widget\": \"create_app()\"},\n",
" path=f\"my app {n}\",\n",
" kernelId=session[\"kernel\"][\"id\"],\n",
" user_expressions={\"main_area_widget\": \"create_app(path)\"},\n",
" path=path,\n",
" )\n",
"\n",
"\n",
Expand Down Expand Up @@ -177,16 +180,8 @@
"metadata": {},
"outputs": [],
"source": [
"# There is a new launcher called 'Start custom app'\n",
"t = app.executeCommand(\"launcher:create\")"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": []
}
],
"metadata": {
Expand Down
17 changes: 9 additions & 8 deletions ipylab/asyncwidget.py
Original file line number Diff line number Diff line change
Expand Up @@ -134,11 +134,11 @@ class AsyncWidgetBase(WidgetBase):
"""The base for all widgets that need async comms with the frontend model."""

kernelId = Unicode(read_only=True).tag(sync=True) # noqa: N815
_async_widget_base_init_complete = False
_ipylab_model_register: ClassVar[dict[str, Any]] = {}
_singleton_register: ClassVar[dict[str, str]] = {}
SINGLETON = False
_ready_response = Instance(Response, ())
_model_id = None
_pending_operations: Dict[str, Response] = Dict()
_tasks: Container[set[asyncio.Task]] = Set()
_comm = None
Expand All @@ -155,13 +155,15 @@ def __new__(cls, *, model_id=None, **kwgs):
return super().__new__(cls, model_id=model_id, **kwgs)

def __init__(self, *, model_id=None, **kwgs):
if self._model_id:
if self._async_widget_base_init_complete:
return
super().__init__(model_id=model_id, **kwgs)
assert self.model_id # noqa: S101
self._ipylab_model_register[self.model_id] = self
if self.SINGLETON:
self._singleton_register[self.__class__.__name__] = self.model_id
self.on_msg(self._on_frontend_msg)
self._async_widget_base_init_complete = True

async def __aenter__(self):
if not self._ready_response.is_set():
Expand All @@ -172,7 +174,7 @@ async def __aexit__(self, exc_type, exc, tb):
pass

def close(self):
self._ipylab_model_register.pop(self._model_id, None) # type: ignore
self._ipylab_model_register.pop(self.model_id, None) # type: ignore
for task in self._tasks:
task.cancel()
super().close()
Expand Down Expand Up @@ -228,20 +230,19 @@ async def _wait_response_check_error(self, response: Response, content: dict, ca

def _on_frontend_msg(self, _, content: dict, buffers: list):
error = self._check_get_error(content)
if error:
pm.hook.on_frontend_error(obj=self, error=error, content=content, buffers=buffers)
if operation := content.get("operation"):
ipylab_backend = content.get("ipylab_BE", "")
payload = content.get("payload", {})
if ipylab_backend:
self._pending_operations.pop(ipylab_backend).set(payload, error)
ipylab_frontend = content.get("ipylab_FE", "")
if ipylab_frontend:
if "ipylab_FE" in content:
task = asyncio.create_task(
self._handle_frontend_operation(ipylab_frontend, operation, payload, buffers)
self._handle_frontend_operation(content["ipylab_FE"], operation, payload, buffers)
)
self._tasks.add(task)
task.add_done_callback(self._tasks.discard)
if error:
pm.hook.on_frontend_error(obj=self, error=error, content=content, buffers=buffers)
elif "init" in content:
self._ready_response.set(content)

Expand Down
9 changes: 2 additions & 7 deletions ipylab/commands.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,14 +22,9 @@ class CommandPalette(AsyncWidgetBase):
_model_name = Unicode("CommandPaletteModel").tag(sync=True)
items = Tuple(read_only=True).tag(sync=True)

def add_item(self, command_id: str, category, *, rank=None, args: dict | None = None, **kwgs) -> asyncio.Task:
def add_item(self, command_id: str, category: str, *, rank=None, args: dict | None = None, **kwgs) -> asyncio.Task:
return self.schedule_operation(
operation="addItem",
id=command_id,
category=category,
rank=rank,
args=args,
**kwgs,
operation="addItem", id=command_id, category=category, rank=rank, args=args, **kwgs
)

def remove_item(self, command_id: str, category) -> asyncio.Task:
Expand Down
8 changes: 4 additions & 4 deletions ipylab/jupyterfrontend.py
Original file line number Diff line number Diff line change
Expand Up @@ -206,19 +206,19 @@ async def _execEval(self, payload: dict, buffers: list) -> Any:
# TODO: consider if globals / locals / async scope should be supported.
code = payload.get("code")
user_expressions = payload.get("user_expressions") or {}
glbls = payload | {"buffers": buffers}
locals_ = payload | {"buffers": buffers}
if code:
exec(code, glbls) # noqa: S102
exec(code, None, locals_) # noqa: S102
if user_expressions:
results = {}
for name, expression in user_expressions.items():
result = eval(expression, glbls) # noqa: S307
result = eval(expression, None, locals_) # noqa: S307
if callable(result):
result = result()
if inspect.isawaitable(result):
result = await result
results[name] = result
return
return results
return None

def startIyplabPythonBackend(self) -> asyncio.Task:
Expand Down
13 changes: 4 additions & 9 deletions ipylab/main_area.py
Original file line number Diff line number Diff line change
Expand Up @@ -74,7 +74,7 @@ def __new__(cls, *, name: str, model_id=None, content: Panel | None = None, **kw
return super().__new__(cls, name=name, **kwgs)

def __init__(self, *, name: str, path="", model_id=None, content: Panel | None = None, **kwgs):
if self._model_id:
if self._async_widget_base_init_complete:
return
path_ = str(pathlib.PurePosixPath(path or name)).lower().strip("/")
if path and path != path_:
Expand Down Expand Up @@ -136,19 +136,14 @@ def unload(self) -> asyncio.Task:
self.set_trait("status", ViewStatus.unloading)
return self.schedule_operation("unload")

def load_console(
self,
*,
name="Console",
mode: InsertMode = InsertMode.split_bottom,
**kwgs,
) -> asyncio.Task:
def load_console(self, *, mode: InsertMode = InsertMode.split_bottom, **kwgs) -> asyncio.Task:
"""Load a console using for the same kernel.
Opening the console will close any existing consoles.
"""
self.set_trait("console_status", ViewStatus.loading)
return self.schedule_operation("open_console", insertMode=InsertMode(mode), name=name, **kwgs)
kwgs = {"name": self.name, "path": self.path} | kwgs
return self.schedule_operation("open_console", insertMode=InsertMode(mode), **kwgs) # type: ignore

def unload_console(self) -> asyncio.Task:
"""Unload the console."""
Expand Down
1 change: 1 addition & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ dynamic = ["version", "description", "authors", "urls", "keywords"]
dependencies = [
"jupyterlab~=4.1",
"ipywidgets~=8.1",
"jupyterlab_widgets>=3.0.11",
"pluggy~=1.1",
"backports.strenum; python_version < '3.11'",
]
Expand Down
2 changes: 1 addition & 1 deletion src/plugin.ts
Original file line number Diff line number Diff line change
Expand Up @@ -72,7 +72,7 @@ async function activate(

registry.registerWidget(widgetExports.IpylabModel.exports);
}
widgetExports.IpylabModel.python_backend.checkStart();
widgetExports.IpylabModel.pythonBackend.checkStart();
}

export default extension;
11 changes: 5 additions & 6 deletions src/widgets/frontend.ts
Original file line number Diff line number Diff line change
Expand Up @@ -128,10 +128,7 @@ export class JupyterFrontEndModel extends IpylabModel {
payload.manager = IpylabModel.defaultBrowser.model.manager;
return await FileDialog.getExistingDirectory(payload).then(_get_result);
case 'newSession':
result = await newSession({
rendermime: IpylabModel.rendermime.clone(),
...payload
});
result = await newSession(payload);
return result.model as any;
case 'newNotebook':
result = await newNotebook(payload);
Expand All @@ -143,7 +140,7 @@ export class JupyterFrontEndModel extends IpylabModel {
jfem = await this.getJupyterFrontEndModel(payload);
return await jfem.scheduleOperation('execEval', payload);
case 'startIyplabPythonBackend':
return (await IpylabModel.python_backend.checkStart()) as any;
return (await IpylabModel.pythonBackend.checkStart()) as any;
case 'shutdownKernel':
if (payload.kernelId) {
await IpylabModel.app.commands.execute('kernelmenu:shutdown', {
Expand Down Expand Up @@ -209,7 +206,9 @@ export class JupyterFrontEndModel extends IpylabModel {
kernel = this.app.serviceManager.kernels.connectTo({ model: model });
} else {
if (payload.kernelId) {
throw new Error(`Kernel does not exist: ${payload.kernelId}`);
throw new Error(
`A kernel does not exist for the kernelId= '${payload.kernelId}'`
);
}
const session = await newSession(payload);
kernel = session.kernel;
Expand Down
42 changes: 20 additions & 22 deletions src/widgets/ipylab.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ import { JSONObject, JSONValue, UUID } from '@lumino/coreutils';
import { ObjectHash } from 'backbone';
import { MODULE_NAME, MODULE_VERSION } from '../version';
import { PythonBackendModel } from './python_backend';
import { PromiseDelegate } from '@lumino/coreutils';
import {
getNestedObject,
listAttributes,
Expand Down Expand Up @@ -49,7 +50,6 @@ export class IpylabModel extends DOMWidgetModel {
super.initialize(attributes, options);
this._kernelId = (this.widget_manager as any).kernel.id;
this.set('kernelId', this._kernelId);
this._pending_backend_operation_callbacks = new Map();
this.on('msg:custom', this._onCustomMessage.bind(this));
this.save_changes();
const msg = `ipylab ${this.get('_model_name')} ready for operations`;
Expand All @@ -73,25 +73,23 @@ export class IpylabModel extends DOMWidgetModel {
/**
* Convert custom messages into operations for action.
* There are two types:
* 1. Response to requested operation sent to Python backend.
* 1. Response to requested operation sent to Python backend (ipylab_FE).
* 2. Operation requests received from the Python backend (ipylab_BE).
* @param msg
*/
private async _onCustomMessage(msg: any): Promise<void> {
const ipylab_FE: string = msg.ipylab_FE;
if (ipylab_FE) {
private _onCustomMessage(msg: any) {
if (msg.ipylab_FE) {
// Frontend operation result
delete (msg as any).ipylab_FE;
const [resolve, reject] =
this._pending_backend_operation_callbacks.get(ipylab_FE);
this._pending_backend_operation_callbacks.delete(ipylab_FE);
if (msg.error) {
reject(msg.error);
} else {
resolve(msg.payload);
const opDone = this._pendingBackendOperations.get(msg.ipylab_FE);
this._pendingBackendOperations.delete(msg.ipylab_FE);
if (opDone) {
if (msg.error) {
opDone.reject(msg.error);
} else {
opDone.resolve(msg.payload);
}
}
} else {
// Backend operation (don't await it)
} else if (msg.ipylab_BE) {
this._do_operation_for_backend(msg);
}
}
Expand Down Expand Up @@ -226,12 +224,10 @@ export class IpylabModel extends DOMWidgetModel {
};
// Create callbacks to be resolved when a custom message is received
// with the key `ipylab_FE`.
const callbacks = this._pending_backend_operation_callbacks;
const promise = new Promise<JSONValue>((resolve, reject) => {
callbacks.set(ipylab_FE, [resolve, reject]);
});
const opDone = new PromiseDelegate();
this._pendingBackendOperations.set(ipylab_FE, opDone);
this.send(msg);
const result: any = await promise;
const result: any = await opDone.promise;
return await transformObject(result, transform ?? 'raw', this);
}

Expand All @@ -248,6 +244,8 @@ export class IpylabModel extends DOMWidgetModel {
}

close(comm_closed?: boolean): Promise<void> {
this._pendingBackendOperations.forEach(opDone => opDone.reject('Closed'));
this._pendingBackendOperations.clear();
comm_closed = comm_closed || !this.kernelLive;
return super.close(comm_closed);
}
Expand All @@ -262,9 +260,9 @@ export class IpylabModel extends DOMWidgetModel {
...WidgetModel.serializers
};

private _pending_backend_operation_callbacks: Map<string, [any, any]>;
private _pendingBackendOperations = new Map<string, PromiseDelegate<any>>();
private _kernelId: string;
static python_backend = new PythonBackendModel();
static pythonBackend = new PythonBackendModel();
static model_name: string;
static model_module = MODULE_NAME;
static model_module_version = MODULE_VERSION;
Expand Down
3 changes: 1 addition & 2 deletions src/widgets/python_backend.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,6 @@ export class PythonBackendModel {
this._backendSession = await newSession({
path: 'Ipylab backend',
name: 'Ipylab backend',
rendermime: IpylabModel.rendermime.clone(),
language: 'python3',
code: 'import ipylab.scripts; ipylab.scripts.init_ipylab_backend()'
});
Expand All @@ -29,7 +28,7 @@ export class PythonBackendModel {
'[project.entry-points.ipylab-python-backend] \n' +
'\tmyproject = "myproject.pluginmodule"',

execute: () => IpylabModel.python_backend.checkStart()
execute: () => IpylabModel.pythonBackend.checkStart()
}
);
if (this._palletItem) {
Expand Down
Loading

0 comments on commit b2503f6

Please sign in to comment.