Skip to content

Commit

Permalink
General bugfixes.
Browse files Browse the repository at this point in the history
  • Loading branch information
Alan Fleming committed Nov 28, 2024
1 parent 0480752 commit e6e674c
Show file tree
Hide file tree
Showing 7 changed files with 78 additions and 42 deletions.
10 changes: 6 additions & 4 deletions examples/plugins.ipynb
Original file line number Diff line number Diff line change
Expand Up @@ -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. "
]
},
Expand Down Expand Up @@ -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",
Expand Down Expand Up @@ -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",
Expand Down
5 changes: 4 additions & 1 deletion ipylab/code_editor.py
Original file line number Diff line number Diff line change
Expand Up @@ -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):
Expand All @@ -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
Expand Down
7 changes: 5 additions & 2 deletions ipylab/commands.py
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
6 changes: 3 additions & 3 deletions ipylab/hookspecs.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@

from __future__ import annotations

from typing import TYPE_CHECKING
from typing import TYPE_CHECKING, Any

import pluggy

Expand Down Expand Up @@ -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.
Expand Down
65 changes: 40 additions & 25 deletions ipylab/jupyterfrontend.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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(
Expand Down Expand Up @@ -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")
Expand Down Expand Up @@ -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."
Expand Down
9 changes: 8 additions & 1 deletion ipylab/lib.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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}
18 changes: 12 additions & 6 deletions src/widgets/code_editor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand Down Expand Up @@ -211,13 +208,22 @@ export class CodeEditorView extends DOMWidgetView {
this._disposeCompleter = () => {
model.dispose();
completer.dispose();
this.handler?.dispose();
delete this.handler;
this.cmdInvoke?.dispose();
this.kbInvoke?.dispose();
};
}
this._updateCommands();
}

disposeCompleter() {
if (this._disposeCompleter) {
this._disposeCompleter();
delete this._disposeCompleter;
}
}

_updateCommands() {
// Add the commands.
this.cmdInvoke?.dispose();
Expand Down

0 comments on commit e6e674c

Please sign in to comment.