From 0b799754a3622403d5d8c42831ba83d83cdfe683 Mon Sep 17 00:00:00 2001
From: Alan Fleming <>
Date: Wed, 4 Dec 2024 17:59:16 +1100
Subject: [PATCH] Tweaks to SimpleOutput
---
examples/simple_output.ipynb | 69 +++++++++++++++++++-----------------
ipylab/code_editor.py | 7 ++--
ipylab/dialog.py | 28 ++++++++++-----
ipylab/jupyterfrontend.py | 13 ++++---
ipylab/log_viewer.py | 5 +--
ipylab/menu.py | 2 +-
ipylab/simple_output.py | 41 ++++++++++++---------
ipylab/widgets.py | 2 +-
8 files changed, 99 insertions(+), 68 deletions(-)
diff --git a/examples/simple_output.ipynb b/examples/simple_output.ipynb
index 4ba824d..3d18830 100644
--- a/examples/simple_output.ipynb
+++ b/examples/simple_output.ipynb
@@ -122,6 +122,28 @@
"cell_type": "markdown",
"id": "11",
"metadata": {},
+ "source": [
+ "### Other formats are also supported"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "id": "12",
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "from IPython.display import HTML, Markdown\n",
+ "\n",
+ "so = SimpleOutput()\n",
+ "so.push(Markdown(\"## Markdown\"), HTML(\"
HTML
\"))\n",
+ "so"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "13",
+ "metadata": {},
"source": [
"### set\n",
"\n",
@@ -131,7 +153,7 @@
{
"cell_type": "code",
"execution_count": null,
- "id": "12",
+ "id": "14",
"metadata": {},
"outputs": [],
"source": [
@@ -141,7 +163,7 @@
{
"cell_type": "code",
"execution_count": null,
- "id": "13",
+ "id": "15",
"metadata": {},
"outputs": [],
"source": [
@@ -151,7 +173,7 @@
{
"cell_type": "code",
"execution_count": null,
- "id": "14",
+ "id": "16",
"metadata": {},
"outputs": [],
"source": [
@@ -161,7 +183,7 @@
},
{
"cell_type": "markdown",
- "id": "15",
+ "id": "17",
"metadata": {},
"source": [
"## max_continuous_streams\n",
@@ -176,7 +198,7 @@
{
"cell_type": "code",
"execution_count": null,
- "id": "16",
+ "id": "18",
"metadata": {},
"outputs": [],
"source": [
@@ -187,7 +209,7 @@
{
"cell_type": "code",
"execution_count": null,
- "id": "17",
+ "id": "19",
"metadata": {},
"outputs": [],
"source": [
@@ -197,7 +219,7 @@
{
"cell_type": "code",
"execution_count": null,
- "id": "18",
+ "id": "20",
"metadata": {},
"outputs": [],
"source": [
@@ -207,7 +229,7 @@
},
{
"cell_type": "markdown",
- "id": "19",
+ "id": "21",
"metadata": {},
"source": [
"# AutoScroll\n",
@@ -221,7 +243,7 @@
},
{
"cell_type": "markdown",
- "id": "20",
+ "id": "22",
"metadata": {},
"source": [
"## Builtin log viewer\n",
@@ -232,27 +254,18 @@
{
"cell_type": "code",
"execution_count": null,
- "id": "21",
- "metadata": {},
- "outputs": [],
- "source": []
- },
- {
- "cell_type": "code",
- "execution_count": null,
- "id": "22",
+ "id": "23",
"metadata": {},
"outputs": [],
"source": [
"app = ipylab.app\n",
- "app.log_viewer.add_to_shell()\n",
"app.log_level = \"DEBUG\""
]
},
{
"cell_type": "code",
"execution_count": null,
- "id": "23",
+ "id": "24",
"metadata": {},
"outputs": [],
"source": [
@@ -268,7 +281,7 @@
{
"cell_type": "code",
"execution_count": null,
- "id": "24",
+ "id": "25",
"metadata": {},
"outputs": [],
"source": [
@@ -277,7 +290,7 @@
},
{
"cell_type": "markdown",
- "id": "25",
+ "id": "26",
"metadata": {},
"source": [
"## Example usage"
@@ -286,7 +299,7 @@
{
"cell_type": "code",
"execution_count": null,
- "id": "26",
+ "id": "27",
"metadata": {},
"outputs": [],
"source": [
@@ -301,7 +314,7 @@
{
"cell_type": "code",
"execution_count": null,
- "id": "27",
+ "id": "28",
"metadata": {},
"outputs": [],
"source": [
@@ -358,14 +371,6 @@
")\n",
"p.add_to_shell(mode=ipylab.InsertMode.split_right)"
]
- },
- {
- "cell_type": "code",
- "execution_count": null,
- "id": "28",
- "metadata": {},
- "outputs": [],
- "source": []
}
],
"metadata": {
diff --git a/ipylab/code_editor.py b/ipylab/code_editor.py
index 9da1ec0..7f8db9f 100644
--- a/ipylab/code_editor.py
+++ b/ipylab/code_editor.py
@@ -17,14 +17,17 @@
"text/x-python",
"text/x-ipython",
"text/x-markdown",
- "application/json",
"text/html",
"text/css",
+ "text/csv",
+ "text/yaml",
+ "text/json",
+ "application/json",
)
@register
-class CodeEditor(DOMWidget, Ipylab):
+class CodeEditor(Ipylab, DOMWidget):
"""A Widget for code editing.
Code completion is provided for Python code for the specified namespace.
diff --git a/ipylab/dialog.py b/ipylab/dialog.py
index 99db1bd..4798c4f 100644
--- a/ipylab/dialog.py
+++ b/ipylab/dialog.py
@@ -65,17 +65,28 @@ def show_dialog(
title: str = "",
body: str | Widget = "",
options: dict | None = None,
+ *,
+ has_close=True,
**kwgs: Unpack[IpylabKwgs],
) -> Task[dict[str, Any]]:
"""Jupyter dialog to get user response with custom buttons and checkbox.
- returns {'value':any, 'isChecked':bool|None}
+ returns {'value':any, 'isChecked':bool|None}
- 'value' is the button 'accept' value of the selected button.
+ 'value' is the button 'accept' value of the selected button.
- title: 'Dialog title', // Can be text or a react element
- body: 'Dialog body', // Can be text, a widget or a react element
- specify kwgs passed as below.
+ title: str
+ The dialog title. // Can be text or a react element
+
+ body: str | Widget
+ Text to show in the body or a widget. 'Dialog body', // Can be text, a widget or a react element
+
+ has_close: bool [True]
+ By default (True), clicking outside the dialog will close it.
+ When `False`, the user must specifically cancel or accept a result.
+
+ options:
+ specify options can be passed as below.
buttons: [ // List of buttons
buttons=[
{
@@ -111,15 +122,14 @@ def show_dialog(
},
defaultButton: 0, // Index of the default button
focusNodeSelector: '.my-input', // Selector for focussing an input element when dialog opens
- hasClose: false, // Whether to display a close button or not
renderer: undefined // To define customized dialog structure
- see: https://jupyterlab.readthedocs.io/en/stable/api/functions/apputils.showDialog.html
- source: https://jupyterlab.readthedocs.io/en/stable/extension/ui_helpers.html#generic-dialog
+ see: https://jupyterlab.readthedocs.io/en/stable/api/functions/apputils.showDialog.html
+ source: https://jupyterlab.readthedocs.io/en/stable/extension/ui_helpers.html#generic-dialog
"""
if isinstance(body, Widget) and "toLuminoWidget" not in kwgs:
kwgs["toLuminoWidget"] = ["body"]
- return self.operation("showDialog", _combine(options, title=title, body=body), **kwgs)
+ return self.operation("showDialog", _combine(options, title=title, body=body, hasClose=has_close), **kwgs)
def show_error_message(
self, title: str, error: str, options: dict | None = None, **kwgs: Unpack[IpylabKwgs]
diff --git a/ipylab/jupyterfrontend.py b/ipylab/jupyterfrontend.py
index 40f223f..a931888 100644
--- a/ipylab/jupyterfrontend.py
+++ b/ipylab/jupyterfrontend.py
@@ -36,12 +36,19 @@
class LastUpdatedDict(OrderedDict):
- "Store items in the order the keys were last added"
+ """Store items in the order the keys were last added.
+
+ mode: Literal["first", "last"]
+ The end to shift the last added key."""
# ref: https://docs.python.org/3/library/collections.html#ordereddict-examples-and-recipes
_updating = False
_last = True
+ def __init__(self, *args, mode: Literal["first", "last"] = "last", **kwargs):
+ self._last = mode == "last"
+ super().__init__(*args, **kwargs)
+
def __setitem__(self, key, value):
super().__setitem__(key, value)
if not self._updating:
@@ -55,10 +62,6 @@ def update(self, 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
class App(Ipylab):
diff --git a/ipylab/log_viewer.py b/ipylab/log_viewer.py
index fb8c9f8..277baee 100644
--- a/ipylab/log_viewer.py
+++ b/ipylab/log_viewer.py
@@ -99,8 +99,9 @@ def close(self):
def _add_record(self, record: logging.LogRecord):
self._records.append(record)
- self.output.push(record.output) # type: ignore
- if record.levelno >= LogLevel.ERROR:
+ if self.output._ready: # noqa: SLF001
+ self.output.push(record.output) # type: ignore
+ if record.levelno >= LogLevel.ERROR and ipylab.app._ready: # noqa: SLF001
self._notify_exception(record)
def _notify_exception(self, record: logging.LogRecord):
diff --git a/ipylab/menu.py b/ipylab/menu.py
index 2d9e3d1..dcf52e9 100644
--- a/ipylab/menu.py
+++ b/ipylab/menu.py
@@ -110,7 +110,7 @@ def activate(self):
return ipylab.app.commands.execute(f"{name}:open")
-class MenuConnection(RankedMenu, InfoConnection):
+class MenuConnection(InfoConnection, RankedMenu):
"""A connection to a custom menu"""
commands = Instance(CommandRegistry)
diff --git a/ipylab/simple_output.py b/ipylab/simple_output.py
index c54d26a..290a102 100644
--- a/ipylab/simple_output.py
+++ b/ipylab/simple_output.py
@@ -6,15 +6,16 @@
from typing import TYPE_CHECKING
from ipywidgets import DOMWidget, Widget, register
-from traitlets import Bool, Enum, Int, Unicode
+from traitlets import Bool, Callable, Enum, Int, Unicode, default
-import ipylab
from ipylab.ipylab import Ipylab
if TYPE_CHECKING:
from asyncio import Task
from typing import Unpack
+ from IPython.display import TextDisplayObject
+
from ipylab.common import IpylabKwgs
from typing import TYPE_CHECKING
@@ -29,7 +30,7 @@
@register
-class SimpleOutput(DOMWidget, Ipylab):
+class SimpleOutput(Ipylab, DOMWidget):
"""An output with no prompts designed to accept frequent additions.
The interface differs from Ipywidgets.Output widget in almost every way.
@@ -42,30 +43,38 @@ class SimpleOutput(DOMWidget, Ipylab):
max_outputs = Int(100, help="The maximum number of individual widgets.").tag(sync=True)
max_continuous_streams = Int(100, help="Max streams to put in same output.").tag(sync=True)
length = Int(read_only=True, help="The current length of the output area").tag(sync=True)
+ formatter = Callable(allow_none=True, default_value=None)
- invalid_data_mode = Enum(["raise", "skip"], "raise", help="What to do with invalid output")
+ @default("formatter")
+ def get_display_formatter(self):
+ try:
+ return self.comm.kernel.shell.display_formatter.format # type: ignore
+ except AttributeError:
+ return None
- def _pack_outputs(self, outputs: tuple[dict[str, str] | Widget | str, ...]):
+ def _pack_outputs(self, outputs: tuple[dict[str, str] | Widget | str | TextDisplayObject, ...]):
outputs_ = []
+ fmt = self.formatter
for output in outputs:
if hasattr(output, "_repr_mimebundle_"):
- if not callable(output._repr_mimebundle_): # type: ignore
- if self.invalid_data_mode == "raise":
- msg = f"Invalid data {output}"
- raise TypeError(msg)
- continue
- outputs_.append(output._repr_mimebundle_()) # type: ignore
+ if callable(output._repr_mimebundle_): # type: ignore
+ outputs_.append(output._repr_mimebundle_()) # type: ignore
+ else:
+ self.log.warning("Unable to pack {output}", output=output)
+ continue
if isinstance(output, str):
outputs_.append({"output_type": "stream", "name": "stdout", "text": output})
elif isinstance(output, dict):
outputs_.append(output)
- else:
- data, metadata = ipylab.app._ipy_shell.display_formatter.format(output) # type: ignore # noqa: SLF001
+ elif fmt:
+ data, metadata = fmt(output)
outputs_.append({"output_type": "display_data", "data": data, "metadata": metadata})
+ else:
+ self.log.warning("Unable to pack {output}", output=output)
return outputs_
- def push(self, *outputs: dict[str, str] | Widget | str):
+ def push(self, *outputs: dict[str, str] | Widget | str | TextDisplayObject):
"""Add one or more items to the output.
Consecutive `streams` of the same type are placed in the same 'output'.
Outputs passed as dicts are assumed to be correctly packed as `repr_mime` data.
@@ -79,13 +88,13 @@ def clear(self, *, wait=False):
True: Will delay clearing until next output is added."""
self.send({"clear": wait})
- def set(self, *outputs: dict[str, str] | Widget | str, **kwgs: Unpack[IpylabKwgs]) -> Task[int]:
+ def set(self, *outputs: dict[str, str] | Widget | str | TextDisplayObject, **kwgs: Unpack[IpylabKwgs]) -> Task[int]:
"""Set the output explicitly by first clearing and then adding the outputs."""
return self.operation("setOutputs", {"outputs": self._pack_outputs(outputs)}, **kwgs)
@register
-class AutoScroll(DOMWidget, WidgetBase):
+class AutoScroll(WidgetBase, DOMWidget):
"""An automatic scrolling container.
The content can be changed and the scrolling enabled and disabled on the fly.
diff --git a/ipylab/widgets.py b/ipylab/widgets.py
index 40d8d18..7cb49ae 100644
--- a/ipylab/widgets.py
+++ b/ipylab/widgets.py
@@ -22,7 +22,7 @@
@register
-class Icon(DOMWidget, WidgetBase):
+class Icon(WidgetBase, DOMWidget):
_model_name = Unicode("IconModel").tag(sync=True)
_view_name = Unicode("IconView").tag(sync=True)