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)