Skip to content

Commit

Permalink
Switch back from DocumentWidget to MainAreaWidget. Added AutoScroll, …
Browse files Browse the repository at this point in the history
…SimpleOutput and logging.
  • Loading branch information
Alan Fleming committed Nov 22, 2024
1 parent d2ee1ce commit 312b338
Show file tree
Hide file tree
Showing 20 changed files with 1,200 additions and 472 deletions.
328 changes: 328 additions & 0 deletions examples/simple_output.ipynb
Original file line number Diff line number Diff line change
@@ -0,0 +1,328 @@
{
"cells": [
{
"cell_type": "markdown",
"id": "0",
"metadata": {},
"source": [
"**To use this notebook:** Run one line at a time waiting for each cell to return before running the next cell."
]
},
{
"cell_type": "markdown",
"id": "1",
"metadata": {
"jp-MarkdownHeadingCollapsed": true
},
"source": [
"## Simple Output\n",
"\n",
"SimpleOutput is a widget that provides an output area to display all types of output. \n",
"\n",
"It is designed to minimise the size of messages and/or number of messages sent to the frontend. It is not supposed to be a drop in replacement for the Ipywidget `Output' widget, rather it provides an alternate type of interface.\n",
"\n",
"Compared to the Ipywidgets `Output` maintains a synchronised model of all loaded outputs. Each item added to `SimpleOutput` is serialized and sent to the frontend. There is no representation of the data left on the Python side meaning that `SimpleOutput` is more suitable for logging applications. "
]
},
{
"cell_type": "markdown",
"id": "2",
"metadata": {},
"source": [
"## Methods\n",
"\n",
"There are two methods to add outputs \n",
"1. `push`\n",
"2. `set`\n",
"\n",
"and one '`clear`' to clear the outputs.\n"
]
},
{
"cell_type": "markdown",
"id": "3",
"metadata": {},
"source": [
"### `push`\n",
"\n",
"`push` serializes and sends data as a custom message which is appended to the existing output."
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "4",
"metadata": {},
"outputs": [],
"source": [
"import ipylab\n",
"from ipylab.simple_output import SimpleOutput"
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "5",
"metadata": {},
"outputs": [],
"source": [
"so = SimpleOutput(layout={\"max_height\": \"200px\"})"
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "6",
"metadata": {},
"outputs": [],
"source": [
"for i in range(50):\n",
" so.push(f\"test {i}\\n\")"
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "7",
"metadata": {},
"outputs": [],
"source": [
"so"
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "8",
"metadata": {},
"outputs": [],
"source": [
"# or we could do it with one message\n",
"so.clear()\n",
"so.push(*(f\"test {i}\\n\" for i in range(100)))\n",
"# Not that we've just sent 100 'outputs' in one message.\n",
"# All of the 100 lines are squashed into the 200px high simple output."
]
},
{
"cell_type": "markdown",
"id": "9",
"metadata": {},
"source": [
"### set\n",
"\n",
"`Set` is similar to push, but is run as task and clears the output prior at adding the new outputs. The task returns the number of outputs in use."
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "10",
"metadata": {},
"outputs": [],
"source": [
"t = so.set(\"Line one\\n\", \"Line two\")"
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "11",
"metadata": {},
"outputs": [],
"source": [
"so"
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "12",
"metadata": {},
"outputs": [],
"source": [
"assert so.length == t.result() # noqa: S101\n",
"so.length"
]
},
{
"cell_type": "markdown",
"id": "13",
"metadata": {},
"source": [
"## max_continuous_streams\n",
"\n",
"Notice that above the length is 1 even though we sent two values? \n",
"\n",
"This is because both items are streams, and by default they get put into the same output in the frontend. \n",
"\n",
"The maximum number of consecutive streams is configurable with `max_continuous_streams`."
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "14",
"metadata": {},
"outputs": [],
"source": [
"# Make each stream go into a new output.\n",
"so.max_continuous_streams = 0"
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "15",
"metadata": {},
"outputs": [],
"source": [
"t = so.set(\"Line one\\n\", \"Line two\")"
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "16",
"metadata": {},
"outputs": [],
"source": [
"assert so.length == t.result() # noqa: S101\n",
"so.length"
]
},
{
"cell_type": "markdown",
"id": "17",
"metadata": {},
"source": [
"# AutoScroll\n",
"\n",
"AutoScroll is a widget that provides automatic scrolling around a content widget. It is intended to be used in panels placed in the shell, and doesn't work correctly when used in notebooks.\n",
"\n",
"**Note**\n",
"\n",
"Autoscroll uses a relatively new feature `onscrollend` ([detail](https://developer.mozilla.org/en-US/docs/Web/API/Element/scrollend_event)) and **may not work well on Safari** for fast update rates."
]
},
{
"cell_type": "markdown",
"id": "18",
"metadata": {},
"source": [
"## Builtin log viewer\n",
"\n",
"The built in log viewer uses the AutoScroll widget to scroll its output."
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "19",
"metadata": {},
"outputs": [],
"source": [
"app = ipylab.app\n",
"\n",
"app.log_viewer.add_to_shell()\n",
"app.log_level = \"INFO\""
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "20",
"metadata": {},
"outputs": [],
"source": [
"for _ in range(100):\n",
" app.log.info(\"Demo\")\n",
" app.log.error(\"Demo\")"
]
},
{
"cell_type": "markdown",
"id": "21",
"metadata": {},
"source": [
"## Example usage"
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "22",
"metadata": {},
"outputs": [],
"source": [
"from datetime import datetime\n",
"\n",
"import ipywidgets as ipw\n",
"\n",
"import ipylab\n",
"from ipylab.simple_output import AutoScroll"
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "23",
"metadata": {},
"outputs": [],
"source": [
"vb = ipw.VBox()\n",
"sw = AutoScroll(content=vb)\n",
"sw_holder = ipw.VBox([sw], layout={\"height\": \"200px\", \"border\": \"solid\"})\n",
"\n",
"enabled = ipw.Checkbox(description=\"Auto scroll\", indent=False)\n",
"ipw.link((sw, \"enabled\"), (enabled, \"value\"))\n",
"sleep = ipw.FloatSlider(description=\"Sleep time (s)\", value=0.3, min=0.05, max=1, step=0.01)\n",
"\n",
"b = ipw.Button(description=\"Start\")\n",
"\n",
"\n",
"def on_click(b):\n",
" if b.description == \"Start\":\n",
" import asyncio\n",
"\n",
" async def generate_output():\n",
" while True:\n",
" vb.children = (*vb.children, ipw.HTML(f\"It is now {datetime.now().isoformat()}\")) # noqa: DTZ005\n",
" await asyncio.sleep(sleep.value)\n",
"\n",
" b.task = ipylab.app.to_task(generate_output())\n",
" b.description = \"Stop\"\n",
" else:\n",
" b.task.cancel()\n",
" b.description = \"Start\"\n",
"\n",
"\n",
"b.on_click(on_click)\n",
"\n",
"p = ipylab.Panel([ipw.HBox([enabled, sleep, b], layout={\"justify_content\": \"center\"}), sw_holder])\n",
"p.add_to_shell(mode=ipylab.InsertMode.split_right)"
]
}
],
"metadata": {
"kernelspec": {
"display_name": "Python 3 (ipykernel)",
"language": "python",
"name": "python3"
},
"language_info": {
"codemirror_mode": {
"name": "ipython",
"version": 3
},
"file_extension": ".py",
"mimetype": "text/x-python",
"name": "python",
"nbconvert_exporter": "python",
"pygments_lexer": "ipython3",
"version": "3.11.10"
}
},
"nbformat": 4,
"nbformat_minor": 5
}
10 changes: 2 additions & 8 deletions ipylab/common.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,8 @@

hookimpl = pluggy.HookimplMarker("ipylab") # Used for plugins

SVGSTR_TEST_TUBE = '<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" viewBox="0 0 392.493 392.493" xml:space="preserve" fill="#000000"><g id="SVGRepo_bgCarrier" stroke-width="0"></g><g id="SVGRepo_tracerCarrier" stroke-linecap="round" stroke-linejoin="round"></g><g id="SVGRepo_iconCarrier"> <polygon style="fill:#FFFFFF;" points="83.2,99.123 169.697,185.62 300.477,185.62 148.687,33.701 "></polygon> <g> <path style="fill:#56ACE0;" d="M21.851,348.917c0,12.024,9.826,21.786,21.786,21.786s21.786-9.826,21.786-21.786 c0-7.111-10.214-25.794-21.786-43.184C32,323.123,21.851,341.806,21.851,348.917z"></path> <path style="fill:#56ACE0;" d="M31.677,218.59c0,6.594,5.301,11.895,11.895,11.895s11.895-5.301,11.895-11.895 c-0.065-3.491-5.042-13.382-11.895-24.113C36.784,205.143,31.741,215.034,31.677,218.59z"></path> </g> <path style="fill:#194F82;" d="M372.622,226.864L164.073,18.315c3.943-4.267,3.879-10.925-0.323-15.063 c-3.62-3.556-10.02-5.042-15.451,0L52.687,98.864c-4.267,4.267-4.267,11.119,0,15.451c4.202,4.202,10.796,4.267,15.063,0.323 l208.549,208.55c25.471,27.345,73.503,25.729,96.259,0C399.127,296.618,399.127,253.499,372.622,226.864z M83.2,99.123 l65.422-65.358l151.919,151.919h-130.78L83.2,99.123z M357.172,307.737c-15.321,16.356-44.8,19.846-65.358,0L191.483,207.406 h130.844l34.844,34.844C375.208,260.351,375.208,289.701,357.172,307.737z"></path> <path style="fill:#FFC10D;" d="M357.172,307.737c18.036-18.036,18.036-47.386,0-65.422l-34.844-34.909H191.483l100.331,100.331 C312.436,327.584,341.851,324.093,357.172,307.737z"></path> <g> <path style="fill:#194F82;" d="M34.844,280.327C29.026,288.149,0,328.295,0,348.917c0,24.048,19.653,43.572,43.636,43.572 s43.572-19.523,43.572-43.572c0-20.622-29.026-60.768-34.844-68.59C48.291,274.767,39.046,274.767,34.844,280.327z M43.636,370.767 c-12.024,0-21.786-9.826-21.786-21.786c0-7.111,10.214-25.794,21.786-43.184c11.572,17.325,21.786,36.008,21.786,43.184 C65.422,360.941,55.661,370.767,43.636,370.767z"></path> <path style="fill:#194F82;" d="M43.636,252.335c18.618,0,33.745-15.127,33.745-33.681c0-15.063-19.071-41.956-24.954-49.842 c-4.073-5.56-13.382-5.56-17.519,0c-5.883,7.887-24.954,34.78-24.954,49.842C9.891,237.272,25.018,252.335,43.636,252.335z M43.636,194.541c6.853,10.731,11.895,20.622,11.895,24.113c0,6.594-5.301,11.895-11.895,11.895s-11.96-5.301-11.96-11.895 C31.741,215.163,36.784,205.272,43.636,194.541z"></path> </g> </g></svg>'

if TYPE_CHECKING:
from collections.abc import Awaitable, Callable
from typing import TypeVar, overload
Expand Down Expand Up @@ -240,11 +242,3 @@ def trait_tuple_add(owner: HasTraits, name: str, value: Any):
items = getattr(owner, name)
if value not in items:
owner.set_trait(name, (*items, value))


def truncated_repr(obj: Any, maxlen=40, tail="…") -> str:
"Do truncated string representation of obj."
rep = repr(obj)
if len(rep) > maxlen:
return rep[0 : maxlen - len(tail)] + tail
return rep
7 changes: 7 additions & 0 deletions ipylab/hookspecs.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,8 @@

import ipylab
from ipylab.common import IpylabKwgs
from ipylab.log import IpylabLogHandler
from ipylab.log_viewer import LogViewer


@hookspec(firstresult=True)
Expand Down Expand Up @@ -89,3 +91,8 @@ def vpath_getter(app: ipylab.App, kwgs: dict) -> Awaitable[str] | str: # type:
vpath is the 'virtual path' for a session.
"""


@hookspec(firstresult=True)
def get_log_viewer(app: ipylab.App, handler: IpylabLogHandler) -> LogViewer: # type: ignore
"""Create a new instance of a logViewer."""
Loading

0 comments on commit 312b338

Please sign in to comment.