From 7cc0d86ae43d3016b492407197f3b46748f22d7c Mon Sep 17 00:00:00 2001 From: Alan Fleming <> Date: Wed, 30 Oct 2024 13:50:23 +1100 Subject: [PATCH] Add config for autostart. --- .copier-answers.yml | 1 - examples/plugins.ipynb | 53 +++++++++++++++++++++++++++++---------- ipylab/hookspecs.py | 15 +++-------- ipylab/jupyterfrontend.py | 4 +-- package.json | 5 +++- schema/settings.json | 14 +++++++++++ src/plugin.ts | 19 ++++++++++---- yarn.lock | 1 + 8 files changed, 77 insertions(+), 35 deletions(-) create mode 100644 schema/settings.json diff --git a/.copier-answers.yml b/.copier-answers.yml index c4b18f1f..2d0ca61b 100644 --- a/.copier-answers.yml +++ b/.copier-answers.yml @@ -11,4 +11,3 @@ project_short_description: Control JupyterLab from Python notebooks python_name: ipylab repository: https://github.com/jtpio/ipylab test: false - diff --git a/examples/plugins.ipynb b/examples/plugins.ipynb index f6190c77..e3dfd139 100644 --- a/examples/plugins.ipynb +++ b/examples/plugins.ipynb @@ -59,7 +59,7 @@ "source": [ "## Autostart\n", "\n", - "Ipylab automatically starts a Python kernel with the path 'ipylab' when the ipylab plugin is activated. The `autostart` hookspec is called as soon as comms has been established. Autostart is performed prior to restoring Jupyterlab such as when refreshing the page or starting from scratch.\n", + "The `autostart` hookspec is called as when the App is ready.\n", "\n", "Possible uses include:\n", "* Create and register custom commands;\n", @@ -193,17 +193,12 @@ " )\n", " await cmd.add_launcher(\"Ipylab\", extra=\"Ipylab extra arguments\")\n", "\n", + " @ipylab.hookimpl\n", + " def vpath_getter(self, app: ipylab.App, kwgs: dict): # noqa: ARG002\n", + " return app.vpath\n", "\n", - "pluginmodule = MyPlugins()" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "Instead of defining entry points and installing a module. Let's roughly simulate what the entry point does.\n", "\n", - "Note that entry points will run 'autostart' for every kernel. The property `app.is_ipylab_kernel` can be used to determine if the code is running in the ipylab kernel." + "pluginmodule = MyPlugins()" ] }, { @@ -238,7 +233,7 @@ "source": [ "## Entry points\n", "\n", - "Entry points provide for automatic registration for when ipylab is loaded.\n", + "Entry points provide for automatic registration for when ipylab is imported.\n", "\n", "Add the following in your `pyproject.toml`\n", "\n", @@ -260,8 +255,40 @@ "\n", "plugin1 = MyPlugins()\n", "plugin2 = MyOtherPlugins()\n", - "```" + "```\n", + "\n", + "\n", + "### Ipylab kernel (autostart)\n", + "\n", + "**Starting kernels with widgets enabled requires a widget manager capable of running independent of a notebook or console.**\n", + "\n", + "Currently this isn't supported by IpyWidgets. There is an outstanding PR [here](https://github.com/jupyter-widgets/ipywidgets/pull/3922) which enables this. \n", + "\n", + "#### Command\n", + "\n", + "The ipylab kernel can be started/re-started with the command 'Start ipylab kernel' (`Ctrl c` -> 'Start ipylab kernel').\n", + "\n", + "#### Configure\n", + "\n", + "An *autostart* ipylab kernel can be enabled in the configuration settings which will automatically start when Jupyterlab is started (intended to run locally). This provides a means for entry points to be loaded when Jupyterlab is launched.\n", + "\n", + "To change the configuration use the Jupyterlab settings editor: \n", + "\n", + "`Ctrl + ,` \n", + "\n", + "`Ipylab` \n", + "\n", + "`ipylab kernel enabled = True`.\n", + "\n", + "The next time Jupyterlab is started, a kerenel on the path 'ipylab' will be started." ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [] } ], "metadata": { @@ -280,7 +307,7 @@ "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", - "version": "3.12.7" + "version": "3.10.15" } }, "nbformat": 4, diff --git a/ipylab/hookspecs.py b/ipylab/hookspecs.py index 6779561f..5d48c1d3 100644 --- a/ipylab/hookspecs.py +++ b/ipylab/hookspecs.py @@ -35,22 +35,13 @@ def ready(obj: ipylab.Ipylab) -> None | Awaitable[None]: @hookspec(historic=True) async def autostart(app: ipylab.App) -> None | Awaitable[None]: """ - Called inside each Python kernel when the frontend is 'ready'. - - Use this with modules that define entry points. - - To run in the Ipylab kernel exclusively use. - - ``` python - if not app.is_ipylab_kernel: - return - ``` + Called inside each Python kernel when the App is 'ready'. Historic -------- - This plugin is historic so will activate when a plugin is registered after - the app is ready. + This plugin is historic so will activate when a plugin is registered if the + App is already ready. """ diff --git a/ipylab/jupyterfrontend.py b/ipylab/jupyterfrontend.py index 3ec936d8..a18faf57 100644 --- a/ipylab/jupyterfrontend.py +++ b/ipylab/jupyterfrontend.py @@ -11,7 +11,7 @@ import ipywidgets from IPython.core.getipython import get_ipython from ipywidgets import TypedTuple, Widget, register -from traitlets import Bool, Container, Dict, Instance, Tuple, Unicode, UseEnum, observe +from traitlets import Container, Dict, Instance, Tuple, Unicode, UseEnum, observe import ipylab import ipylab.hookspecs @@ -70,7 +70,6 @@ class App(Ipylab): notification = Instance(NotificationManager, (), read_only=True) console = Instance(ShellConnection, (), allow_none=True, read_only=True) - is_ipylab_kernel = Bool() active_namespace = Unicode("", read_only=True, help="name of the current namespace") namespace_names: Container[tuple[str, ...]] = Tuple(read_only=True).tag(sync=True) @@ -89,7 +88,6 @@ def __init__(self, **kwgs): return if vpath := kwgs.pop("vpath", None): self.set_trait("vpath", vpath) - self.set_trait("is_ipylab_kernel", vpath == "ipylab") super().__init__(**kwgs) def close(self): diff --git a/package.json b/package.json index e4b7f3f7..eef07ac4 100644 --- a/package.json +++ b/package.json @@ -13,7 +13,8 @@ "dist/*.js", "style/*.css", "style/*.js", - "style/index.js" + "style/index.js", + "schema/*.json" ], "homepage": "https://github.com/jtpio/ipylab", "bugs": { @@ -95,6 +96,7 @@ "@jupyterlab/mainmenu": "^4.2.5", "@jupyterlab/observables": "^5.2.5", "@jupyterlab/rendermime": "^4.2.5", + "@jupyterlab/settingregistry": "^4.2.5", "@lumino/commands": "^2.3.1", "@lumino/disposable": "^2.1.3", "@lumino/widgets": "^2.5.0", @@ -134,6 +136,7 @@ "jupyterlab": { "extension": "lib/plugin", "outputDir": "ipylab/labextension", + "schemaDir": "schema", "sharedPackages": { "@jupyter-widgets/base": { "bundled": false, diff --git a/schema/settings.json b/schema/settings.json new file mode 100644 index 00000000..5ae5997b --- /dev/null +++ b/schema/settings.json @@ -0,0 +1,14 @@ +{ + "title": "Ipylab", + "description": "Settings for ipylab.", + "properties": { + "autostart": { + "type": "boolean", + "title": "Ipylab kernel enabled", + "description": "Automatically start the ipylab kernel when Jupyterlab is started. This enables plugins to use the autostart feature. Note: This feature requires a special version of Jupyterlab widgets https://github.com/jupyter-widgets/ipywidgets/pull/3922", + "default": false + } + }, + "additionalProperties": false, + "type": "object" +} diff --git a/src/plugin.ts b/src/plugin.ts index 4df9ffe0..a64413f1 100644 --- a/src/plugin.ts +++ b/src/plugin.ts @@ -14,11 +14,13 @@ import { ILauncher } from '@jupyterlab/launcher'; import { ILoggerRegistry } from '@jupyterlab/logconsole'; import { IMainMenu } from '@jupyterlab/mainmenu'; import { IRenderMimeRegistry } from '@jupyterlab/rendermime'; +import { ISettingRegistry } from '@jupyterlab/settingregistry'; import { ITranslator } from '@jupyterlab/translation'; import { MODULE_NAME, MODULE_VERSION } from './version'; import { IpylabModel, JupyterFrontEndModel, ShellModel } from './widget'; import { IpylabAutostart } from './widgets/autostart'; -const EXTENSION_ID = 'ipylab:plugin'; + +const PLUGIN_ID = 'ipylab:settings'; /** * The command IDs used by the plugin. @@ -34,9 +36,9 @@ namespace CommandIDs { * The default plugin. */ const extension: JupyterFrontEndPlugin = { - id: EXTENSION_ID, + id: PLUGIN_ID, autoStart: true, - requires: [IJupyterWidgetRegistry, IRenderMimeRegistry], + requires: [IJupyterWidgetRegistry, IRenderMimeRegistry, ISettingRegistry], optional: [ ILayoutRestorer, ICommandPalette, @@ -65,6 +67,7 @@ async function activate( app: JupyterFrontEnd, registry: IJupyterWidgetRegistry, rendermime: IRenderMimeRegistry, + settings: ISettingRegistry, restorer: ILayoutRestorer, palette: ICommandPalette, labShell: ILabShell | null, @@ -95,8 +98,8 @@ async function activate( registry.registerWidget(widgetExports.IpylabModel.exports); } app.commands.addCommand(CommandIDs.checkStartKernel, { - label: 'Ipylab restart default kernel', - caption: 'Start or restart the default Ipylab Kernel.', + label: 'Start ipylab kernel', + caption: 'Start or restart the python kernel with path="ipylab".', execute: () => IpylabAutostart.checkStart(true) }); app.commands.addCommand(CommandIDs.restore, { @@ -125,6 +128,12 @@ async function activate( category: 'ipylab', rank: 50 }); + await settings.load(PLUGIN_ID); + const autostart = (await settings.get(PLUGIN_ID, 'autostart')) + .composite as boolean; + if (autostart) { + app.commands.execute(CommandIDs.checkStartKernel); + } // Handle state restoration. if (restorer) { diff --git a/yarn.lock b/yarn.lock index 4a925e59..24d8183d 100644 --- a/yarn.lock +++ b/yarn.lock @@ -4185,6 +4185,7 @@ __metadata: "@jupyterlab/mainmenu": ^4.2.5 "@jupyterlab/observables": ^5.2.5 "@jupyterlab/rendermime": ^4.2.5 + "@jupyterlab/settingregistry": ^4.2.5 "@lumino/commands": ^2.3.1 "@lumino/disposable": ^2.1.3 "@lumino/widgets": ^2.5.0