From e6e674cbe7b6471a409609a67122eeb49cd4246e Mon Sep 17 00:00:00 2001 From: Alan Fleming <> Date: Thu, 28 Nov 2024 12:49:27 +1100 Subject: [PATCH] General bugfixes. --- examples/plugins.ipynb | 10 +++--- ipylab/code_editor.py | 5 ++- ipylab/commands.py | 7 ++-- ipylab/hookspecs.py | 6 ++-- ipylab/jupyterfrontend.py | 65 +++++++++++++++++++++++--------------- ipylab/lib.py | 9 +++++- src/widgets/code_editor.ts | 18 +++++++---- 7 files changed, 78 insertions(+), 42 deletions(-) diff --git a/examples/plugins.ipynb b/examples/plugins.ipynb index 4b2dc36..e981b6e 100644 --- a/examples/plugins.ipynb +++ b/examples/plugins.ipynb @@ -75,7 +75,7 @@ "\n", "The following example demonstrates a few plugins:\n", "1. `autostart` This is a 'historic' plugin and runs once the app is 'ready'. \n", - "2. `namespace_objects` This allows for modify the namespace feature implemented in ipylab.\n", + "2. `default_namespace_objects` This allows for modify the namespace feature implemented in ipylab.\n", "3. `ready` This hook is called for each Ipylab instance when it is ready. " ] }, @@ -120,7 +120,7 @@ "\n", "# Now lets load a different namespace is available.\n", "app.activate_namespace('test')\n", - "test # Should print 'Test' as defined in the 'namespace_objects' plugin.\n", + "test # Should print 'Test' as defined in the 'default_namespace_objects' plugin.\n", "dir()\n", "\n", "# To switch back use\n", @@ -159,9 +159,11 @@ " # Add a plugin in this kernel. Instead of defining a class, you can also define a module eg: 'ipylab.lib.py'\n", " class MyLocalPlugin:\n", " @ipylab.hookimpl\n", - " def namespace_objects(self, objects: dict, namespace_name: str, app: ipylab.App): # noqa: ARG002\n", + " def default_namespace_objects(self, namespace_name: str, app: ipylab.App):\n", " if namespace_name == \"test\":\n", - " objects[\"test\"] = \"TEST\"\n", + " # Define alternate default objects for this namespace\n", + " return {\"test\": \"TEST\", \"app\": app, \"ipylab\": ipylab}\n", + " return {}\n", "\n", " @ipylab.hookimpl\n", " def ready(self, obj: ipylab.Ipylab):\n", diff --git a/ipylab/code_editor.py b/ipylab/code_editor.py index b762a61..9da1ec0 100644 --- a/ipylab/code_editor.py +++ b/ipylab/code_editor.py @@ -41,6 +41,7 @@ class CodeEditor(DOMWidget, Ipylab): mime_type = Unicode("text/plain", help="syntax style").tag(sync=True) key_bindings = Dict().tag(sync=True) namespace_name = Unicode("").tag(sync=True) + _cr_name: str | None = None @default("key_bindings") def _default_key_bindings(self): @@ -64,7 +65,9 @@ async def _do_operation_for_frontend(self, operation: str, payload: dict, buffer async def _complete_request(self, code: str, cursor_pos: int): """Handle a completion request.""" - ipylab.app.activate_namespace(self.namespace_name) + if self._cr_name != self.namespace_name: + ipylab.app.activate_namespace(self.namespace_name) + self._cr_name = self.namespace_name matches = self.comm.kernel.do_complete(code, cursor_pos) # type: ignore if inspect.isawaitable(matches): matches = await matches diff --git a/ipylab/commands.py b/ipylab/commands.py index 1244dcd..c86794e 100644 --- a/ipylab/commands.py +++ b/ipylab/commands.py @@ -233,8 +233,11 @@ async def _execute_for_frontend(self, payload: dict, buffers: list): kwgs = {} for n, p in inspect.signature(cmd).parameters.items(): if n in ["current_widget", "ref"]: - kwgs[n] = ShellConnection(cids[n]) if n in cids else None - await kwgs[n].ready() + if cid := cids.get(n, ""): + kwgs[n] = ShellConnection(cid) + await kwgs[n].ready() + else: + kwgs[n] = None elif n in args: kwgs[n] = args[n] elif n in glbls: diff --git a/ipylab/hookspecs.py b/ipylab/hookspecs.py index 493e877..df7c607 100644 --- a/ipylab/hookspecs.py +++ b/ipylab/hookspecs.py @@ -3,7 +3,7 @@ from __future__ import annotations -from typing import TYPE_CHECKING +from typing import TYPE_CHECKING, Any import pluggy @@ -45,8 +45,8 @@ async def autostart(app: ipylab.App) -> None | Awaitable[None]: """ -@hookspec(firstresult=True) -def namespace_objects(objects: dict, namespace_name: str, app: ipylab.App) -> None: +@hookspec +def default_namespace_objects(namespace_name: str, app: ipylab.App) -> dict[str, Any]: # type: ignore """ Called when loading a namespace. diff --git a/ipylab/jupyterfrontend.py b/ipylab/jupyterfrontend.py index bc9fdfe..40f223f 100644 --- a/ipylab/jupyterfrontend.py +++ b/ipylab/jupyterfrontend.py @@ -6,9 +6,8 @@ import functools import inspect from collections import OrderedDict -from typing import TYPE_CHECKING, Any, Unpack +from typing import TYPE_CHECKING, Any, Literal, Unpack -import ipywidgets from IPython.core.getipython import get_ipython from ipywidgets import Widget, register from traitlets import Bool, Container, Dict, Instance, Unicode, UseEnum, default, observe @@ -40,10 +39,25 @@ class LastUpdatedDict(OrderedDict): "Store items in the order the keys were last added" # ref: https://docs.python.org/3/library/collections.html#ordereddict-examples-and-recipes + _updating = False + _last = True def __setitem__(self, key, value): super().__setitem__(key, value) - self.move_to_end(key) + if not self._updating: + self.move_to_end(key, self._last) + + @override + def update(self, m, **kwargs): + self._updating = True + try: + super().update(m, **kwargs) + finally: + self._updating = False + + def set_end(self, mode: Literal["first", "last"] = "last"): + "The end to move the last updated key." + self._last = mode == "last" @register @@ -78,7 +92,7 @@ class App(Ipylab): namespaces: Container[dict[str, LastUpdatedDict]] = Dict(read_only=True) # type: ignore _ipy_shell = get_ipython() - _ipy_default_namespace: ClassVar = getattr(_ipy_shell, "user_ns", {}) + _hidden: ClassVar = {"_ih", "_oh", "_dh", "In", "Out", "get_ipython", "exit", "quit", "open"} @classmethod @override @@ -178,8 +192,6 @@ async def _evaluate(self, options: dict, buffers: list): buffers = glbls.pop("buffers", []) if namespace_name == self.active_namespace: self.activate_namespace(namespace_name) - else: - self.get_namespace(namespace_name, glbls) return {"payload": glbls.get("payload"), "buffers": buffers} def _context_open_console( @@ -220,12 +232,12 @@ async def _open_console(): ) for result in plugin_results: self.ensure_run(result) - self.activate_namespace(args.pop("namespace_name", ""), objects=objects) - + namespace_name_ = args.pop("namespace_name", "") conn: ShellConnection = await self.commands.execute("console:open", args, **kwargs) conn.add_as_trait(self, "console") if objects and (ref := objects.get("ref")) and isinstance(ref.widget, ipylab.Panel): conn.add_as_trait(ref.widget, "console") + self.activate_namespace(namespace_name_, objects=objects) return conn return self.to_task(_open_console(), "Open console") @@ -280,28 +292,31 @@ def evaluate( def get_namespace(self, name="", objects: dict | None = None): "Get the 'globals' namespace stored for name." - if self._ipy_shell: - if "" not in self.namespaces: - self.namespaces[""] = LastUpdatedDict(self._ipy_shell.user_ns) - if self.active_namespace == name: - self.namespaces.update(self._ipy_shell.user_ns) + sh = self._ipy_shell + if sh and "" not in self.namespaces: + self._init_namespace(name, sh.user_ns) if name not in self.namespaces: - self.namespaces[name] = LastUpdatedDict(self._ipy_default_namespace) - objects = {"ipylab": ipylab, "ipywidgets": ipywidgets, "ipw": ipywidgets, "app": self} | (objects or {}) - ipylab.plugin_manager.hook.namespace_objects(objects=objects, namespace_name=name, app=self) - self.namespaces[name].update(objects) - return self.namespaces[name] + self._init_namespace(name, {}) + ns = self.namespaces[name] + for objs in ipylab.plugin_manager.hook.default_namespace_objects(namespace_name=name, app=self): + ns.update(objs) + if objects: + ns.update(objects) + if sh and name == self.activate_namespace: + ns.update(sh.user_ns) + return ns + + def _init_namespace(self, name: str, objs: dict): + self.namespaces[name] = LastUpdatedDict(objs) def activate_namespace(self, name="", objects: dict | None = None): "Sets the ipython/console namespace." - if self.active_namespace != name: - if not self._ipy_shell: - msg = "Ipython shell is not loaded!" - raise RuntimeError(msg) - ns = self.get_namespace(name, objects) + ns = self.get_namespace(name, objects) + if self._ipy_shell: self._ipy_shell.reset() - self._ipy_shell.push(ns) - self.set_trait("active_namespace", name) + self._ipy_shell.push({k: v for k, v in ns.items() if k not in self._hidden}) + self.set_trait("active_namespace", name) + return ns def reset_namespace(self, name: str, *, activate=True, objects: dict | None = None): "Reset the namespace to default. If activate is False it won't be created." diff --git a/ipylab/lib.py b/ipylab/lib.py index a06ebe3..b7ce640 100644 --- a/ipylab/lib.py +++ b/ipylab/lib.py @@ -5,12 +5,14 @@ from typing import TYPE_CHECKING +import ipywidgets + +import ipylab from ipylab.common import IpylabKwgs, hookimpl if TYPE_CHECKING: from collections.abc import Awaitable - import ipylab from ipylab import App from ipylab.ipylab import Ipylab from ipylab.log import IpylabLogHandler @@ -61,3 +63,8 @@ def get_log_viewer(app: App, handler: IpylabLogHandler): # type: ignore @hookimpl def default_editor_key_bindings(app: ipylab.App, obj: ipylab.CodeEditor): # noqa: ARG001 return {"invoke_completer": ["Ctrl Space"], "evaluate": ["Shift Enter"]} + + +@hookimpl +def default_namespace_objects(namespace_name: str, app: ipylab.App): + return {"ipylab": ipylab, "ipw": ipywidgets, "app": app, "namespace_name": namespace_name} diff --git a/src/widgets/code_editor.ts b/src/widgets/code_editor.ts index 571f015..d8edfb0 100644 --- a/src/widgets/code_editor.ts +++ b/src/widgets/code_editor.ts @@ -150,15 +150,12 @@ export class CodeEditorView extends DOMWidgetView { remove() { this.model.off('change:mimeType', this.updateCompleter, this); this.model.off('change:completer_invoke_keys', this.updateCompleter, this); - this?._disposeCompleter(); + this.disposeCompleter(); super.remove(); } private updateCompleter() { - if ( - ['text/x-python', 'text/x-ipython'].includes( - this.model.editorModel.mimeType - ) - ) { + if (!this.model.editorModel.mimeType.toLowerCase().includes('python')) { + this.disposeCompleter(); return; } this._updateCompleter(); @@ -211,6 +208,8 @@ export class CodeEditorView extends DOMWidgetView { this._disposeCompleter = () => { model.dispose(); completer.dispose(); + this.handler?.dispose(); + delete this.handler; this.cmdInvoke?.dispose(); this.kbInvoke?.dispose(); }; @@ -218,6 +217,13 @@ export class CodeEditorView extends DOMWidgetView { this._updateCommands(); } + disposeCompleter() { + if (this._disposeCompleter) { + this._disposeCompleter(); + delete this._disposeCompleter; + } + } + _updateCommands() { // Add the commands. this.cmdInvoke?.dispose();