From 65f905545218942c004dab37090ced24f39ac50f Mon Sep 17 00:00:00 2001 From: David Bau Date: Mon, 21 Mar 2022 00:02:08 -0400 Subject: [PATCH] Rewrite show, and modify labwidget to use it. --- baulab/__init__.py | 14 ++ baulab/labwidget.py | 125 +++++------ baulab/paintwidget.py | 24 +-- baulab/plotwidget.py | 6 +- baulab/show.py | 491 +++++++++++++++++++++++++++++++----------- 5 files changed, 454 insertions(+), 206 deletions(-) create mode 100644 baulab/__init__.py diff --git a/baulab/__init__.py b/baulab/__init__.py new file mode 100644 index 0000000..06f9eee --- /dev/null +++ b/baulab/__init__.py @@ -0,0 +1,14 @@ +from .labwidget import Model, Widget, Trigger, Property, Event +from .labwidget import Button, Label, Textbox, Numberbox, Range, ColorPicker +from .labwidget import Choice, Menu, Datalist, Div, ClickDiv, Image +from .paintwidget import PaintWidget +from .plotwidget import PlotWidget +from . import pbar +from .nethook import Trace, TraceDict, set_requires_grad +from .nethook import subsequence, get_module, get_parameter, replace_module +from .runningstats import Stat, Mean, Variance, Covariance, Bincount, CrossCovariance +from .runningstats import IoU, CrossIoU, Quantile, TopK, History, CombinedStat +from .runningstats import tally +from . import show +from .workerpool import WorkerBase, WorkerPool +from .pidfile import reserve_dir diff --git a/baulab/labwidget.py b/baulab/labwidget.py index 5af0732..03620b9 100644 --- a/baulab/labwidget.py +++ b/baulab/labwidget.py @@ -49,6 +49,7 @@ import json import html import re from inspect import signature +from . import show class Model(object): @@ -180,7 +181,7 @@ class Widget(Model): for the top-level widget element. ''' - def __init__(self, style=None, data=None, className=None): + def __init__(self, style=None): # In the jupyter case, there can be some delay between js injection # and comm creation, so we need to queue some initial messages. if WIDGET_ENV == 'jupyter': @@ -197,8 +198,6 @@ class Widget(Model): # The style and data properties come standard, and are used to # control the style and data attributes on the toplevel element. self.style = Property(style) - self.className = Property(className) - self.data = Property(data) # Each widget has a "write" event that is used to insert # html before the widget. self.write = Trigger() @@ -217,7 +216,8 @@ class Widget(Model): Override to define the initial HTML view of the widget. Should define an element with id given by view_id(). ''' - return f'
' + with show.enter_tag() as t: + return t.begin() + t.end() def view_id(self): ''' @@ -228,13 +228,9 @@ class Widget(Model): def std_attrs(self): ''' - Returns id and (if applicable) style attributes, escaped and - formatted for use within the top-level element of widget HTML. + Returns id attribute, and possibly other standard attrs. ''' - return (f'id="{self.view_id()}"' + - style_attr(self.style) + - class_attr(self.className) + - data_attrs(self.data)) + return show.attr(id=self.view_id()) def _repr_html_(self): ''' @@ -557,9 +553,8 @@ class Button(Widget): ''') def widget_html(self): - return f'''''' - + return show.emit_tag('input', self.std_attrs(), + type='button', value=self.label) class Label(Widget): def __init__(self, value='', **kwargs): @@ -578,17 +573,18 @@ class Label(Widget): ''') def widget_html(self): - return f'''''' + out = [] + with show.enter_tag('label', self.std_attrs(), out=out): + out.append(html.escape(str(self.value))) + return ''.join(out) class Textbox(Widget): - def __init__(self, value='', size=20, style=None, desc=None, **kwargs): + def __init__(self, value='', size=20, style=None, **kwargs): super().__init__(style=defaulted(style, display='inline-block'), **kwargs) # databinding is defined using Property objects. self.value = Property(value) self.size = Property(size) - self.desc = Property(desc) def widget_js(self): # Both "model" and "element" objects are defined within the scope @@ -614,21 +610,16 @@ class Textbox(Widget): ''') def widget_html(self): - - html_str = f'''''' - if self.desc is not None: - html_str = f"""{self.desc}{html_str}""" - return html_str + return show.emit_tag('input', self.std_attrs(), + type='text', value=self.value, size=self.size) class Numberbox(Widget): - def __init__(self, value='', size=20, style=None, desc=None, **kwargs): + def __init__(self, value='', size=20, style=None, **kwargs): super().__init__(style=defaulted(style, display='inline-block'), **kwargs) # databinding is defined using Property objects. self.value = Property(value) self.size = Property(size) - self.desc = Property(desc) def widget_js(self): # Both "model" and "element" objects are defined within the scope @@ -654,12 +645,8 @@ class Numberbox(Widget): ''') def widget_html(self): - - html_str = f'''''' - if self.desc is not None: - html_str = f"""{self.desc}{html_str}""" - return html_str + return show.emit_tag('input', self.std_attrs(), + type='numeric', value=self.value, size=self.size) class Range(Widget): def __init__(self, value=50, min=0, max=100, step=1, **kwargs): @@ -688,8 +675,9 @@ class Range(Widget): ''') def widget_html(self): - return f'''''' + return show.emit_tag('input', self.std_attrs(), + type='range', value=self.value, + min=self.min, max=self.max, step=self.step) class ColorPicker(Widget): @@ -714,11 +702,8 @@ class ColorPicker(Widget): ''') def widget_html(self): - html_str = f'''''' - if self.desc is not None: - html_str = f"""{self.desc}{html_str}""" - return html_str + return show.emit_tag('input', self.std_attrs(), + type='color', value=self.value) class Choice(Widget): @@ -726,13 +711,12 @@ class Choice(Widget): A set of radio button choices. """ - def __init__(self, choices=None, selection=None, horizontal=False, + def __init__(self, choices=None, selection=None, **kwargs): super().__init__(**kwargs) if choices is None: choices = [] self.choices = Property(choices) - self.horizontal = Property(horizontal) self.selection = Property(selection) def widget_js(self): @@ -748,7 +732,7 @@ class Choice(Widget): return '' }); - element.innerHTML = lines.join(model.get('horizontal')?' ':'
'); + element.innerHTML = lines.join(); } model.on('choices horizontal', render); model.on('selection', (ev) => { @@ -762,14 +746,14 @@ class Choice(Widget): ''') def widget_html(self): - radios = [ - f"""""" - for value in self.choices] - sep = " " if self.horizontal else "
" - return f'
{sep.join(radios)}
' - + out = [] + with show.enter_tag('form', self.std_attrs(), out=out): + for value in self.choices: + with show.enter_tag('label', out=out): + show.emit_tag('input', + (show.attrs(checked=None) if value == self.selection else None), + name='choice', type='radio', value=value, out=out) + return ''.join(out) class Menu(Widget): """ @@ -798,7 +782,6 @@ class Menu(Widget): }); element.menu.innerHTML = lines.join('\\n'); } - model.on('choices horizontal', render); model.on('selection', (ev) => { [...element.querySelectorAll('option')].forEach((e) => { e.selected = (e.value == ev.value); @@ -810,14 +793,15 @@ class Menu(Widget): ''') def widget_html(self): - options = [ - f"""""" - for value in self.choices] - sep = "\n" - return f'''
''' + out = [] + with show.enter_tag('form', self.std_attrs(), out=out): + with show.enter_tag(show.Tag('select', name='menu'), out=out): + for value in self.choices: + with show.enter_tag(show.Tag('option', + (show.attrs(selected=None) if value == self.selection else None), + value=value, out=out)): + out.append(html.escape(str(value))) + return ''.join(out) class Datalist(Widget): @@ -877,15 +861,14 @@ class Datalist(Widget): ''') def widget_html(self): - options = [ - f"""