diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml
index ab2b161..b430e9c 100644
--- a/.pre-commit-config.yaml
+++ b/.pre-commit-config.yaml
@@ -27,7 +27,7 @@ repos:
- id: requirements-txt-fixer
- id: check-builtin-literals
- repo: https://github.com/python-jsonschema/check-jsonschema
- rev: 0.28.0
+ rev: 0.28.1
hooks:
- id: check-github-workflows
- repo: https://github.com/ComPWA/mirrors-taplo
@@ -35,11 +35,11 @@ repos:
hooks:
- id: taplo
- repo: https://github.com/astral-sh/ruff-pre-commit
- rev: v0.3.4
+ rev: v0.3.5
hooks:
- id: ruff
types_or: [python, jupyter]
- args: ['--fix', '--show-fixes']
+ args: ['--fix']
- id: ruff-format
types_or: [python, jupyter]
- repo: https://github.com/codespell-project/codespell
diff --git a/.vscode/settings.json b/.vscode/settings.json
index e30d11e..c991dcd 100644
--- a/.vscode/settings.json
+++ b/.vscode/settings.json
@@ -12,5 +12,6 @@
},
"editor.formatOnSave": true,
"python.terminal.activateEnvInCurrentTerminal": true,
- "python.createEnvironment.trigger": "prompt"
+ "python.createEnvironment.trigger": "prompt",
+ "python.analysis.typeCheckingMode": "basic"
}
diff --git a/README.md b/README.md
index fe409a6..5025802 100644
--- a/README.md
+++ b/README.md
@@ -88,7 +88,7 @@ jupyter lab
```bash
# create a new conda environment
-mamba create -n ipylab -c conda-forge jupyter-packaging nodejs python=3.10 -y
+mamba create -n ipylab -c conda-forge jupyter-packaging nodejs python=3.11 -y
# activate the environment
conda activate ipylab
diff --git a/docs/conf.py b/docs/conf.py
index bb87f00..c15fbe0 100644
--- a/docs/conf.py
+++ b/docs/conf.py
@@ -1,4 +1,4 @@
-from __future__ import annotations
+from __future__ import annotations # noqa: INP001
extensions = ["myst_parser", "jupyterlite_sphinx"]
diff --git a/examples/autostart.ipynb b/examples/autostart.ipynb
index 3844b5a..0096ffe 100644
--- a/examples/autostart.ipynb
+++ b/examples/autostart.ipynb
@@ -67,15 +67,20 @@
"source": [
"# @my_module.autostart.py\n",
"\n",
+ "import asyncio\n",
+ "\n",
+ "import ipylab\n",
+ "\n",
"\n",
"async def create_app():\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",
+ " global ma # noqa: PLW0603\n",
+ "\n",
" ma = ipylab.MainArea(name=\"My demo app\")\n",
" await ma.wait_ready()\n",
" ma.content.title.label = \"Simple app\"\n",
@@ -86,10 +91,8 @@
" 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.executeCommand(\"Not a command\"))\n",
+ " console_button.on_click(lambda _: ma.load_console() if ma.console_status == \"unloaded\" else ma.unload_console())\n",
+ " error_button.on_click(lambda _: ma.executeCommand(\"Not a command\"))\n",
" ma.content.children = [\n",
" ipw.HTML(f\"
My simple app
Welcome to my app.
kernel id: {ma.kernelId}\"),\n",
" ipw.HBox([console_button, error_button]),\n",
@@ -105,27 +108,25 @@
" class IpylabPlugins:\n",
" # Define plugins (see IpylabHookspec for available hooks)\n",
" @ipylab.hookimpl\n",
- " def on_frontend_error(self, obj, error, content):\n",
+ " def on_frontend_error(self, obj, error, content): # noqa: ARG002\n",
" ma.app.dialog.show_error_message(\"Error\", str(error))\n",
"\n",
" # Register plugin for this kernel.\n",
- " ipylab.hookspecs.pm.register(IpylabPlugins())\n",
+ " ipylab.hookspecs.pm.register(IpylabPlugins()) # type: ignore\n",
" await ma.load()\n",
"\n",
"\n",
- "import asyncio\n",
- "\n",
- "import ipylab\n",
- "\n",
"n = 0\n",
"app = ipylab.JupyterFrontEnd()\n",
"\n",
"\n",
- "async def start_my_app(cwd):\n",
- " global n\n",
+ "async def start_my_app(cwd): # noqa: ARG001\n",
+ " global n # noqa: PLW0603\n",
" n += 1\n",
" task = app.execEval(\n",
- " code=create_app, user_expressions={\"main_area_widget\": \"create_app()\"}, path=f\"my app {n}\"\n",
+ " code=create_app,\n",
+ " user_expressions={\"main_area_widget\": \"create_app()\"},\n",
+ " path=f\"my app {n}\",\n",
" )\n",
" if app.current_widget_id.startswith(\"launcher\"):\n",
" await app.executeMethod(\"app.shell.currentWidget.dispose\")\n",
diff --git a/examples/commands.ipynb b/examples/commands.ipynb
index 0fd910b..af5d9cf 100644
--- a/examples/commands.ipynb
+++ b/examples/commands.ipynb
@@ -157,9 +157,7 @@
"metadata": {},
"outputs": [],
"source": [
- "split_panel = ipylab.SplitPanel(\n",
- " children=[ipw.Button(description=\"Item A\"), ipw.Button(description=\"Item B\")]\n",
- ")\n",
+ "split_panel = ipylab.SplitPanel(children=[ipw.Button(description=\"Item A\"), ipw.Button(description=\"Item B\")])\n",
"split_panel.addToShell()"
]
},
@@ -177,9 +175,7 @@
"outputs": [],
"source": [
"def toggle_orientation():\n",
- " split_panel.orientation = list(\n",
- " {\"horizontal\", \"vertical\"}.difference((split_panel.orientation,))\n",
- " )[0]\n",
+ " split_panel.orientation = next(iter({\"horizontal\", \"vertical\"}.difference((split_panel.orientation,))))\n",
" split_panel.title.label = split_panel.orientation\n",
" return \"Data returned from this custom function 'toggle_orientation'\""
]
@@ -264,13 +260,20 @@
"Also the list of commands gets updated with the newly added command:"
]
},
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "metadata": {},
+ "outputs": [],
+ "source": []
+ },
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
- "assert \"swap_orientation\" in app.command.commands"
+ "assert \"swap_orientation\" in app.command.commands # noqa: S101"
]
},
{
@@ -354,7 +357,7 @@
"metadata": {},
"outputs": [],
"source": [
- "assert \"swap_orientation\" not in app.command.commands"
+ "assert \"swap_orientation\" not in app.command.commands # noqa: S101"
]
}
],
diff --git a/examples/icons.ipynb b/examples/icons.ipynb
index 0887da4..c101909 100644
--- a/examples/icons.ipynb
+++ b/examples/icons.ipynb
@@ -2,7 +2,7 @@
"cells": [
{
"cell_type": "markdown",
- "id": "690da69f-cf3d-40a9-b8e0-610339c859e9",
+ "id": "0",
"metadata": {},
"source": [
"# Icons"
@@ -10,7 +10,7 @@
},
{
"cell_type": "markdown",
- "id": "d5267848-fde6-4108-b7ff-be9e7303d0e4",
+ "id": "1",
"metadata": {},
"source": [
"Icons can be applied to both the `Title` of a `Panel` [widgets](./widgets.ipynb) and [commands](./commands.ipynb), providing more customization than `icon_class`."
@@ -19,7 +19,7 @@
{
"cell_type": "code",
"execution_count": null,
- "id": "5eb2e5cc",
+ "id": "2",
"metadata": {},
"outputs": [],
"source": [
@@ -29,7 +29,7 @@
{
"cell_type": "code",
"execution_count": null,
- "id": "2b18ff57-b458-4b92-bbf3-0e660343d067",
+ "id": "3",
"metadata": {
"tags": []
},
@@ -45,7 +45,7 @@
},
{
"cell_type": "markdown",
- "id": "5a05db60-7147-4466-b8e3-296e59204013",
+ "id": "4",
"metadata": {},
"source": [
"## SVG\n",
@@ -62,7 +62,7 @@
{
"cell_type": "code",
"execution_count": null,
- "id": "a02c39d9-1284-4f84-b13f-d7028640c821",
+ "id": "5",
"metadata": {
"tags": []
},
@@ -76,7 +76,7 @@
},
{
"cell_type": "markdown",
- "id": "22c3c5a5-ae3c-435c-9f12-21e247cf80f3",
+ "id": "6",
"metadata": {},
"source": [
"Icons can be displayed directly, and sized with the `layout` member inherited from `ipywidgets.DOMWidget`."
@@ -85,19 +85,19 @@
{
"cell_type": "code",
"execution_count": null,
- "id": "add039d5-35d7-44ee-a70e-7d9cf38f56eb",
+ "id": "7",
"metadata": {
"tags": []
},
"outputs": [],
"source": [
- "icon = ipylab.Icon(name=\"my-icon\", svgstr=SVG, layout=dict(width=\"32px\"))\n",
+ "icon = ipylab.Icon(name=\"my-icon\", svgstr=SVG, layout={\"width\": \"32px\"})\n",
"icon"
]
},
{
"cell_type": "markdown",
- "id": "2555a92e-3335-407d-8e8e-22d686794913",
+ "id": "8",
"metadata": {},
"source": [
"### More about `jp-icon` classes\n",
@@ -107,7 +107,7 @@
{
"cell_type": "code",
"execution_count": null,
- "id": "ccea8868-2d3e-454d-b3d5-15e16cf51f54",
+ "id": "9",
"metadata": {
"tags": []
},
@@ -118,10 +118,6 @@
"background = ipw.SelectionSlider(description=\"background\", options=options)\n",
"foreground = ipw.SelectionSlider(description=\"foreground\", options=options)\n",
"\n",
- "repaint = lambda: SVG.replace(\"jp-icon3\", background.value).replace(\n",
- " \"jp-contrast0\", foreground.value\n",
- ")\n",
- "\n",
"traitlets.dlink((background, \"value\"), (icon, \"svgstr\"), lambda x: SVG.replace(\"jp-icon3\", x))\n",
"traitlets.dlink((foreground, \"value\"), (icon, \"svgstr\"), lambda x: SVG.replace(\"jp-contrast0\", x))\n",
"size = ipw.FloatSlider(32, description=\"size\")\n",
@@ -132,7 +128,7 @@
},
{
"cell_type": "markdown",
- "id": "5f0a6742-3a3b-4879-8bb0-e675c80f03ed",
+ "id": "10",
"metadata": {},
"source": [
"## Icons on Panel Titles\n",
@@ -143,7 +139,7 @@
{
"cell_type": "code",
"execution_count": null,
- "id": "29a7cd8c-e26b-4a54-aca5-ae19fab2772d",
+ "id": "11",
"metadata": {
"tags": []
},
@@ -158,7 +154,7 @@
},
{
"cell_type": "markdown",
- "id": "1df9a22e-90d6-440a-9921-3519e0e4da53",
+ "id": "12",
"metadata": {},
"source": [
"### More Title Options\n",
@@ -169,7 +165,7 @@
{
"cell_type": "code",
"execution_count": null,
- "id": "ebb3698f-7752-4584-89f1-7d552264a04e",
+ "id": "13",
"metadata": {
"tags": []
},
@@ -204,7 +200,7 @@
},
{
"cell_type": "markdown",
- "id": "f3018262-e383-4b8e-b0cb-b8cefd936d98",
+ "id": "14",
"metadata": {},
"source": [
"## Icons on Commands\n",
@@ -215,7 +211,7 @@
{
"cell_type": "code",
"execution_count": null,
- "id": "386a1149-26f9-4def-9eab-146baaebfdb3",
+ "id": "15",
"metadata": {
"tags": []
},
@@ -226,15 +222,15 @@
"\n",
"\n",
"async def randomize_icon(count=10):\n",
- " for i in range(count):\n",
- " background.value = random.choice(options)\n",
+ " for _ in range(count):\n",
+ " background.value = random.choice(options) # noqa: S311\n",
" await asyncio.sleep(0.1)"
]
},
{
"cell_type": "code",
"execution_count": null,
- "id": "5b16eaad-69e2-436c-a99a-d9e299977973",
+ "id": "16",
"metadata": {
"tags": []
},
@@ -250,7 +246,7 @@
},
{
"cell_type": "markdown",
- "id": "dc396794-ee61-4af4-aac3-d303321eb04a",
+ "id": "17",
"metadata": {},
"source": [
"To see these, add the a _Command Palette_ with the same `command_id`:"
@@ -259,7 +255,7 @@
{
"cell_type": "code",
"execution_count": null,
- "id": "82b36a75-235f-4ac1-92a5-b79131b480f1",
+ "id": "18",
"metadata": {
"tags": []
},
@@ -271,16 +267,16 @@
{
"cell_type": "code",
"execution_count": null,
- "id": "0de77440-c03d-4e7b-98d9-d9b29d72de5c",
+ "id": "19",
"metadata": {},
"outputs": [],
"source": [
- "assert panel.app.command_pallet.items"
+ "assert panel.app.command_pallet.items # noqa: S101"
]
},
{
"cell_type": "markdown",
- "id": "4d961ace-35da-4965-b529-703b69ce828b",
+ "id": "20",
"metadata": {},
"source": [
"Then open the _Command Palette_ (keyboard shortcut is `CTRL + SHIFT + C`)."
@@ -289,7 +285,7 @@
{
"cell_type": "code",
"execution_count": null,
- "id": "0bcbb157-6741-446a-a04b-de8ad2dda74a",
+ "id": "21",
"metadata": {
"tags": []
},
@@ -300,7 +296,7 @@
},
{
"cell_type": "markdown",
- "id": "d5a79812-5973-47ff-8b87-e7f415a5dcae",
+ "id": "22",
"metadata": {},
"source": [
"And run 'Randomize my icon'"
@@ -309,7 +305,7 @@
{
"cell_type": "code",
"execution_count": null,
- "id": "c023ea16-619e-4961-92b9-98dcf0d8f60d",
+ "id": "23",
"metadata": {},
"outputs": [],
"source": [
diff --git a/examples/main_area.ipynb b/examples/main_area.ipynb
index ced1f70..b18fb97 100644
--- a/examples/main_area.ipynb
+++ b/examples/main_area.ipynb
@@ -60,13 +60,13 @@
"console_button = ipw.Button(description=\"Toggle console\")\n",
"\n",
"\n",
- "def on_click(b):\n",
+ "def on_click(_):\n",
" ma.load_console() if ma.console_status == \"unloaded\" else ma.unload_console()\n",
"\n",
"\n",
"console_button.on_click(on_click)\n",
"close_button = ipw.Button(description=\"Close\")\n",
- "close_button.on_click(lambda b: ma.close())\n",
+ "close_button.on_click(lambda _: ma.close())\n",
"ma.content.children = [some_html, console_button, close_button]"
]
},
@@ -85,7 +85,7 @@
"metadata": {},
"outputs": [],
"source": [
- "t = ma.load(area=\"main\")"
+ "t = ma.load(area=ipylab.Area.main)"
]
},
{
diff --git a/ipylab/__init__.py b/ipylab/__init__.py
index a09f3d0..a017bbe 100644
--- a/ipylab/__init__.py
+++ b/ipylab/__init__.py
@@ -2,7 +2,7 @@
# Distributed under the terms of the Modified BSD License.
from __future__ import annotations
-from ._version import __version__
+from ipylab._version import __version__
__all__ = [
"__version__",
diff --git a/ipylab/asyncwidget.py b/ipylab/asyncwidget.py
index eeccb45..257c19c 100644
--- a/ipylab/asyncwidget.py
+++ b/ipylab/asyncwidget.py
@@ -4,37 +4,35 @@
import asyncio
import inspect
-import sys
import traceback
-import types
import uuid
-from collections.abc import Callable, Coroutine
-from typing import Any
+from collections.abc import Callable
+from enum import StrEnum
+from typing import TYPE_CHECKING, Any
from ipywidgets import Widget, register, widget_serialization
-from traitlets import Bool, Dict, Instance, Set, Unicode
+from traitlets import Bool, Container, 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):
- from enum import StrEnum
-else:
- from backports.strenum import StrEnum
+if TYPE_CHECKING:
+ import types
+ from typing import ClassVar
__all__ = ["AsyncWidgetBase", "WidgetBase", "register", "pack", "Widget"]
-def pack(obj: Widget):
+def pack(obj: Widget | Any):
"""Return serialized obj if it is a Widget otherwise return it unchanged."""
if isinstance(obj, Widget):
- obj = widget_serialization["to_json"](obj, None)
+ return widget_serialization["to_json"](obj, None)
return obj
-def pack_code(code: str | types.ModuleType) -> str:
+def pack_code(code: str | types.ModuleType | Callable) -> str:
"""Convert code to a string suitable to run in a kernel."""
if not isinstance(code, str):
code = inspect.getsource(code)
@@ -74,10 +72,9 @@ class TransformMode(StrEnum):
```
transform = {
- "mode": "function",
- "code": "function (obj) { return obj.id; }",
- }
- """
+ "mode": "function",
+ "code": "function (obj) { return obj.id; }",
+ }"""
raw = "raw"
done = "done"
@@ -86,10 +83,23 @@ class TransformMode(StrEnum):
function = "function"
+class JavascriptType(StrEnum):
+ string = "string"
+ number = "number"
+ boolean = "boolean"
+ object = "object"
+ function = "function"
+
+
+CallbackType = Callable[[dict[str, Any], Any], Any]
+TransformType = TransformMode | dict[str, str]
+
+
class Response(asyncio.Event):
def set(self, payload, error: Exception | None = None) -> None:
- if self._value:
- raise RuntimeError("Already set!")
+ if getattr(self, "_value", False):
+ msg = "Already set!"
+ raise RuntimeError(msg)
self.payload = payload
self.error = error
super().set()
@@ -112,21 +122,23 @@ class WidgetBase(Widget, HasApp):
_view_module = Unicode(_fe.module_name, read_only=True).tag(sync=True)
_view_module_version = Unicode(_fe.module_version, read_only=True).tag(sync=True)
_comm = None
+ add_traits = None # type: ignore # Don't support the method HasTraits.add_traits as it creates a new type that isn't a subclass of its origin)
class AsyncWidgetBase(WidgetBase):
"""The base for all widgets that need async comms with the frontend model."""
- kernelId = Unicode(read_only=True).tag(sync=True)
- _ipylab_model_register: dict[str, AsyncWidgetBase] = {}
- _singleton_register: dict[type, str] = {}
+ kernelId = Unicode(read_only=True).tag(sync=True) # noqa: N815
+ _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: set[asyncio.Task] = Set()
+ _pending_operations: Dict[str, Response] = Dict()
+ _tasks: Container[set[asyncio.Task]] = Set()
_comm = None
closed = Bool(read_only=True).tag(sync=True)
+ add_traits = None # type: ignore # Don't support the method HasTraits.add_traits as it creates a new type that isn't a subclass of its origin)
def __repr__(self):
return f"<{self.__class__.__name__} at {id(self)}>"
@@ -136,8 +148,7 @@ def __new__(cls, *, model_id=None, **kwgs):
model_id = cls._singleton_register.get(cls.__name__)
if model_id and model_id in cls._ipylab_model_register:
return cls._ipylab_model_register[model_id]
- inst = super().__new__(cls, model_id=model_id, **kwgs)
- return inst
+ return super().__new__(cls, model_id=model_id, **kwgs)
def __init__(self, *, model_id=None, **kwgs):
if self._model_id:
@@ -161,7 +172,7 @@ def open(self) -> None:
super().open()
def close(self):
- self._ipylab_model_register.pop(self._model_id, None)
+ self._ipylab_model_register.pop(self._model_id, None) # type: ignore
for task in self._tasks:
task.cancel()
super().close()
@@ -169,9 +180,12 @@ def close(self):
def _check_closed(self):
if self.closed:
- raise RuntimeError(f"This object is closed {self}")
+ msg = f"This object is closed {self}"
+ raise RuntimeError(msg)
- def _check_get_error(self, content={}) -> IpylabFrontendError | None:
+ def _check_get_error(self, content: dict | None = None) -> IpylabFrontendError | None:
+ if content is None:
+ content = {}
error = content.get("error")
if error:
operation = content.get("operation")
@@ -185,8 +199,7 @@ def _check_get_error(self, content={}) -> IpylabFrontendError | None:
return IpylabFrontendError(msg)
return IpylabFrontendError(f'{self.__class__.__name__} failed with message "{error}"')
- else:
- return None
+ return None
async def wait_ready(self) -> None:
if not self._ready_response.is_set():
@@ -200,15 +213,13 @@ def send(self, content, buffers=None):
except Exception as error:
pm.hook.on_frontend_error(obj=self, error=error, content=content, buffers=buffers)
- async def _send_receive(self, content: dict, callback: Callable):
+ async def _send_receive(self, content: dict, callback: CallbackType | None):
async with self:
self._pending_operations[content["ipylab_BE"]] = response = Response()
self.send(content)
return await self._wait_response_check_error(response, content, callback)
- async def _wait_response_check_error(
- self, response: Response, content: dict, callback: Callable
- ) -> Any:
+ async def _wait_response_check_error(self, response: Response, content: dict, callback: CallbackType | None) -> Any:
payload = await response.wait()
if callback:
payload = callback(content, payload)
@@ -219,31 +230,25 @@ async def _wait_response_check_error(
def _on_frontend_msg(self, _, content: dict, buffers: list):
error = self._check_get_error(content)
if operation := content.get("operation"):
- ipylab_BE = content.get("ipylab_BE", "")
+ ipylab_backend = content.get("ipylab_BE", "")
payload = content.get("payload", {})
- if ipylab_BE:
- self._pending_operations.pop(ipylab_BE).set(payload, error)
- ipylab_FE = content.get("ipylab_FE", "")
- if ipylab_FE:
+ if ipylab_backend:
+ self._pending_operations.pop(ipylab_backend).set(payload, error)
+ ipylab_frontend = content.get("ipylab_FE", "")
+ if ipylab_frontend:
task = asyncio.create_task(
- self._handle_frontend_operation(ipylab_FE, operation, payload, buffers)
+ self._handle_frontend_operation(ipylab_frontend, 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_message := content.get("init"):
+ elif "init" in content:
self._ready_response.set(content)
- print(init_message)
-
- def add_traits(self, **traits):
- raise NotImplementedError("Using this method is a bad idea! Make a subclass instead.")
- async def _handle_frontend_operation(
- self, ipylab_FE: str, operation: str, payload: dict, buffers: list
- ):
+ async def _handle_frontend_operation(self, ipylab_FE: str, operation: str, payload: dict, buffers: list):
"""Handle operation requests from the frontend and reply with a result."""
- content = {"ipylab_FE": ipylab_FE}
+ content: dict[str, Any] = {"ipylab_FE": ipylab_FE}
buffers = []
try:
result = await self._do_operation_for_frontend(operation, payload, buffers)
@@ -254,7 +259,10 @@ async def _handle_frontend_operation(
except asyncio.CancelledError:
content["error"] = "Cancelled"
except Exception as e:
- content["error"] = {"repr": repr(e), "traceback": traceback.format_tb(e.__traceback__)}
+ content["error"] = {
+ "repr": repr(e),
+ "traceback": traceback.format_tb(e.__traceback__),
+ }
pm.hook.on_frontend_error(obj=self, error=e, content=content, buffers=buffers)
finally:
try:
@@ -268,7 +276,7 @@ async def _handle_frontend_operation(
self.send(content, buffers)
pm.hook.on_frontend_error(obj=self, error=e, content=content, buffers=buffers)
- async def _do_operation_for_frontend(self, operation: str, payload: dict, buffers):
+ async def _do_operation_for_frontend(self, operation: str, payload: dict, buffers: list): # noqa: ARG002
"""Overload this function as required.
or if there is a buffer can return a dict {"payload":dict, "buffers":[]}
"""
@@ -278,8 +286,8 @@ def schedule_operation(
self,
operation: str,
*,
- callback: Callable[[any, any], None | Coroutine] = None,
- transform: TransformMode | dict[str, str] = TransformMode.raw,
+ callback: CallbackType | None = None,
+ transform: TransformType = TransformMode.raw,
**kwgs,
) -> asyncio.Task:
"""
@@ -299,12 +307,13 @@ def schedule_operation(
# validation
if not operation or not isinstance(operation, str):
- raise ValueError(f"Invalid {operation=}")
+ msg = f"Invalid {operation=}"
+ raise ValueError(msg)
if isinstance(transform, str):
TransformMode(transform)
else:
TransformMode(transform["mode"])
- ipylab_BE = str(uuid.uuid4())
+ ipylab_BE = str(uuid.uuid4()) # noqa: N806
content = {
"ipylab_BE": ipylab_BE,
"operation": operation,
@@ -312,7 +321,8 @@ def schedule_operation(
"transform": transform,
}
if callback and not callable(callback):
- raise TypeError(f"callback is not callable {type(callback)=}")
+ msg = f"callback is not callable {type(callback)=}"
+ raise TypeError(msg)
task = asyncio.create_task(self._send_receive(content, callback))
self._tasks.add(task)
task.add_done_callback(self._tasks.discard)
@@ -322,8 +332,8 @@ def executeMethod(
self,
method: str,
*args,
- callback: Callable[[any, any], None | Coroutine] = None,
- transform: TransformMode | dict[str, str] = TransformMode.raw,
+ callback: CallbackType | None = None,
+ transform: TransformType = TransformMode.raw,
widget: Widget | None = None,
) -> asyncio.Task:
"""Call a method on the corresponding frontend object.
@@ -338,8 +348,9 @@ def executeMethod(
# This operation is sent to the frontend function _fe_execute in 'ipylab/src/widgets/ipylab.ts'
# validation
- if callback:
- assert callable(callback)
+ if callback is not None and not callable(callback):
+ msg = "callback must be a callable or None"
+ raise TypeError(msg)
return self.schedule_operation(
operation="FE_execute",
FE_execute={
@@ -355,40 +366,34 @@ def getAttribute(
self,
path: str,
*,
- callback: Callable[[any, any], None | Coroutine] = None,
- transform: TransformMode | dict[str, str] = TransformMode.raw,
+ callback: CallbackType | None = None,
+ transform: TransformType = TransformMode.raw,
widget: Widget | None = None,
):
"""A serialized version of the attribute relative to this object."""
- return self.executeMethod(
- "getAttribute",
- path,
- callback=callback,
- transform=transform,
- widget=widget,
- )
+ return self.executeMethod("getAttribute", path, callback=callback, transform=transform, widget=widget)
- def listMethods(self, path: str = "", depth=2, skip_hidden=True) -> asyncio.Task[list[str]]:
+ def listMethods(self, path: str = "", depth=2, skip_hidden=True) -> asyncio.Task[list[str]]: # noqa: FBT002
"""Get a list of methods belonging to the object 'path' of the Frontend instance.
depth: The depth in the object inheritance to search for methods.
"""
- def callback(content: dict, payload: list):
+ def callback(content: dict, payload: list): # noqa: ARG001
if skip_hidden:
return [n for n in payload if not n.startswith("_")]
return payload
- return self.listAttributes(path, "function", depth, how="names", callback=callback)
+ return self.listAttributes(path, "function", depth, how="names", callback=callback) # type: ignore
def listAttributes(
self,
path: str = "",
- type="",
+ type: JavascriptType = JavascriptType.function, # noqa: A002
depth=2,
*,
how="group",
- callback: Callable[[any, any], None | Coroutine] = None,
- transform: TransformMode | dict[str, str] = TransformMode.raw,
+ callback: CallbackType | None = None,
+ transform: TransformType = TransformMode.raw,
widget: Widget | None = None,
) -> asyncio.Task[dict | list]:
"""Get a mapping of attributes of the object at 'path' of the Frontend instance.
@@ -397,7 +402,7 @@ def listAttributes(
how: ['names', 'group', 'raw'] (ignored if callback provided)
"""
- def callback_(content: dict, payload: list):
+ def callback_(content: dict, payload: Any):
if how == "names":
payload = [row["name"] for row in payload]
elif how == "group":
@@ -412,21 +417,10 @@ def callback_(content: dict, payload: list):
return payload
return self.executeMethod(
- "listAttributes",
- path,
- type,
- depth,
- callback=callback_,
- transform=transform,
- widget=widget,
+ "listAttributes", path, type, depth, callback=callback_, transform=transform, widget=widget
)
- def executeCommand(
- self,
- command_id: str,
- transform: TransformMode | dict[str, str] = TransformMode.done,
- **args,
- ) -> asyncio.Task:
+ def executeCommand(self, command_id: str, transform: TransformType = TransformMode.done, **args) -> asyncio.Task:
"""Execute command_id.
`args` correspond to `args` in JupyterLab.
diff --git a/ipylab/commands.py b/ipylab/commands.py
index d8b4de2..43e487c 100644
--- a/ipylab/commands.py
+++ b/ipylab/commands.py
@@ -2,15 +2,19 @@
# Distributed under the terms of the Modified BSD License.
from __future__ import annotations
-import asyncio
import inspect
-from collections.abc import Callable
+from typing import TYPE_CHECKING, Any
from traitlets import Dict, Tuple, Unicode
from ipylab.asyncwidget import AsyncWidgetBase, TransformMode, pack, register
from ipylab.hookspecs import pm
-from ipylab.widgets import Icon
+
+if TYPE_CHECKING:
+ import asyncio
+ from collections.abc import Callable
+
+ from ipylab.widgets import Icon
@register
@@ -18,9 +22,7 @@ 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, *, rank=None, args: dict | None = None, **kwgs) -> asyncio.Task:
return self.schedule_operation(
operation="addItem",
id=command_id,
@@ -44,12 +46,12 @@ 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()
+ _execute_callbacks: Dict[str, Callable[[], None]] = Dict()
- async def _do_operation_for_frontend(self, operation: str, payload: dict, buffers: list) -> any:
+ async def _do_operation_for_frontend(self, operation: str, payload: dict, buffers: list) -> Any:
match operation:
case "execute":
- command_id = payload.get("id")
+ command_id: str = payload.get("id") # type:ignore
cmd = self._get_command(command_id)
kwgs = payload.get("kwgs") or {} | {"buffers": buffers}
for k in set(kwgs).difference(inspect.signature(cmd).parameters.keys()):
@@ -60,6 +62,7 @@ async def _do_operation_for_frontend(self, operation: str, payload: dict, buffer
return result
case _:
pm.hook.unhandled_frontend_operation_message(obj=self, operation=operation)
+ return None
def _get_command(self, command_id: str) -> Callable:
"Get a registered Python command"
@@ -76,7 +79,7 @@ def addPythonCommand(
caption="",
label="",
icon_class="",
- icon: Icon = None,
+ icon: Icon | None = None,
command_result_transform: TransformMode = TransformMode.raw,
**kwgs,
):
@@ -96,11 +99,10 @@ def addPythonCommand(
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._execute_callbacks:
- raise ValueError(f"{command_id=} is not a registered command!")
+ msg = f"{command_id=} is not a registered command!"
+ raise ValueError(msg)
- def callback(content: dict, payload: list):
+ def callback(content: dict, payload: list): # noqa: ARG001
self._execute_callbacks.pop(command_id, None)
- return self.schedule_operation(
- "removePythonCommand", command_id=command_id, callback=callback, **kwgs
- )
+ return self.schedule_operation("removePythonCommand", command_id=command_id, callback=callback, **kwgs)
diff --git a/ipylab/dialog.py b/ipylab/dialog.py
index e44db04..389fd4a 100644
--- a/ipylab/dialog.py
+++ b/ipylab/dialog.py
@@ -2,11 +2,14 @@
# Distributed under the terms of the Modified BSD License.
from __future__ import annotations
-import asyncio
+from typing import TYPE_CHECKING
from ipylab.asyncwidget import Widget, pack
from ipylab.hasapp import HasApp
+if TYPE_CHECKING:
+ import asyncio
+
class Dialog(HasApp):
def get_boolean(self, title: str) -> asyncio.Task:
@@ -42,7 +45,11 @@ def get_password(self, title: str) -> asyncio.Task:
return self.app.schedule_operation("getPassword", title=title)
def show_dialog(
- self, title: str = "", body: str | Widget = "", host: None | Widget = None, **kwgs
+ self,
+ title: str = "",
+ body: str | Widget = "",
+ host: None | Widget = None,
+ **kwgs,
):
"""Jupyter dialog to get user response with custom buttons and checkbox.
@@ -96,13 +103,9 @@ def show_dialog(
source: https://jupyterlab.readthedocs.io/en/stable/extension/ui_helpers.html#generic-dialog
"""
- return self.app.schedule_operation(
- "showDialog", title=pack(title), body=pack(body), host=pack(host), **kwgs
- )
+ return self.app.schedule_operation("showDialog", title=pack(title), body=pack(body), host=pack(host), **kwgs)
- def show_error_message(
- self, title: str, error: str, buttons: None | list[dict[str, str]] = None
- ) -> asyncio.Task:
+ def show_error_message(self, title: str, error: str, buttons: None | list[dict[str, str]] = None) -> asyncio.Task:
"""Jupyter error message.
buttons = [
@@ -121,9 +124,7 @@ def show_error_message(
https://jupyterlab.readthedocs.io/en/stable/api/functions/apputils.showErrorMessage.html#showErrorMessage
"""
- return self.app.schedule_operation(
- "showErrorMessage", title=title, error=error, buttons=buttons
- )
+ return self.app.schedule_operation("showErrorMessage", title=title, error=error, buttons=buttons)
class FileDialog(HasApp):
diff --git a/ipylab/hookspecs.py b/ipylab/hookspecs.py
index bb308cc..10d7682 100644
--- a/ipylab/hookspecs.py
+++ b/ipylab/hookspecs.py
@@ -16,9 +16,7 @@
class IpylabHookspec:
@hookspec
- def on_frontend_error(
- self, obj: AsyncWidgetBase, error: Exception, content: dict, buffers
- ) -> t.NoReturn | None:
+ def on_frontend_error(self, obj: AsyncWidgetBase, error: Exception, content: dict, buffers) -> t.NoReturn | None:
"""Intercept an error message for logging purposes.
Fired when the task handling comms receives the error prior to raising it.
@@ -40,7 +38,8 @@ def unhandled_frontend_operation_message(self, obj: AsyncWidgetBase, operation:
class IpylabDefaultsPlugin:
@hookimpl
def unhandled_frontend_operation_message(self, obj: AsyncWidgetBase, operation: str):
- raise RuntimeError(f"Unhandled frontend_operation_message from {obj=} {operation=}")
+ msg = f"Unhandled frontend_operation_message from {obj=} {operation=}"
+ raise RuntimeError(msg)
pm.add_hookspecs(IpylabHookspec)
diff --git a/ipylab/jupyterfrontend.py b/ipylab/jupyterfrontend.py
index c65bf78..7ef554f 100644
--- a/ipylab/jupyterfrontend.py
+++ b/ipylab/jupyterfrontend.py
@@ -4,24 +4,22 @@
import asyncio
import inspect
-import types
+from typing import TYPE_CHECKING, Any, NotRequired, Self
from traitlets import Dict, Instance, Tuple, Unicode
-from typing_extensions import NotRequired, Self, TypedDict
-
-from ipylab.asyncwidget import (
- AsyncWidgetBase,
- TransformMode,
- pack_code,
- register,
- widget_serialization,
-)
+from typing_extensions import TypedDict
+
+from ipylab.asyncwidget import AsyncWidgetBase, TransformMode, pack_code, register, widget_serialization
from ipylab.commands import CommandPalette, CommandRegistry, Launcher
from ipylab.dialog import Dialog, FileDialog
from ipylab.hookspecs import pm
from ipylab.sessions import SessionManager
from ipylab.shell import Shell
+if TYPE_CHECKING:
+ import types
+ from collections.abc import Callable
+
class LauncherOptions(TypedDict):
name: str
@@ -37,9 +35,7 @@ class JupyterFrontEnd(AsyncWidgetBase):
version = Unicode(read_only=True).tag(sync=True)
command = Instance(CommandRegistry, (), read_only=True).tag(sync=True, **widget_serialization)
- command_pallet = Instance(CommandPalette, (), read_only=True).tag(
- sync=True, **widget_serialization
- )
+ command_pallet = Instance(CommandPalette, (), read_only=True).tag(sync=True, **widget_serialization)
launcher = Instance(Launcher, (), read_only=True).tag(sync=True, **widget_serialization)
current_widget_id = Unicode(read_only=True).tag(sync=True)
@@ -82,7 +78,7 @@ async def wait_ready(self, timeout=5) -> Self:
await asyncio.wait_for(future, timeout)
return self
- def _init_python_backend(self) -> str:
+ def _init_python_backend(self):
"Run by the Ipylab python backend."
# This is called in a separate kernel started by the JavaScript frontend
# the first time the ipylab plugin is activated.
@@ -90,30 +86,25 @@ def _init_python_backend(self) -> str:
try:
count = pm.load_setuptools_entrypoints("ipylab_backend")
- self.log.info(f"Ipylab python backend found {count} plugin entry points.")
+ self.log.info("Ipylab python backend found {%} plugin entry points.", count)
except Exception as e:
- self.log.error("An exception occurred when loading plugins")
+ self.log.exception("An exception occurred when loading plugins")
self.dialog.show_error_message("Plugin failure", str(e))
- async def _do_operation_for_frontend(self, operation: str, payload: dict, buffers: list) -> any:
+ async def _do_operation_for_frontend(self, operation: str, payload: dict, buffers: list) -> Any:
match operation:
case "execEval":
return await self._execEval(payload, buffers)
case _:
pm.hook.unhandled_frontend_operation_message(obj=self, operation=operation)
+ return None
def shutdownKernel(self, kernelId: str | None = None) -> asyncio.Task:
"""Shutdown the kernel"""
return self.schedule_operation("shutdownKernel", kernelId=kernelId)
def newSession(
- self,
- path: str = "",
- *,
- name: str = "",
- kernelId="",
- kernelName="python3",
- code: str | types.ModuleType = "",
+ 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.
@@ -140,13 +131,7 @@ def newSession(
)
def newNotebook(
- self,
- path: str = "",
- *,
- name: str = "",
- kernelId="",
- kernelName="python3",
- code: str | types.ModuleType = "",
+ self, path: str = "", *, name: str = "", kernelId="", kernelName="python3", code: str | types.ModuleType = ""
) -> asyncio.Task:
"""Create a new notebook."""
return self.schedule_operation(
@@ -160,10 +145,7 @@ def newNotebook(
)
def injectCode(
- self,
- kernelId: str,
- code: str | types.ModuleType,
- user_expressions: dict[str, str | types.ModuleType] | None,
+ self, kernelId: str, code: str | types.ModuleType, user_expressions: dict[str, str | types.ModuleType] | None
) -> asyncio.Task:
"""
Inject code into a running kernel using the Jupyter builtin `requestExecute`.
@@ -192,7 +174,7 @@ def injectCode(
def execEval(
self,
- code: str | types.ModuleType,
+ code: str | types.ModuleType | Callable,
user_expressions: dict[str, str | types.ModuleType] | None,
sessionId="",
**kwgs,
@@ -226,7 +208,7 @@ def execEval(
**kwgs,
)
- async def _execEval(self, payload: dict, buffers: list) -> any:
+ async def _execEval(self, payload: dict, buffers: list) -> Any:
"""exec/eval code corresponding to a call from execEval, likely from
another kernel."""
# TODO: consider if globals / locals / async scope should be supported.
@@ -234,11 +216,11 @@ async def _execEval(self, payload: dict, buffers: list) -> any:
user_expressions = payload.get("user_expressions") or {}
glbls = payload | {"buffers": buffers}
if code:
- exec(code, glbls)
+ exec(code, glbls) # noqa: S102
if user_expressions:
results = {}
for name, expression in user_expressions.items():
- result = eval(expression, glbls)
+ result = eval(expression, glbls) # noqa: S307
if callable(result):
result = result()
if inspect.isawaitable(result):
diff --git a/ipylab/jupyterfrontend_subsection.py b/ipylab/jupyterfrontend_subsection.py
index 3e2a13a..c863905 100644
--- a/ipylab/jupyterfrontend_subsection.py
+++ b/ipylab/jupyterfrontend_subsection.py
@@ -2,12 +2,16 @@
# Distributed under the terms of the Modified BSD License.
from __future__ import annotations
-import asyncio
-from collections.abc import Callable, Coroutine
+from typing import TYPE_CHECKING
from ipylab import TransformMode
from ipylab.hasapp import HasApp
+if TYPE_CHECKING:
+ import asyncio
+
+ from ipylab.asyncwidget import CallbackType, TransformType
+
class JupyterFrontEndSubsection(HasApp):
"""Use as a sub section in the JupyterFrontEnd class"""
@@ -18,11 +22,7 @@ class JupyterFrontEndSubsection(HasApp):
JFE_JS_SUB_PATH = ""
def executeMethod(
- self,
- method: str,
- *args,
- callback: Callable[[any, any], None | Coroutine] = None,
- transform: TransformMode | dict[str, str] = TransformMode.raw,
+ self, method: str, *args, callback: CallbackType | None = None, transform: TransformType = TransformMode.raw
) -> asyncio.Task:
"""Execute a nested method on this objects JFE_SUB_PATH relative to the instance of the
JupyterFrontEndModel in the JS frontend.
@@ -32,14 +32,11 @@ def executeMethod(
return self.app.executeMethod(method, *args, callback=callback, transform=transform)
def get_attribute(
- self,
- name: str,
- *,
- callback: Callable[[any, any], None | Coroutine] = None,
- transform: TransformMode | dict[str, str] = TransformMode.raw,
+ self, name: str, *, callback: CallbackType | None = None, transform: TransformType = TransformMode.raw
) -> asyncio.Task:
"""A serialized version of the attribute relative to this object."""
- raise NotImplementedError("TODO")
+ msg = "TODO"
+ raise NotImplementedError(msg)
return self.app.get_attribute(
f"{self.JFE_JS_SUB_PATH}.{name}",
callback=callback,
@@ -50,11 +47,10 @@ def list_attributes(
self,
base: str = "",
*,
- callback: Callable[[any, any], None | Coroutine] = None,
- transform: TransformMode | dict[str, str] = TransformMode.raw,
+ callback: CallbackType | None = None,
+ transform: TransformType = TransformMode.raw,
) -> asyncio.Task:
"""Get a list of all attributes"""
- raise NotImplementedError("TODO")
- return self.app.list_attributes(
- f"{self.JFE_JS_SUB_PATH}.{base}", callback=callback, transform=transform
- )
+ msg = "TODO"
+ raise NotImplementedError(msg)
+ return self.app.list_attributes(f"{self.JFE_JS_SUB_PATH}.{base}", callback=callback, transform=transform)
diff --git a/ipylab/main_area.py b/ipylab/main_area.py
index 52b2c10..0493ee2 100644
--- a/ipylab/main_area.py
+++ b/ipylab/main_area.py
@@ -3,22 +3,20 @@
from __future__ import annotations
-import asyncio
import pathlib
-import sys
+from enum import StrEnum
+from typing import TYPE_CHECKING, ClassVar
from ipywidgets import register
-from traitlets import Instance, Unicode, UseEnum, observe, validate
+from traitlets import Instance, TraitType, Unicode, UseEnum, observe, validate
from ipylab.asyncwidget import AsyncWidgetBase, pack, widget_serialization
from ipylab.hasapp import HasApp
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
+if TYPE_CHECKING:
+ import asyncio
class ViewStatus(StrEnum):
@@ -35,47 +33,48 @@ class MainArea(AsyncWidgetBase, HasApp):
Also provides methods to open/close a console using the context of the loaded widget.
"""
- _main_area_names: dict[str, MainArea] = {}
+ _main_area_names: ClassVar[dict[str, MainArea]] = {}
_model_name = Unicode("MainAreaModel").tag(sync=True)
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: ViewStatus = UseEnum(ViewStatus, read_only=True).tag(sync=True)
- console_status: ViewStatus = UseEnum(ViewStatus, read_only=True).tag(sync=True)
+ 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):
- raise RuntimeError(f"Changing the value of {trait=} is not allowed!")
+ msg = f"Changing the value of {trait=} is not allowed!"
+ raise RuntimeError(msg)
value = proposal["value"]
if value != value.strip():
- raise ValueError(f"Leading/trailing whitespace is not allowed for {trait}: '{value}'")
+ msg = f"Leading/trailing whitespace is not allowed for {trait}: '{value}'"
+ raise ValueError(msg)
return value
@observe("closed")
- def _observe_closed(self, change):
+ def _observe_closed(self, _):
if self.closed:
self.set_trait("status", ViewStatus.unloaded)
self.set_trait("console_status", ViewStatus.unloaded)
- def __new__(cls, *, name: str, model_id=None, content: Panel = None, **kwgs):
+ def __new__(cls, *, name: str, model_id=None, content: Panel | None = None, **kwgs): # noqa: ARG003
if not name:
- raise (ValueError("name not supplied"))
+ msg = "name not supplied"
+ raise (ValueError(msg))
if name in cls._main_area_names:
return cls._main_area_names[name]
- inst = super().__new__(cls, name=name, **kwgs)
- return inst
+ return super().__new__(cls, name=name, **kwgs)
- def __init__(self, *, name: str, path="", model_id=None, content: Panel = None, **kwgs):
+ def __init__(self, *, name: str, path="", model_id=None, content: Panel | None = None, **kwgs):
if self._model_id:
return
path_ = str(pathlib.PurePosixPath(path or name)).lower().strip("/")
if path and path != path_:
- raise ValueError(
- f"`path` must be lowercase and not start/finish with '/' but got '{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:
@@ -89,8 +88,8 @@ def close(self):
def load(
self,
*,
- content: Panel = None,
- area: Area = "main",
+ content: Panel | None = None,
+ area: Area = Area.main,
activate: bool = True,
mode: InsertMode = InsertMode.split_right,
rank: int | None = None,
@@ -123,12 +122,7 @@ def load(
return self.schedule_operation(
"load",
area=area,
- options={
- "mode": InsertMode(mode),
- "rank": rank,
- "activate": activate,
- "ref": pack(ref),
- },
+ options={"mode": InsertMode(mode), "rank": rank, "activate": activate, "ref": pack(ref)},
className=class_name,
)
@@ -149,9 +143,7 @@ def load_console(
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
- )
+ return self.schedule_operation("open_console", insertMode=InsertMode(mode), name=name, **kwgs)
def unload_console(self) -> asyncio.Task:
"""Unload the console."""
diff --git a/ipylab/scripts.py b/ipylab/scripts.py
index 92e4fab..cb89255 100644
--- a/ipylab/scripts.py
+++ b/ipylab/scripts.py
@@ -5,7 +5,7 @@
import sys
-def init_ipylab_backend() -> str:
+def init_ipylab_backend():
"""Initialize an ipylab backend.
Intended to run inside a kenrnel launched by Jupyter.
@@ -13,7 +13,7 @@ def init_ipylab_backend() -> str:
from ipylab.jupyterfrontend import JupyterFrontEnd
app = JupyterFrontEnd()
- return app._init_python_backend()
+ return app._init_python_backend() # noqa: SLF001
def launch_jupyterlab():
diff --git a/ipylab/sessions.py b/ipylab/sessions.py
index 7941dca..32ff6b7 100644
--- a/ipylab/sessions.py
+++ b/ipylab/sessions.py
@@ -21,4 +21,4 @@ def stopIfNeeded(self, path) -> asyncio.Task:
"""
https://jupyterlab.readthedocs.io/en/latest/api/interfaces/services.Session.IManager.html#stopIfNeeded
"""
- return self.executeMethod("stopIfNeeded", path=path)
+ return self.executeMethod("stopIfNeeded", path)
diff --git a/ipylab/shell.py b/ipylab/shell.py
index 6b33370..46fc24d 100644
--- a/ipylab/shell.py
+++ b/ipylab/shell.py
@@ -2,21 +2,16 @@
# Distributed under the terms of the Modified BSD License.
from __future__ import annotations
-import asyncio
-import sys
import typing as t
-
-import ipywidgets as ipw
+from enum import StrEnum
from ipylab import pack
from ipylab.jupyterfrontend_subsection import JupyterFrontEndSubsection
-if sys.version_info >= (3, 11):
- from enum import StrEnum
-else:
- from backports.strenum import StrEnum
-
if t.TYPE_CHECKING:
+ import asyncio
+
+ import ipywidgets as ipw
from ipywidgets import Widget
@@ -30,8 +25,8 @@ class Area(StrEnum):
right = "right"
header = "header"
top = "top"
- bottom = ("bottom",)
- down = ("down",)
+ bottom = "bottom"
+ down = "down"
menu = "menu"
@@ -65,7 +60,8 @@ class Shell(JupyterFrontEndSubsection):
def addToShell(
self,
widget: Widget,
- area: Area,
+ *,
+ area: Area = Area.main,
activate: bool = True,
mode: InsertMode = InsertMode.split_right,
rank: int | None = None,
diff --git a/ipylab/widgets.py b/ipylab/widgets.py
index 3d11133..8fe4bbb 100644
--- a/ipylab/widgets.py
+++ b/ipylab/widgets.py
@@ -3,18 +3,21 @@
from __future__ import annotations
-import asyncio
+from typing import TYPE_CHECKING
import ipywidgets as ipw
from ipywidgets import Box, DOMWidget, register, widget_serialization
from ipywidgets.widgets.trait_types import InstanceDict
-from traitlets import Bool, Dict, Unicode
+from traitlets import Bool, Dict, Instance, Unicode
import ipylab._frontend as _fe
from ipylab.asyncwidget import WidgetBase
from ipylab.hasapp import HasApp
from ipylab.shell import Area, InsertMode
+if TYPE_CHECKING:
+ import asyncio
+
@register
class Icon(DOMWidget, WidgetBase):
@@ -37,7 +40,7 @@ class Title(WidgetBase):
dataset = Dict().tag(sync=True)
icon_label = Unicode().tag(sync=True)
# Widgets
- icon: Icon = InstanceDict(Icon, allow_none=True).tag(sync=True, **widget_serialization)
+ icon: Instance[Icon] = InstanceDict(Icon, allow_none=True).tag(sync=True, **widget_serialization)
@register
@@ -49,7 +52,7 @@ class Panel(Box, HasApp):
_model_name = Unicode("PanelModel").tag(sync=True)
_view_name = Unicode("PanelView").tag(sync=True)
- title: Title = InstanceDict(Title, ()).tag(sync=True, **widget_serialization)
+ title: Instance[Title] = InstanceDict(Title, ()).tag(sync=True, **widget_serialization)
class_name = Unicode("ipylab-panel").tag(sync=True)
_comm = None
closed = Bool(read_only=True).tag(sync=True)
@@ -64,10 +67,12 @@ def close(self) -> None:
def _check_closed(self):
if self.closed:
- raise RuntimeError(f"This object is closed {self}")
+ msg = f"This object is closed {self}"
+ raise RuntimeError(msg)
def addToShell(
self,
+ *,
area: Area = Area.main,
mode: InsertMode = InsertMode.split_right,
activate: bool = True,
@@ -89,13 +94,7 @@ def addToShell(
"""
self._check_closed()
return self.app.shell.addToShell(
- self,
- Area(area),
- mode=InsertMode(mode),
- activate=activate,
- rank=rank,
- ref=ref,
- **options,
+ self, area=Area(area), mode=InsertMode(mode), activate=activate, rank=rank, ref=ref, **options
)
diff --git a/pyproject.toml b/pyproject.toml
index 95a1281..1417f91 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -1,7 +1,7 @@
[build-system]
requires = [
"hatchling>=1.5.0",
- "jupyterlab>=4.0.0,<5",
+ "jupyterlab>=4.1.0,<5",
"hatch-nodejs-version>=0.3.2",
]
build-backend = "hatchling.build"
@@ -10,7 +10,7 @@ build-backend = "hatchling.build"
name = "ipylab"
readme = "README.md"
license = { file = "LICENSE" }
-requires-python = ">=3.10"
+requires-python = ">=3.11"
classifiers = [
"Framework :: Jupyter",
"Framework :: Jupyter :: JupyterLab",
@@ -25,13 +25,7 @@ classifiers = [
]
dynamic = ["version", "description", "authors", "urls", "keywords"]
-dependencies = [
- "jupyterlab>=4.1.0",
- "ipywidgets>=8.1.2,<9",
- "pluggy>=1.1",
- 'backports.strenum;python_version<"3.11"',
- 'typing-extensions>=4.9.0',
-]
+dependencies = ["jupyterlab>=4.1.0", "ipywidgets>=8.1.2,<9", "pluggy>=1.1"]
[project.optional-dependencies]
dev = ["hatch", "ruff", "pre-commit"]
@@ -79,6 +73,9 @@ npm = ["jlpm"]
source_dir = "src"
build_dir = "ipylab/labextension"
+[tool.hatch.envs.hatch-static-analysis]
+config-path = "ruff_defaults.toml"
+
[tool.jupyter-releaser.options]
version_cmd = "hatch version"
@@ -94,10 +91,9 @@ before-build-python = ["jlpm clean:all"]
ignore = ["W002"]
[tool.ruff]
-target-version = "py310"
-line-length = 100
-fix-only = true
-extend-include = ["*.ipynb"]
+extend = "ruff_defaults.toml"
+target-version = "py311"
+
[tool.codespell]
skip = 'yarn.lock,node_modules*,lib,.yarn*,./ipylab*'
@@ -106,27 +102,11 @@ write = true
[tool.ruff.lint]
extend-select = [
- "B", # flake8-bugbear
- "I", # isort
- "ARG", # flake8-unused-arguments
- "C4", # flake8-comprehensions
- "EM", # flake8-errmsg
- "ICN", # flake8-import-conventions
- "G", # flake8-logging-format
- "PGH", # pygrep-hooks
- "PIE", # flake8-pie
- "PL", # pylint
- "PT", # flake8-pytest-style
"PTH", # flake8-use-pathlib
- "RET", # flake8-return
- "RUF", # Ruff-specific
- "SIM", # flake8-simplify
- "T20", # flake8-print
- "UP", # pyupgrade
- "YTT", # flake8-2020
- "EXE", # flake8-executable
"NPY", # NumPy specific rules
"PD", # pandas-vet
"FURB", # refurb
- "PYI", # flake8-pyi
]
+ignore = ["BLE001", "N802", "N803"]
+[tool.ruff.format]
+docstring-code-format = true
diff --git a/ruff_defaults.toml b/ruff_defaults.toml
new file mode 100644
index 0000000..0afcd93
--- /dev/null
+++ b/ruff_defaults.toml
@@ -0,0 +1,525 @@
+line-length = 120
+
+[format]
+docstring-code-format = true
+docstring-code-line-length = 80
+
+[lint]
+select = [
+ "A001",
+ "A002",
+ "A003",
+ "ARG001",
+ "ARG002",
+ "ARG003",
+ "ARG004",
+ "ARG005",
+ "ASYNC100",
+ "ASYNC101",
+ "ASYNC102",
+ "B002",
+ "B003",
+ "B004",
+ "B005",
+ "B006",
+ "B007",
+ "B008",
+ "B009",
+ "B010",
+ "B011",
+ "B012",
+ "B013",
+ "B014",
+ "B015",
+ "B016",
+ "B017",
+ "B018",
+ "B019",
+ "B020",
+ "B021",
+ "B022",
+ "B023",
+ "B024",
+ "B025",
+ "B026",
+ "B028",
+ "B029",
+ "B030",
+ "B031",
+ "B032",
+ "B033",
+ "B034",
+ "B904",
+ "B905",
+ "BLE001",
+ "C400",
+ "C401",
+ "C402",
+ "C403",
+ "C404",
+ "C405",
+ "C406",
+ "C408",
+ "C409",
+ "C410",
+ "C411",
+ "C413",
+ "C414",
+ "C415",
+ "C416",
+ "C417",
+ "C418",
+ "C419",
+ "COM818",
+ "DTZ001",
+ "DTZ002",
+ "DTZ003",
+ "DTZ004",
+ "DTZ005",
+ "DTZ006",
+ "DTZ007",
+ "DTZ011",
+ "DTZ012",
+ "E101",
+ "E401",
+ "E402",
+ "E501",
+ "E701",
+ "E702",
+ "E703",
+ "E711",
+ "E712",
+ "E713",
+ "E714",
+ "E721",
+ "E722",
+ "E731",
+ "E741",
+ "E742",
+ "E743",
+ "E902",
+ "E999",
+ "EM101",
+ "EM102",
+ "EM103",
+ "EXE001",
+ "EXE002",
+ "EXE003",
+ "EXE004",
+ "EXE005",
+ "F401",
+ "F402",
+ "F403",
+ "F404",
+ "F405",
+ "F406",
+ "F407",
+ "F501",
+ "F502",
+ "F503",
+ "F504",
+ "F505",
+ "F506",
+ "F507",
+ "F508",
+ "F509",
+ "F521",
+ "F522",
+ "F523",
+ "F524",
+ "F525",
+ "F541",
+ "F601",
+ "F602",
+ "F621",
+ "F622",
+ "F631",
+ "F632",
+ "F633",
+ "F634",
+ "F701",
+ "F702",
+ "F704",
+ "F706",
+ "F707",
+ "F722",
+ "F811",
+ "F821",
+ "F822",
+ "F823",
+ "F841",
+ "F842",
+ "F901",
+ "FA100",
+ "FA102",
+ "FBT001",
+ "FBT002",
+ "FLY002",
+ "G001",
+ "G002",
+ "G003",
+ "G004",
+ "G010",
+ "G101",
+ "G201",
+ "G202",
+ "I001",
+ "I002",
+ "ICN001",
+ "ICN002",
+ "ICN003",
+ "INP001",
+ "INT001",
+ "INT002",
+ "INT003",
+ "ISC003",
+ "N801",
+ "N802",
+ "N803",
+ "N804",
+ "N805",
+ "N806",
+ "N807",
+ "N811",
+ "N812",
+ "N813",
+ "N814",
+ "N815",
+ "N816",
+ "N817",
+ "N818",
+ "N999",
+ "PERF101",
+ "PERF102",
+ "PERF401",
+ "PERF402",
+ "PGH001",
+ "PGH002",
+ "PGH005",
+ "PIE790",
+ "PIE794",
+ "PIE796",
+ "PIE800",
+ "PIE804",
+ "PIE807",
+ "PIE808",
+ "PIE810",
+ "PLC0105",
+ "PLC0131",
+ "PLC0132",
+ "PLC0205",
+ "PLC0208",
+ "PLC0414",
+ "PLC3002",
+ "PLE0100",
+ "PLE0101",
+ "PLE0116",
+ "PLE0117",
+ "PLE0118",
+ "PLE0241",
+ "PLE0302",
+ "PLE0307",
+ "PLE0604",
+ "PLE0605",
+ "PLE1142",
+ "PLE1205",
+ "PLE1206",
+ "PLE1300",
+ "PLE1307",
+ "PLE1310",
+ "PLE1507",
+ "PLE1700",
+ "PLE2502",
+ "PLE2510",
+ "PLE2512",
+ "PLE2513",
+ "PLE2514",
+ "PLE2515",
+ "PLR0124",
+ "PLR0133",
+ "PLR0206",
+ "PLR0402",
+ "PLR1701",
+ "PLR1711",
+ "PLR1714",
+ "PLR1722",
+ "PLR2004",
+ "PLR5501",
+ "PLW0120",
+ "PLW0127",
+ "PLW0129",
+ "PLW0131",
+ "PLW0406",
+ "PLW0602",
+ "PLW0603",
+ "PLW0711",
+ "PLW1508",
+ "PLW1509",
+ "PLW1510",
+ "PLW2901",
+ "PLW3301",
+ "PT001",
+ "PT002",
+ "PT003",
+ "PT006",
+ "PT007",
+ "PT008",
+ "PT009",
+ "PT010",
+ "PT011",
+ "PT012",
+ "PT013",
+ "PT014",
+ "PT015",
+ "PT016",
+ "PT017",
+ "PT018",
+ "PT019",
+ "PT020",
+ "PT021",
+ "PT022",
+ "PT023",
+ "PT024",
+ "PT025",
+ "PT026",
+ "PT027",
+ "PYI001",
+ "PYI002",
+ "PYI003",
+ "PYI004",
+ "PYI005",
+ "PYI006",
+ "PYI007",
+ "PYI008",
+ "PYI009",
+ "PYI010",
+ "PYI011",
+ "PYI012",
+ "PYI013",
+ "PYI014",
+ "PYI015",
+ "PYI016",
+ "PYI017",
+ "PYI018",
+ "PYI019",
+ "PYI020",
+ "PYI021",
+ "PYI024",
+ "PYI025",
+ "PYI026",
+ "PYI029",
+ "PYI030",
+ "PYI032",
+ "PYI033",
+ "PYI034",
+ "PYI035",
+ "PYI036",
+ "PYI041",
+ "PYI042",
+ "PYI043",
+ "PYI044",
+ "PYI045",
+ "PYI046",
+ "PYI047",
+ "PYI048",
+ "PYI049",
+ "PYI050",
+ "PYI051",
+ "PYI052",
+ "PYI053",
+ "PYI054",
+ "PYI055",
+ "PYI056",
+ "RET503",
+ "RET504",
+ "RET505",
+ "RET506",
+ "RET507",
+ "RET508",
+ "RSE102",
+ "RUF001",
+ "RUF002",
+ "RUF003",
+ "RUF005",
+ "RUF006",
+ "RUF007",
+ "RUF008",
+ "RUF009",
+ "RUF010",
+ "RUF011",
+ "RUF012",
+ "RUF013",
+ "RUF015",
+ "RUF016",
+ "RUF100",
+ "RUF200",
+ "S101",
+ "S102",
+ "S103",
+ "S104",
+ "S105",
+ "S106",
+ "S107",
+ "S108",
+ "S110",
+ "S112",
+ "S113",
+ "S301",
+ "S302",
+ "S303",
+ "S304",
+ "S305",
+ "S306",
+ "S307",
+ "S308",
+ "S310",
+ "S311",
+ "S312",
+ "S313",
+ "S314",
+ "S315",
+ "S316",
+ "S317",
+ "S318",
+ "S319",
+ "S320",
+ "S321",
+ "S323",
+ "S324",
+ "S501",
+ "S506",
+ "S508",
+ "S509",
+ "S601",
+ "S602",
+ "S604",
+ "S605",
+ "S606",
+ "S607",
+ "S608",
+ "S609",
+ "S612",
+ "S701",
+ "SIM101",
+ "SIM102",
+ "SIM103",
+ "SIM105",
+ "SIM107",
+ "SIM108",
+ "SIM109",
+ "SIM110",
+ "SIM112",
+ "SIM114",
+ "SIM115",
+ "SIM116",
+ "SIM117",
+ "SIM118",
+ "SIM201",
+ "SIM202",
+ "SIM208",
+ "SIM210",
+ "SIM211",
+ "SIM212",
+ "SIM220",
+ "SIM221",
+ "SIM222",
+ "SIM223",
+ "SIM300",
+ "SIM910",
+ "SLF001",
+ "SLOT000",
+ "SLOT001",
+ "SLOT002",
+ "T100",
+ "T201",
+ "T203",
+ "TCH001",
+ "TCH002",
+ "TCH003",
+ "TCH004",
+ "TCH005",
+ "TD004",
+ "TD005",
+ "TD006",
+ "TD007",
+ "TID251",
+ "TID252",
+ "TID253",
+ "TRY002",
+ "TRY003",
+ "TRY004",
+ "TRY200",
+ "TRY201",
+ "TRY300",
+ "TRY301",
+ "TRY302",
+ "TRY400",
+ "TRY401",
+ "UP001",
+ "UP003",
+ "UP004",
+ "UP005",
+ "UP006",
+ "UP007",
+ "UP008",
+ "UP009",
+ "UP010",
+ "UP011",
+ "UP012",
+ "UP013",
+ "UP014",
+ "UP015",
+ "UP017",
+ "UP018",
+ "UP019",
+ "UP020",
+ "UP021",
+ "UP022",
+ "UP023",
+ "UP024",
+ "UP025",
+ "UP026",
+ "UP027",
+ "UP028",
+ "UP029",
+ "UP030",
+ "UP031",
+ "UP032",
+ "UP033",
+ "UP034",
+ "UP035",
+ "UP036",
+ "UP037",
+ "UP038",
+ "UP039",
+ "UP040",
+ "W291",
+ "W292",
+ "W293",
+ "W505",
+ "W605",
+ "YTT101",
+ "YTT102",
+ "YTT103",
+ "YTT201",
+ "YTT202",
+ "YTT203",
+ "YTT204",
+ "YTT301",
+ "YTT302",
+ "YTT303",
+]
+
+[lint.per-file-ignores]
+"**/scripts/*" = ["INP001", "T201"]
+"**/tests/**/*" = ["PLC1901", "PLR2004", "PLR6301", "S", "TID252"]
+
+[lint.flake8-tidy-imports]
+ban-relative-imports = "all"
+
+[lint.isort]
+known-first-party = ["ipylab"]
+
+[lint.flake8-pytest-style]
+fixture-parentheses = false
+mark-parentheses = false