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