diff --git a/ipylab/__init__.py b/ipylab/__init__.py index 8ef5069..347bd00 100644 --- a/ipylab/__init__.py +++ b/ipylab/__init__.py @@ -11,8 +11,6 @@ "DisposableConnection", "Panel", "SplitPanel", - "MainArea", - "ViewStatus", "Icon", "Area", "InsertMode", @@ -27,7 +25,6 @@ from ipylab.hasapp import HasApp from ipylab.hookspecs import hookimpl from ipylab.jupyterfrontend import JupyterFrontEnd -from ipylab.main_area import MainArea, ViewStatus from ipylab.shell import Area, InsertMode from ipylab.widgets import Icon, Panel, SplitPanel diff --git a/ipylab/main_area.py b/ipylab/main_area.py deleted file mode 100644 index 979dd70..0000000 --- a/ipylab/main_area.py +++ /dev/null @@ -1,147 +0,0 @@ -# Copyright (c) ipylab contributors. -# Distributed under the terms of the Modified BSD License. - -from __future__ import annotations - -import pathlib -import sys -from typing import ClassVar - -from ipywidgets import register -from traitlets import Instance, TraitType, Unicode, UseEnum, observe, validate - -from ipylab.asyncwidget import AsyncWidgetBase, widget_serialization -from ipylab.shell import Area, InsertMode -from ipylab.widgets import Panel - -if sys.version_info >= (3, 11): - from enum import StrEnum -else: - from backports.strenum import StrEnum - - -class ViewStatus(StrEnum): - unloaded = "unloaded" - loaded = "loaded" - loading = "loading" - unloading = "unloading" - - -@register -class MainArea(AsyncWidgetBase): - """A MainAreaWidget that can be loaded / unloaded with a single 'view'. - - Also provides methods to open/close a console using the context of the loaded widget. - """ - - _model_name = Unicode("MainAreaModel").tag(sync=True) - _main_area_names: ClassVar[dict[str, MainArea]] = {} - - path = Unicode(read_only=True).tag(sync=True) - name = Unicode(read_only=True).tag(sync=True) - content = Instance(Panel, (), read_only=True).tag(sync=True, **widget_serialization) - status: TraitType[ViewStatus, ViewStatus] = UseEnum(ViewStatus, read_only=True).tag(sync=True) - console_status: TraitType[ViewStatus, ViewStatus] = UseEnum(ViewStatus, read_only=True).tag(sync=True) - - @validate("name", "path") - def _validate_name_path(self, proposal): - trait = proposal["trait"].name - if getattr(self, trait): - msg = f"Changing the value of {trait=} is not allowed!" - raise RuntimeError(msg) - value = proposal["value"] - if value != value.strip(): - msg = f"Leading/trailing whitespace is not allowed for {trait}: '{value}'" - raise ValueError(msg) - return value - - @observe("comm") - def _observe_comm(self, _): - if not self.comm: - self.set_trait("status", ViewStatus.unloaded) - self.set_trait("console_status", ViewStatus.unloaded) - - def __new__(cls, *, name: str, model_id=None, content: Panel | None = None, **kwgs): # noqa: ARG003 - if not name: - msg = "name not supplied" - raise (ValueError(msg)) - if name in cls._main_area_names: - return cls._main_area_names[name] - return super().__new__(cls, name=name, **kwgs) - - def __init__(self, *, name: str, path="", model_id=None, content: Panel | None = None, **kwgs): - if self._async_widget_base_init_complete: - return - path_ = str(pathlib.PurePosixPath(path or name)).lower().strip("/") - if path and path != path_: - msg = f"`path` must be lowercase and not start/finish with '/' but got '{path}'" - raise ValueError(msg) - self.set_trait("name", name) - self.set_trait("path", path_) - if content: - self.set_trait("content", content) - super().__init__(model_id=model_id, **kwgs) - - def close(self): - self._main_area_names.pop(self.name, None) - super().close() - - def load( - self, - *, - content: Panel | None = None, - area: Area = Area.main, - activate: bool = True, - mode: InsertMode = InsertMode.tab_after, - rank: int | None = None, - ref: str = "", - class_name="ipylab-main-area", - ): - """Load into the shell. - - Only one main_area_widget (view) can exist at a time, any existing widget will be disposed - prior to loading a new widget. - - When this function is call the trait `status` will be set to 'loading'. It will change to 'loaded' once - the widget has been loaded in the Frontend. - - Use `unload` to dispose the widget from the shell (will also close the linked console if it is open). - - content: [Panel] - The content - ref: - The id of the widget to insert relative to in the shell. (default is app.current_widget_id). - name: - The session name to use. - class_name: - The css class to add to the widget. - """ - if content: - self.set_trait("content", content) - self.set_trait("status", ViewStatus.loading) - options = { - "mode": InsertMode(mode), - "rank": rank, - "activate": activate, - "ref": ref or self.app.current_widget_id, - } - return self.schedule_operation("load", area=area, options=options, className=class_name) - - def unload(self): - "Remove from the shell" - self.set_trait("status", ViewStatus.unloading) - return self.schedule_operation("unload") - - def load_console(self, *, mode: InsertMode = InsertMode.split_bottom, **kwgs): - """Load a console using for the same kernel. - - Opening the console will close any existing consoles. - """ - self.set_trait("console_status", ViewStatus.loading) - kwgs = {"name": self.name, "path": self.path} | kwgs - return self.schedule_operation("open_console", insertMode=InsertMode(mode), **kwgs) # type: ignore - - def unload_console(self): - """Unload the console.""" - self.set_trait("console_status", ViewStatus.unloading) - return self.schedule_operation("close_console") diff --git a/package.json b/package.json index 19cfbf7..0f11625 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "ipylab", - "version": "2.0.0-b0", + "version": "2.0.0-b1", "description": "Control JupyterLab from Python notebooks", "keywords": [ "jupyter", diff --git a/src/plugin.ts b/src/plugin.ts index c6d4f11..0e749d2 100644 --- a/src/plugin.ts +++ b/src/plugin.ts @@ -13,17 +13,15 @@ import { ILauncher } from '@jupyterlab/launcher'; import { IRenderMimeRegistry } from '@jupyterlab/rendermime'; import { ITranslator } from '@jupyterlab/translation'; import { MODULE_NAME, MODULE_VERSION } from './version'; -import { IMainAreaWidgetTracker } from './widgets/main_area'; const EXTENSION_ID = 'ipylab:plugin'; /** * The default plugin. */ -const extension: JupyterFrontEndPlugin = { +const extension: JupyterFrontEndPlugin = { id: EXTENSION_ID, autoStart: true, - provides: IMainAreaWidgetTracker, requires: [IJupyterWidgetRegistry, IRenderMimeRegistry], optional: [ ICommandPalette, @@ -55,7 +53,7 @@ async function activate( defaultBrowser: IDefaultFileBrowser | null, launcher: ILauncher | null, translator: ITranslator | null -): Promise { +): Promise { const widgetExports = await import('./widget'); if (!widgetExports.JupyterFrontEndModel.app) { // add globals @@ -75,7 +73,6 @@ async function activate( registry.registerWidget(widgetExports.IpylabModel.exports); } widgetExports.IpylabModel.pythonBackend.checkStart(); - return widgetExports.MainAreaModel.tracker; } export default extension; diff --git a/src/widget.ts b/src/widget.ts index 8a0210a..7757dbb 100644 --- a/src/widget.ts +++ b/src/widget.ts @@ -2,11 +2,10 @@ // Distributed under the terms of the Modified BSD License. import { CommandRegistryModel } from './widgets/commands'; +import { DisposableConnectionModel } from './widgets/disposable_connection'; import { JupyterFrontEndModel } from './widgets/frontend'; import { IconModel, IconView } from './widgets/icon'; import { IpylabModel } from './widgets/ipylab'; -import { DisposableConnectionModel } from './widgets/disposable_connection'; -import { MainAreaModel } from './widgets/main_area'; import { CommandPaletteModel, LauncherModel } from './widgets/palette'; import { PanelModel, PanelView } from './widgets/panel'; import { SplitPanelModel, SplitPanelView } from './widgets/split_panel'; @@ -15,13 +14,12 @@ import { TitleModel } from './widgets/title'; export { CommandPaletteModel, CommandRegistryModel, + DisposableConnectionModel, IconModel, IconView, IpylabModel, JupyterFrontEndModel, LauncherModel, - DisposableConnectionModel, - MainAreaModel, PanelModel, PanelView, SplitPanelModel, diff --git a/src/widgets/main_area.ts b/src/widgets/main_area.ts deleted file mode 100644 index 531905b..0000000 --- a/src/widgets/main_area.ts +++ /dev/null @@ -1,304 +0,0 @@ -// Copyright (c) Jupyter Development Team. -// Distributed under the terms of the Modified BSD License. - -import { IBackboneModelOptions, unpack_models } from '@jupyter-widgets/base'; -import { - ISessionContext, - IWidgetTracker, - MainAreaWidget, - SessionContext, - WidgetTracker -} from '@jupyterlab/apputils'; -import { ConsolePanel } from '@jupyterlab/console'; -import { Token } from '@lumino/coreutils'; -import { Message } from '@lumino/messaging'; -import { SplitPanel, Widget } from '@lumino/widgets'; -import { ObjectHash } from 'backbone'; -import { IDisposable, IpylabModel, JSONValue } from './ipylab'; -/** - * A main area widget with a sessionContext and the ability to add other children. - */ -export class IpylabMainAreaWidget extends MainAreaWidget { - /** - * Construct a MainAreaWidget with a context. - * closing in the shell - */ - constructor(options: IpylabMainAreaWidget.IOptions) { - //TODO: support more parts of the MainAreaWidget - - const { content, kernelId, name, type, className, path } = options; - super({ content: content }); - this._sessionContext = new SessionContext({ - sessionManager: IpylabModel.app.serviceManager.sessions, - specsManager: IpylabModel.app.serviceManager.kernelspecs, - path: path, - name: name, - type: type, - kernelPreference: { - id: kernelId, - language: 'python3' - } - }); - this._sessionContext.initialize(); - this.addClass(className ?? 'ipylab-main-area'); - SplitPanel.setStretch(this.content, 1); - this.node.removeChild(this.toolbar.node); // Temp until toolbar is supported - MainAreaModel.tracker.add(this); - } - - /** - * The session used by the main area. - */ - get sessionContext(): ISessionContext { - return this._sessionContext; - } - - /** - * Dispose the widget. - * - */ - dispose(): void { - if (this.isDisposed) { - return; - } - this.sessionContext.dispose(); - super.dispose(); - } - - /** - * Handle `'close-request'` messages. - */ - protected onCloseRequest(msg: Message): void { - super.onCloseRequest(msg); - this.dispose(); - } - - private _sessionContext: ISessionContext; -} - -/** - * The model for controlling the content of a MainArea. - * This model can: - * - add/remove itself from the shell. - * - open/close a console with a maximum of one console open. - * - */ -export class MainAreaModel extends IpylabModel { - async initialize( - attributes: ObjectHash, - options: IBackboneModelOptions - ): Promise { - super.initialize(attributes, options); - this._mutex_key = `main_area ${this.model_id}`; - this._sessionContext = new SessionContext({ - sessionManager: IpylabModel.app.serviceManager.sessions, - specsManager: IpylabModel.app.serviceManager.kernelspecs, - path: this.get('path'), - name: this.get('name'), - type: 'ipylab main area', - kernelPreference: { id: this.kernelId, language: 'python3' } - }); - await this.sessionContext.initialize(); - } - - async operation(op: string, payload: any): Promise { - switch (op) { - case 'load': - // Using lock for mutex - return await navigator.locks.request(this._mutex_key, () => - this._load_main_area_widget(payload) - ); - case 'unload': - return await navigator.locks.request(this._mutex_key, () => - this._unload_mainarea_widget() - ); - case 'open_console': - return await navigator.locks.request(this._mutex_key, () => - this._open_console(payload) - ); - case 'close_console': - return await navigator.locks.request(this._mutex_key, () => - this._close_console() - ); - default: - throw new Error( - `operation='${op}' has not been implemented in ${MainAreaModel.model_name}!` - ); - } - } - - close(comm_closed?: boolean): Promise { - this._unload_mainarea_widget(); - return super.close(comm_closed); - } - - async _load_main_area_widget(payload: any) { - this._unload_mainarea_widget(); - const { area, options, className } = payload; - const content = this.get('content'); - const view = await this.widget_manager.create_view(content, {}); - const luminoWidget = new IpylabMainAreaWidget({ - content: view.luminoWidget, - kernelId: this.kernelId, - name: this.sessionContext.name, - path: this.sessionContext.path, - type: this.sessionContext.type, - className: className - }); - this._unload_mainarea_widget(); // unload any existing widgets. - luminoWidget.disposed.connect(() => { - this.set('status', 'unloaded'); - this.save_changes(); - this._luminoWidget = null; - this._close_console(); - }, this); - IpylabModel.app.shell.add(luminoWidget, area, options); - await luminoWidget.sessionContext.ready; - this._luminoWidget = luminoWidget; - this.set('status', 'loaded'); - this.save_changes(); - return { id: this._luminoWidget.id }; - } - - _unload_mainarea_widget() { - if (this._luminoWidget) { - this._luminoWidget.dispose(); - } - this._close_console(); - } - async _open_console(options: any) { - // https://jupyterlab.readthedocs.io/en/stable/api/interfaces/console.ConsolePanel.IOptions.html - this._close_console(); - const cp: ConsolePanel = await IpylabModel.app.commands.execute( - 'console:create', - { - basePath: this.sessionContext.path, - // type: 'Linked Console', - ref: this._luminoWidget?.id, - kernelPreference: { id: this.kernelId, language: 'python3' }, - ...options - } - ); - // The console toolbar takes up space and currently only provides a debugger - if (cp?.toolbar?.node) { - cp.node.removeChild(cp.toolbar.node); - } - await cp.sessionContext.ready; - cp.disposed.connect(() => { - if (this._consolePanel === cp) { - this._consolePanel = null; - this.set('console_status', 'unloaded'); - this.save_changes(); - } - }, this); - this._consolePanel = cp; - this.set('console_status', 'loaded'); - this.save_changes(); - return { id: cp.id }; - } - - _close_console(): Promise { - if (this._consolePanel) { - this._consolePanel.dispose(); - } - return null; - } - - /** - * The session used by the main area. - */ - get sessionContext(): ISessionContext { - return this._sessionContext; - } - - /** - * The default attributes. - */ - defaults(): any { - return { - ...super.defaults(), - _model_name: MainAreaModel.model_name, - _model_module: IpylabModel.model_module, - _model_module_version: IpylabModel.model_module_version, - _view_name: MainAreaModel.view_name, - _view_module: IpylabModel.model_module, - _view_module_version: IpylabModel.model_module_version - }; - } - private _mutex_key: string; - private _luminoWidget: IpylabMainAreaWidget; - private _consolePanel: ConsolePanel; - private _sessionContext: ISessionContext; - - static tracker = new WidgetTracker({ - namespace: 'console' - }); - static model_name = 'MainAreaModel'; - static view_name = 'MainAreaView'; - class_name = 'ipylab-main_area'; - static serializers = { - ...IpylabModel.serializers, - content: { deserialize: unpack_models } - }; -} - -/** - * A namespace for IpylabMainAreaWidget statics. - */ -export namespace IpylabMainAreaWidget { - /** - * The initialization options for a main area panel. - */ - export interface IOptions { - /** - * The widget to use in the main area. - */ - content: Widget; - - /** - * The id of the python kernel. - */ - kernelId: string; - - /** - * The path of an existing session. - */ - path?: string; - - /** - * The base path for a new sessions. - */ - basePath?: string; - - /** - * The name of the IpylabMainAreaWidget. - */ - name: string; - - /** - * The name of class. - */ - className?: string; - - /** - * The type of session. - */ - type?: string; - } -} - -/** - * The MainAreaWidget tracker token. - */ -export const IMainAreaWidgetTracker = new Token( - 'ipylab:mainAreaTracker', - `A widget tracker for MainAreaWidget instances. - Use this if you want to be able to iterate over and interact with MainAreaWidgets - created by the application.` -); - -/** - * A class that tracks MainAreaWidget widgets. - */ -export interface IMainAreaWidgetTracker - extends IWidgetTracker {}