diff --git a/examples/autostart.ipynb b/examples/autostart.ipynb
index 096e8669..0baaff45 100644
--- a/examples/autostart.ipynb
+++ b/examples/autostart.ipynb
@@ -15,13 +15,12 @@
"source": [
"# Autostart\n",
"\n",
- "Autostart is a feature implemented using the [`pluggy`](https://pluggy.readthedocs.io/en/stable/index.html#pluggy) plugin system. Plugins registered under the entry point `ipylab-python-backend` will be called once when `ipylab` is activated. Normally activation of `ipylab` will occur when Jupyterlab is started (assuming `ipylab` is installed and enabled). \n",
+ "Autostart is a feature implemented using the [`pluggy`](https://pluggy.readthedocs.io/en/stable/index.html#pluggy) plugin system. The code associated with the entry point `ipylab-python-backend` will be called (imported) when `ipylab` is activated. `ipylab` will activate when Jupyterlab is started (provided `ipylab` is installed and enabled). \n",
"\n",
- "There are no limitations to what can be done. But some possibilities include:\n",
- "* Launch an app to run in its own thread;\n",
+ "There are no limitations to what can be done. But it is recommended to import on demand to minimise the time required to launch. Some possibilities include:\n",
"* Create and register custom commands;\n",
"* Create launchers;\n",
- "* Create new notebooks;\n",
+ "* Modify the appearance of Jupyterlab.\n",
"\n",
"## Entry points\n",
"\n",
@@ -32,27 +31,24 @@
"my-plugins-name = \"my_module.ipylab_plugin:ipylab_plugin\"\n",
"```\n",
"\n",
- "In `my_module.autostart.py` define the plugins.\n",
+ "In `my_module.autostart.py` write code that will be run once.\n",
"\n",
"Example:\n",
"\n",
"```python\n",
"# @ipylab_plugin.py\n",
"\n",
- "import ipylab\n",
- "\n",
- "app = ipylab.JupyterFrontEnd()\n",
+ "import asyncio\n",
"\n",
- "def create_app():\n",
- " Add code here to create the app\n",
+ "async def startup():\n",
+ " import ipylab\n",
+ " \n",
+ " app = ipylab.JupyterFrontEnd() \n",
+ " await app.read_wait()\n",
+ " #Do everything to startup\n",
"\n",
- "class IpylabPlugins:\n",
- " @ipylab.hookspecs.hookimpl()\n",
- " def run_once_at_startup(self):\n",
- " # May want to use a launcher instead\n",
- " app.newSession(path=\"my app\", code=create_app)\n",
- " # Do more stuff ...\n",
- "ipylab_plugin = IpylabPlugins()\n",
+ "ipylab_plugin = None # Provide an empty object with the expected name.\n",
+ "asyncio.create_task(startup())\n",
"```"
]
},
@@ -60,7 +56,7 @@
"cell_type": "markdown",
"metadata": {},
"source": [
- "## Example launching a small app"
+ "## Example creating a launcher"
]
},
{
@@ -71,50 +67,78 @@
"source": [
"# @my_module.autostart.py\n",
"\n",
+ "import asyncio\n",
+ "\n",
"import ipylab\n",
"\n",
"app = ipylab.JupyterFrontEnd()\n",
"\n",
+ "n = 0\n",
+ "\n",
"\n",
"async def create_app():\n",
- " # Ensure this function provides all the imports.\n",
+ " # The code in this function is called in the new kernel (session).\n",
+ " # Ensure imports are performed inside the function.\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",
+ " ma.content.title.label = \"Simple app\"\n",
+ " ma.content.title.caption = ma.kernelId\n",
+ " await ma.load()\n",
" console_button = ipw.Button(description=\"Toggle console\")\n",
+ " error_button = ipw.Button(\n",
+ " description=\"Do an error\",\n",
+ " tooltip=\"An error dialog will pop up when this is clicked.\\n\"\n",
+ " \"The dialog demonstrates the use of the `on_frontend_error` plugin.\",\n",
+ " )\n",
" console_button.on_click(\n",
" lambda b: ma.load_console() if ma.console_status == \"unloaded\" else ma.unload_console()\n",
" )\n",
+ " error_button.on_click(lambda b: ma.execute_command(\"Not a command\"))\n",
" ma.content.children = [\n",
" ipw.HTML(f\"
My simple app
Welcome to my app. kernel id: {ma.kernelId}\"),\n",
- " console_button,\n",
+ " ipw.HBox([console_button, error_button]),\n",
" ]\n",
- " ma.content.label = \"This is my app\"\n",
- " ma.load()\n",
- " print(\"Finished creating my app\")\n",
"\n",
+ " # Shutdown when MainArea is unloaded.\n",
+ " def on_status_change(change):\n",
+ " if change[\"new\"] == \"unloaded\":\n",
+ " ma.app.shutdownKernel()\n",
"\n",
- "class MyPlugins:\n",
- " @ipylab.hookspecs.hookimpl()\n",
- " def run_once_at_startup(self):\n",
- " app.newSession(path=\"my app\", code=create_app)\n",
+ " ma.observe(on_status_change, \"status\")\n",
"\n",
+ " class IpylabPlugins:\n",
+ " # Define plugins (see IpylabHookspec for available hooks)\n",
+ " @ipylab.hookimpl\n",
+ " def on_frontend_error(self, obj, error, content):\n",
+ " ma.app.dialog.show_error_message(\"Error\", str(error))\n",
"\n",
- "ipylab_plugin = MyPlugins()"
- ]
- },
- {
- "cell_type": "markdown",
- "metadata": {},
- "source": [
- "### Launch the app manually\n",
+ " # Register plugin for this kernel.\n",
+ " ipylab.hookspecs.pm.register(IpylabPlugins())\n",
"\n",
- "We can 'launch' the app in a new kernel."
+ "\n",
+ "def start_my_app(cwd):\n",
+ " global n\n",
+ " n += 1\n",
+ " app.newSession(path=f\"my app {n}\", code=create_app)\n",
+ "\n",
+ "\n",
+ "async def register_commands():\n",
+ " await app.command.addPythonCommand(\n",
+ " \"start_my_app\",\n",
+ " execute=start_my_app,\n",
+ " label=\"Start Custom App\",\n",
+ " icon_class=\"jp-PythonIcon\",\n",
+ " )\n",
+ " await app.launcher.add_item(\"start_my_app\", \"Ipylab\")\n",
+ " return \"done\"\n",
+ "\n",
+ "\n",
+ "ipylab_plugin = None\n",
+ "t = asyncio.create_task(register_commands())"
]
},
{
@@ -123,15 +147,7 @@
"metadata": {},
"outputs": [],
"source": [
- "app.newSession(path=\"my app\", code=create_app)"
- ]
- },
- {
- "cell_type": "markdown",
- "metadata": {},
- "source": [
- "### Auto Launch app\n",
- "Simulate code launch in the as it happens in `Ipylab backend`"
+ "t.result()"
]
},
{
@@ -140,11 +156,8 @@
"metadata": {},
"outputs": [],
"source": [
- "# Register plugin (normally via the entry point `ipylab-python-backend`)\n",
- "ipylab.hookspecs.pm.register(ipylab_plugin)\n",
- "\n",
- "# Called when Ipylab is activated and Ipylab backend launches\n",
- "app._init_python_backend()"
+ "# There is a new launcher called 'Start custom app'\n",
+ "t = app.execute_command(\"launcher:create\")"
]
}
],
diff --git a/examples/commands.ipynb b/examples/commands.ipynb
index 6c054502..6d6b68f0 100644
--- a/examples/commands.ipynb
+++ b/examples/commands.ipynb
@@ -58,7 +58,7 @@
"metadata": {},
"outputs": [],
"source": [
- "len(app.commands.commands)"
+ "len(app.command.commands)"
]
},
{
@@ -74,7 +74,7 @@
"metadata": {},
"outputs": [],
"source": [
- "t = app.commands.execute(\n",
+ "t = app.execute_command(\n",
" \"console:create\",\n",
" insertMode=\"split-right\",\n",
" kernelPreference={\"id\": app.kernelId},\n",
@@ -104,7 +104,7 @@
"metadata": {},
"outputs": [],
"source": [
- "app.commands.execute(\"apputils:change-theme\", theme=\"JupyterLab Dark\")"
+ "app.execute_command(\"apputils:change-theme\", theme=\"JupyterLab Dark\")"
]
},
{
@@ -113,7 +113,7 @@
"metadata": {},
"outputs": [],
"source": [
- "t = app.commands.execute(\"apputils:change-theme\", theme=\"JupyterLab Light\")"
+ "t = app.execute_command(\"apputils:change-theme\", theme=\"JupyterLab Light\")"
]
},
{
@@ -138,7 +138,7 @@
"metadata": {},
"outputs": [],
"source": [
- "app.commands.execute(\"terminal:create-new\")\n",
+ "app.execute_command(\"terminal:create-new\")\n",
"# It can take up to 10 seconds for the terminal to show up after the cell returns."
]
},
@@ -208,7 +208,7 @@
"metadata": {},
"outputs": [],
"source": [
- "app.commands.addPythonCommand(\n",
+ "app.command.addPythonCommand(\n",
" \"swap_orientation\",\n",
" execute=toggle_orientation,\n",
" label=\"Swap orientation\",\n",
@@ -229,7 +229,7 @@
"metadata": {},
"outputs": [],
"source": [
- "t = app.commands.execute(\"swap_orientation\")"
+ "t = app.execute_command(\"swap_orientation\")"
]
},
{
@@ -270,7 +270,7 @@
"metadata": {},
"outputs": [],
"source": [
- "assert \"swap_orientation\" in app.commands.commands"
+ "assert \"swap_orientation\" in app.command.commands"
]
},
{
@@ -312,6 +312,24 @@
"Open the command palette CTRL + SHIFT + C and the command should show now be visible. Look for `Swap orientation`"
]
},
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "app.launcher.add_item(\"swap_orientation\", \"Python Commands\")"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "t.result()"
+ ]
+ },
{
"cell_type": "markdown",
"metadata": {},
@@ -327,7 +345,7 @@
"metadata": {},
"outputs": [],
"source": [
- "app.commands.remove_command(\"swap_orientation\")"
+ "app.command.remove_command(\"swap_orientation\")"
]
},
{
@@ -336,7 +354,7 @@
"metadata": {},
"outputs": [],
"source": [
- "assert \"swap_orientation\" not in app.commands.commands"
+ "assert \"swap_orientation\" not in app.command.commands"
]
}
],
@@ -356,14 +374,7 @@
"name": "python",
"nbconvert_exporter": "python",
"pygments_lexer": "ipython3",
- "version": "3.12.1"
- },
- "widgets": {
- "application/vnd.jupyter.widget-state+json": {
- "state": {},
- "version_major": 2,
- "version_minor": 0
- }
+ "version": "3.10.13"
}
},
"nbformat": 4,
diff --git a/examples/icons.ipynb b/examples/icons.ipynb
index ea645f4f..0b19c28e 100644
--- a/examples/icons.ipynb
+++ b/examples/icons.ipynb
@@ -240,7 +240,7 @@
},
"outputs": [],
"source": [
- "panel.app.commands.addPythonCommand(\n",
+ "panel.app.command.addPythonCommand(\n",
" \"my-icon:randomize\",\n",
" randomize_icon,\n",
" label=\"Randomize My Icon\",\n",
@@ -295,7 +295,7 @@
},
"outputs": [],
"source": [
- "panel.app.commands.execute(\"apputils:activate-command-palette\")"
+ "panel.app.execute_command(\"apputils:activate-command-palette\")"
]
},
{
@@ -313,7 +313,7 @@
"metadata": {},
"outputs": [],
"source": [
- "panel.app.commands.execute(\"my-icon:randomize\", count=1)"
+ "panel.app.execute_command(\"my-icon:randomize\", count=1)"
]
}
],
diff --git a/examples/ipytree.ipynb b/examples/ipytree.ipynb
index dd73a29f..42059255 100644
--- a/examples/ipytree.ipynb
+++ b/examples/ipytree.ipynb
@@ -262,7 +262,7 @@
"Let's also define a couple of buttons to:\n",
"\n",
"- open the selected files\n",
- "- expand all nodes of the tre\n",
+ "- expand all nodes of the tree\n",
"- collapse all nodes of the tree"
]
},
@@ -315,7 +315,7 @@
"cell_type": "markdown",
"metadata": {},
"source": [
- "When the \"Open\" button is clicked, we call `app.commands.execute` with the path to the file to open it in the JupyterLab interface."
+ "When the \"Open\" button is clicked, we call `app.execute_command` with the path to the file to open it in the JupyterLab interface."
]
},
{
@@ -328,7 +328,7 @@
" for node in file_tree.selected_nodes:\n",
" filepath = node.fullpath\n",
" if filepath:\n",
- " app.commands.execute(\"docmanager:open\", {\"path\": filepath})\n",
+ " app.execute_command(\"docmanager:open\", {\"path\": filepath})\n",
"\n",
"\n",
"open_button.on_click(on_open_clicked)"
@@ -413,13 +413,6 @@
"nbconvert_exporter": "python",
"pygments_lexer": "ipython3",
"version": "3.8.2"
- },
- "widgets": {
- "application/vnd.jupyter.widget-state+json": {
- "state": {},
- "version_major": 2,
- "version_minor": 0
- }
}
},
"nbformat": 4,
diff --git a/examples/sessions.ipynb b/examples/sessions.ipynb
index d0ed6f35..d3eef614 100644
--- a/examples/sessions.ipynb
+++ b/examples/sessions.ipynb
@@ -80,7 +80,7 @@
"metadata": {},
"outputs": [],
"source": [
- "app.commands.execute(\"console:create\", **app.current_session)"
+ "app.execute_command(\"console:create\", **app.current_session)"
]
},
{
@@ -89,7 +89,7 @@
"metadata": {},
"outputs": [],
"source": [
- "app.commands.execute(\"notebook:create-console\")"
+ "app.execute_command(\"notebook:create-console\")"
]
},
{
diff --git a/ipylab/asyncwidget.py b/ipylab/asyncwidget.py
index 3f7e1de3..35118a3a 100644
--- a/ipylab/asyncwidget.py
+++ b/ipylab/asyncwidget.py
@@ -15,6 +15,7 @@
from traitlets import Bool, Dict, Instance, Set, Unicode
import ipylab._frontend as _fe
+from ipylab.hasapp import HasApp
from ipylab.hookspecs import pm
if sys.version_info >= (3, 11):
@@ -113,7 +114,7 @@ class IpylabFrontendError(IOError):
pass
-class WidgetBase(Widget):
+class WidgetBase(Widget, HasApp):
_model_module = Unicode(_fe.module_name, read_only=True).tag(sync=True)
_model_module_version = Unicode(_fe.module_version, read_only=True).tag(sync=True)
_view_module = Unicode(_fe.module_name, read_only=True).tag(sync=True)
@@ -156,6 +157,7 @@ def __init__(self, *, model_id=None, **kwgs):
self.on_msg(self._on_frontend_msg)
async def __aenter__(self):
+ self._check_closed()
if not self._ready_response.is_set():
await self.wait_ready()
@@ -180,25 +182,31 @@ def _check_closed(self):
def _check_get_error(self, content={}) -> IpylabFrontendError | None:
error = content.get("error")
if error:
- if operation := content.get("operation"):
- return IpylabFrontendError(
- f"{self.__class__.__name__} operation '{operation}' failed with message \"{error}\""
- )
+ operation = content.get("operation")
+ if operation:
+ msg = f"{self.__class__.__name__} operation '{operation}' failed with message \"{error}\""
+ if "cyclic" in error:
+ msg += (
+ "\nNote: A cyclic error may be due a return value that cannot be converted to JSON. "
+ "Try changing the transform (eg: transform=ipylab.TransformMode.done)."
+ )
+ return IpylabFrontendError(msg)
+
return IpylabFrontendError(f'{self.__class__.__name__} failed with message "{error}"')
else:
return None
async def wait_ready(self) -> None:
if not self._ready_response.is_set():
- self.log.info(f"Connecting to frontend {self._model_name}")
+ self.log.info(f"Connecting to frontend model '{self._model_name}'")
await self._ready_response.wait()
- self.log.info(f"Connected to frontend {self._model_name}")
+ self.log.info(f"Connected to frontend model '{self._model_name}'")
def send(self, content, buffers=None):
try:
super().send(content, buffers)
except Exception as error:
- pm.hook.on_send_error(self, error, content, buffers)
+ pm.hook.on_send_error(obj=self, error=error, content=content, buffers=buffers)
async def _send_receive(self, content: dict, callback: Callable):
async with self:
@@ -223,8 +231,6 @@ def _on_frontend_msg(self, _, content: dict, buffers: list):
payload = content.get("payload", {})
if ipylab_BE:
self._pending_operations.pop(ipylab_BE).set(payload, error)
- elif error:
- pm.hook.on_frontend_error(obj=self, error=self.error, content=content)
ipylab_FE = content.get("ipylab_FE", "")
if ipylab_FE:
task = asyncio.create_task(
@@ -232,6 +238,8 @@ def _on_frontend_msg(self, _, content: dict, buffers: list):
)
self._tasks.add(task)
task.add_done_callback(self._tasks.discard)
+ if error:
+ pm.hook.on_frontend_error(obj=self, error=error, content=content)
elif init_message := content.get("init"):
self._ready_response.set(content)
print(init_message)
@@ -247,9 +255,6 @@ async def _handle_frontend_operation(
buffers = []
try:
result = await self._do_operation_for_frontend(operation, payload, buffers)
- if result is None:
- pm.hook.unhandled_frontend_operation_message(self, operation)
- raise ValueError(f"{operation=}")
if isinstance(result, dict) and "buffers" in result:
buffers = result["buffers"]
result = result["payload"]
@@ -258,7 +263,7 @@ async def _handle_frontend_operation(
content["error"] = "Cancelled"
except Exception as e:
content["error"] = str(e)
- pm.hook.on_frontend_operation_error(self, error=e, content=payload)
+ pm.hook.on_frontend_operation_error(obj=self, error=e, content=payload)
finally:
self.send(content, buffers)
@@ -266,7 +271,7 @@ async def _do_operation_for_frontend(self, operation: str, payload: dict, buffer
"""Overload this function as required.
or if there is a buffer can return a dict {"payload":dict, "buffers":[]}
"""
- pm.hook.unhandled_frontend_operation_message(self, operation)
+ pm.hook.unhandled_frontend_operation_message(obj=self, operation=operation)
def schedule_operation(
self,
@@ -414,3 +419,20 @@ def callback_(content: dict, payload: list):
transform=transform,
widget=widget,
)
+
+ def execute_command(
+ self,
+ command_id: str,
+ transform: TransformMode | dict[str, str] = TransformMode.done,
+ **args,
+ ) -> asyncio.Task:
+ """Execute command_id.
+
+ `args` correspond to `args` in JupyterLab.
+
+ Finding what the `args` are remains an outstanding issue in JupyterLab.
+
+ see: https://github.com/jtpio/ipylab/issues/128#issuecomment-1683097383 for hints
+ about how args can be found.
+ """
+ return self.execute_method("app.commands.execute", command_id, args, transform=transform)
diff --git a/ipylab/commands.py b/ipylab/commands.py
index 4717c760..ac787cfb 100644
--- a/ipylab/commands.py
+++ b/ipylab/commands.py
@@ -3,11 +3,12 @@
from __future__ import annotations
import asyncio
+import inspect
from collections.abc import Callable
-from traitlets import Dict, Tuple, Unicode, observe
+from traitlets import Dict, Tuple, Unicode
-from ipylab.asyncwidget import AsyncWidgetBase, TransformMode, pack, register
+from ipylab.asyncwidget import AsyncWidgetBase, pack, register
from ipylab.hookspecs import pm
from ipylab.widgets import Icon
@@ -15,7 +16,6 @@
@register
class CommandPalette(AsyncWidgetBase):
_model_name = Unicode("CommandPaletteModel").tag(sync=True)
-
items = Tuple(read_only=True).tag(sync=True)
def add_item(
@@ -31,7 +31,12 @@ def add_item(
)
def remove_item(self, command_id: str, category) -> asyncio.Task:
- return self.schedule_operation(operation="addItem", id=command_id, category=category)
+ return self.schedule_operation(operation="removeItem", id=command_id, category=category)
+
+
+@register
+class Launcher(CommandPalette):
+ _model_name = Unicode("LauncherModel").tag(sync=True)
@register
@@ -39,45 +44,30 @@ class CommandRegistry(AsyncWidgetBase):
_model_name = Unicode("CommandRegistryModel").tag(sync=True)
SINGLETON = True
commands = Tuple(read_only=True).tag(sync=True)
-
_execute_callbacks: dict[str : Callable[[], None]] = Dict()
- @observe("commands")
- def _observe_commands(self, change):
- commands = self.commands
- for k in tuple(self._execute_callbacks):
- if k not in commands:
- self._execute_callbacks.pop(k)
-
async def _do_operation_for_frontend(
self, operation: str, payload: dict, buffers: list
) -> bool | None:
if operation == "execute":
command_id = payload.get("id")
- cmd = self._execute_callbacks[command_id]
- result = cmd(**payload.get("kwgs", {}))
+ cmd = self._get_command(command_id)
+ kwgs = dict(payload.get("kwgs", {}))
+ for k in set(inspect.signature(cmd).parameters.keys()).difference(kwgs):
+ kwgs.pop(k)
+ result = cmd(**kwgs)
if asyncio.iscoroutine(result):
result = await result
return result
else:
- pm.hook.unhandled_frontend_operation_message(self, operation)
-
- def execute(
- self,
- command_id: str,
- transform: TransformMode | dict[str, str] = TransformMode.raw,
- **args,
- ) -> asyncio.Task:
- """Execute command_id.
-
- `args` correspond to `args` in JupyterLab.
-
- Finding what the `args` are remains an outstanding issue in JupyterLab.
+ pm.hook.unhandled_frontend_operation_message(obj=self, operation=operation)
- see: https://github.com/jtpio/ipylab/issues/128#issuecomment-1683097383 for hints
- about how args can be found.
- """
- return self.execute_method("app.commands.execute", command_id, args, transform=transform)
+ def _get_command(self, command_id: str) -> Callable:
+ "Get a registered Python command"
+ if command_id not in self._execute_callbacks:
+ msg = f"{command_id} is not a registered command!"
+ raise KeyError(msg)
+ return self._execute_callbacks[command_id]
def addPythonCommand(
self,
@@ -90,8 +80,6 @@ def addPythonCommand(
icon: Icon = None,
**kwgs,
):
- if command_id in self.commands:
- raise Exception(f"Command '{command_id} is already registered!")
# TODO: support other parameters (isEnabled, isVisible...)
self._execute_callbacks = self._execute_callbacks | {command_id: execute}
return self.schedule_operation(
@@ -104,8 +92,14 @@ def addPythonCommand(
**kwgs,
)
- def remove_command(self, command_id: str, **kwgs) -> asyncio.Task:
+ def removePythonCommand(self, command_id: str, **kwgs) -> asyncio.Task:
# TODO: check whether to keep this method, or return disposables like in lab
- if command_id not in self.commands:
+ if command_id not in self._execute_callbacks:
raise ValueError(f"{command_id=} is not a registered command!")
- return self.schedule_operation("removePythonCommand", command_id=command_id, **kwgs)
+
+ def callback(content: dict, payload: list):
+ self._execute_callbacks.pop(command_id, None)
+
+ return self.schedule_operation(
+ "removePythonCommand", command_id=command_id, callback=callback, **kwgs
+ )
diff --git a/ipylab/hookspecs.py b/ipylab/hookspecs.py
index 80bc5e89..97145e43 100644
--- a/ipylab/hookspecs.py
+++ b/ipylab/hookspecs.py
@@ -11,8 +11,6 @@
hookspec = pluggy.HookspecMarker("ipylab")
pm = pluggy.PluginManager("ipylab")
if t.TYPE_CHECKING:
- import jupyterlab.labapp
-
from ipylab.asyncwidget import AsyncWidgetBase
@@ -42,62 +40,12 @@ def on_send_error(self, obj: AsyncWidgetBase, error: Exception, content: dict, b
def on_frontend_operation_error(self, obj: AsyncWidgetBase, error: Exception, content: dict):
"""Handle an error processing an operation from the frontend."""
- @hookspec(firstresult=True)
- def get_ipylab_backend_class(self) -> type[jupyterlab.labapp.LabApp]:
- """Return the class to use as the backend.
-
- This will override the app used when launching with the console command `ipylab ...`.
- or when calling `python -m ipylab ...`
- """
-
- @hookspec()
- def run_once_at_startup(self):
- """The function will run once when Ipylab is activated (requires entry point as explained below).
-
- ``` python
- # @ ipylab_plugin.py
- import ipylab
-
- class myPluginDefs:
- @ipylab.hookimpl(specname="run_once_at_startup")
- def plugin_my_launcher():
- # Do my startup tasks
-
-
-
- myPlugins = myPluginDefs()
-
- ```
-
- Note: The package should be installed (re-installed) with the entry point "ipylab-python-backend"
-
- in pyproject.toml
- ``` toml
- [project.entry-points.ipylab-python-backend]
- my-plugins-name = "my_module.ipylab_plugin:myPlugins"
-
- ```
-
- entry_point: str [: