bridge_cfg.update(auto_show=True, auto_mount=True)
app, brt, rt = get_app(hooks=True, show_logger=True)Widget (very WIP)
FashHTML widgets
Replacing ipywidgets with Fasthtml
ipywidgets handles:
- Layout: ipywidgets uses python, JS -> replace with FastHTML, HTMX.
- Comunication kernel-frontend: ipywidgets uses Comm -> Bridget/HTTP-Ajax
- Serialization: ipywidgets uses Traitlets -> replace with pydantic dataclasses, msgspec, pysignal.
- Observer pattern: ipywidgets uses Traitlets -> replace with custom or psignal.
brt.bridge.plugins{'loader': <bridget.bridge.Loader at 0x11f6e7c50>,
'htmx': <bridget.bridge.HTMXPlugin at 0x13f2a9700>,
'fasthtmljs': <bridget.bridge.BridgePlugin at 0x14b283590>,
'observer': <bridget.bridge.BridgePlugin at 0x128774e00>,
'brd_mark': <bridget.bridge.BridgePlugin at 0x14b047f80>,
'nbhooks': <bridget.bridge_plugins.NBHooksPlugin at 0x1181cf3e0>,
'commander': <bridget.bridge_plugins.HTMXCommanderPlugin at 0x12ffd3200>,
'bridget': <bridget.bridget.Bridget at 0x13808e960>}
dv = Div(cls='bridget slider')
dv.set(cls=f"{dv.get('class')} value")
with bridge_cfg(auto_show=False):
display(dv)<div class="bridget slider value"></div>nb = brt.bridge.state
nb.cells(#3) [{'idx': 0, 'source': 'brt.bridge.plugins', 'id': 'X23sZmlsZQ==', 'cell_type': 'code', 'outputs': [{'output_type': 'display_data', 'data': {'text/plain': "{'loader': <bridget.bridge.Loader at 0x11f6e7c50>,\n 'htmx': <bridget.bridge.HTMXPlugin at 0x13f2a9700>,\n 'fasthtmljs': <bridget.bridge.BridgePlugin at 0x14b283590>,\n 'observer': <bridget.bridge.BridgePlugin at 0x128774e00>,\n 'brd_mark': <bridget.bridge.BridgePlugin at 0x14b047f80>,\n 'nbhooks': <bridget.bridge_plugins.NBHooksPlugin at 0x1181cf3e0>,\n 'commander': <bridget.bridge_plugins.HTMXCommanderPlugin at 0x12ffd3200>,\n 'bridget': <bridget.bridget.Bridget at 0x13808e960>}"}, 'metadata': {'bridge': {'captured': True}}}, {'output_type': 'execute_result', 'execution_count': 12, 'data': {'text/plain': "{'loader': <bridget.bridge.Loader at 0x11f6e7c50>,\n 'htmx': <bridget.bridge.HTMXPlugin at 0x13f2a9700>,\n 'fasthtmljs': <bridget.bridge.BridgePlugin at 0x14b283590>,\n 'observer': <bridget.bridge.BridgePlugin at 0x128774e00>,\n 'brd_mark': <bridget.bridge.BridgePlugin at 0x14b047f80>,\n 'nbhooks': <bridget.bridge_plugins.NBHooksPlugin at 0x1181cf3e0>,\n 'commander': <bridget.bridge_plugins.HTMXCommanderPlugin at 0x12ffd3200>,\n 'bridget': <bridget.bridget.Bridget at 0x13808e960>}"}, 'metadata': {}}], 'execution_count': 12},{'idx': 1, 'source': 'dv = Div(cls=\'bridget slider\')\ndv.set(cls=f"{dv.get(\'class\')} value")\nwith bridge_cfg(auto_show=False):\n display(dv)', 'id': 'X24sZmlsZQ==', 'cell_type': 'code', 'outputs': [{'output_type': 'display_data', 'data': {'text/plain': "div((),{'class': 'bridget slider value'})", 'text/markdown': '```html\n<div class="bridget slider value"></div>\n\n```'}, 'metadata': {}}], 'execution_count': 13},{'idx': 2, 'source': 'nb = brt.bridge.state\nnb.cells', 'id': 'X25sZmlsZQ==', 'cell_type': 'code', 'outputs': []}]
Helpers
def show_routes(app):
cprint(app.routes)
while app.routes and (app := getattr(app.routes[-1], 'app', None)):
if hasattr(app, 'routes') and isinstance(app.routes, list): cprint(app.routes)
else: breakLink
Replicate traitlets.link
def link(pair1, pair2, transform=None):
"""
Bidirectionally link two fields, possibly on the same object, using optional transform (forward, reverse).
pair1, pair2: tuples of (object, attr_name)
transform: (func1→2, func2→1), or None.
"""
a_obj, a_field = pair1
b_obj, b_field = pair2
tf_ab = transform[0] if transform and transform[0] else (lambda v: v)
tf_ba = transform[1] if transform and transform[1] else (lambda v: v)
def on_a(val):
new = tf_ab(val)
if getattr(b_obj, b_field) != new:
with getattr(a_obj.events, a_field).blocked():
setattr(b_obj, b_field, new)
def on_b(val):
new = tf_ba(val)
if getattr(a_obj, a_field) != new:
with getattr(b_obj.events, b_field).blocked():
setattr(a_obj, a_field, new)
getattr(a_obj.events, a_field).connect(on_a)
getattr(b_obj.events, b_field).connect(on_b)@psygnal.evented
@dataclass
class Item:
x: float = 0.0
y: float = 0.0
item = Item(x=32.0, y=0.0)
# x is Fahrenheit, y is Celsius
f_to_c = lambda f: (f - 32) * 5/9
c_to_f = lambda c: c * 9/5 + 32
link((item, "x"), (item, "y"), (f_to_c, c_to_f))
item.x = 212.0
test_eq(item.y, 100)
item.y = 0.0
test_eq(item.x, 32)_debug_req
def _debug_req(name, url, method, body):
return {
'headers': {
'HX-Request': 'true',
'HX-Trigger-Name': name,
'Content-Type': 'application/x-www-form-urlencoded'
},
'headerNames': {'hx-request': 'HX-Request', 'hx-current-url': 'HX-Current-URL',
'hx-trigger-name': 'HX-Trigger-Name', 'hx-trigger-value': 'HX-Trigger-Value',
'content-type':'Content-Type'},
'status': 0,
'method': method,
'url': url,
'async': True,
'timeout': 0,
'withCredentials': False,
'body': body,
}@dataclass
class Person:
name: str
age: int = 0
events: ClassVar[psygnal.SignalGroupDescriptor] = psygnal.SignalGroupDescriptor()
@dataclass
class Citizen(Person):
country: str = 'USA'
_city: str = 'New York'
@dataclass
class Employee(Citizen):
company: str = 'Central Perk'
e = Employee(name='John', age=30)
psygnal.is_evented(Employee), psygnal.is_evented(e)(True, True)
e.events.signalsmappingproxy({'_city': <_DataclassFieldSignalInstance '_city' on <SignalGroup 'EmployeeSignalGroup' with 5 signals>>,
'age': <_DataclassFieldSignalInstance 'age' on <SignalGroup 'EmployeeSignalGroup' with 5 signals>>,
'company': <_DataclassFieldSignalInstance 'company' on <SignalGroup 'EmployeeSignalGroup' with 5 signals>>,
'country': <_DataclassFieldSignalInstance 'country' on <SignalGroup 'EmployeeSignalGroup' with 5 signals>>,
'name': <_DataclassFieldSignalInstance 'name' on <SignalGroup 'EmployeeSignalGroup' with 5 signals>>})
BridgetRouteProvider
def attrgetterd(*attrs, default=None):
def _call(obj):
oo = tuple(getattr(obj, attr, default) for attr in attrs)
return oo[0] if len(oo) == 1 else oo
return _callr = AD(name='a')
test_eq(attrgetterd('name')(r), 'a')
test_eq(attrgetterd('name', 'kind')(r), ('a', None))def types(o, stop=-1): return reversed(type(o).__mro__[:stop])[*types(APIRouterD(), None)] # type: ignore[object,
fasthtml.core.APIRouter,
bridget.routing.APIRouterC,
bridget.routing.APIRouterD]
if typing.TYPE_CHECKING:
class Control: ...
def _cls_attr(cls: type, attr_name='_wrapper_class') -> str:
if (n := cls.__name__).startswith('_'): return ''
if hasattr(cls, attr_name): return getattr(cls, attr_name)
if issubclass(cls, Control): return n.lower()
return ''
_get_css = attrgetterd('_css')
def _cls_attrs(o, f=_cls_attr): return ' '.join(filter(None, (f(_) for _ in types(o)))).strip()class BridgetRouteProvider(RouteProvider):
bridget = get_bridget()
_updating_ = False
@contextmanager
def _updating(self): self._updating_ = True; yield; self._updating_ = False
def setup_values(self): ...
events: ClassVar[psygnal.SignalGroupDescriptor]
def __init_subclass__(cls):
# cls.bridget.clear_routes(cls)
# cls.bridget.app.routes.clear()
# cls.ar = ar = APIRouterD(f"/{cls.__name__}")
# ar.__set_name__(cls, 'ar')
if not 'events' in vars(cls):
cls.events = events = psygnal.SignalGroupDescriptor(warn_on_no_fields=False)
events.__set_name__(cls, 'events') # call explicitly: outside class definition
def setup_events(self):
if dataclasses.is_dataclass(self):
fields = {fld.name:fld.metadata.get('bridget', {}) for fld in dataclasses.fields(self)}
else: fields = {}
for name in self.events.signals.keys():
mtdt = fields.get(name, {})
if mtdt.get('skip', False): continue
mth = mtdt.get('on', None) or getattr(self, f"on_{name}", None)
if not mth: mth = partial(getattr(self, 'on_attr'), attr=name)
if isinstance(mth, (MethodType, partial)): self.events[name].connect(mth)
def on_attr(self, nw: Any, old: Any, *, attr:str):
if self.bridget and not self._updating_:
with self._updating(): self.setup_values()
self.bridget.bridge.commander.swap(f"#{self.wrapper_id()}", to_xml(self.__ft__()), swapStyle='innerHTML')
ar = APIRouterD()
_mounted = False
def wrapper_id(self): return self.ar.name().replace(':', '_')
def wrapper_class(self): return _cls_attrs(self)
def style(self): return _cls_attrs(self, f=_get_css)
def controls(self): return ()
def wrapper(self): return Div(id=self.wrapper_id(), cls=self.wrapper_class(), style=self.style())
def __ft__(self):
if bridge_cfg.auto_mount and not self._mounted: get_bridget().mount(self, show=False) # type: ignore
return self.wrapper()(*self.controls())
dh: DisplayHandle | None = None
def _ipython_display_(self):
# clear previous display; unlike ipywidgets, bridget shows only one view, MVC not welcomed here
if self.dh is not None: self.dh.update(HTML())
self.dh = self.bridget(self, display_id=self.dh)
self.cell = this()Control
class _Control(BridgetRouteProvider):
disabled:bool=False; description:str=''
def on_disabled(self, nw: Any, old: Any): ...
def on_description(self, nw: Any, old: Any): ...
@dataclass
class Control(_Control):
_: KW_ONLY
disabled:bool = False
description:str = ''
def __post_init__(self):
with self._updating(): self.setup_values()
self.setup_events()# app.routes.clear()c = Control(disabled=True)
with bridge_cfg(auto_show=False):
display(c.__ft__())<div class="control"></div>show_routes(app)[]
Value
@dataclass
class Value(Control):
value: Any = None
@ar('/value', methods=['POST'], name='value')
def changed(self, value:str):
with self._updating(): self.value = value
return self.value
def value_control(self):
return H.Input(name='value', value=self.value,
hx_post=self.ar.value.to(), hx_trigger='input changed', hx_swap='none')
def controls(self):
ctl = self.value_control()
if hasattr(ctl, '__ft__'): ctl = ctl.__ft__()
if isinstance(ctl, (FT, Control)): return (ctl,)
if isinstance(ctl, tuple): return ctl
if isinstance(ctl, Iterable): return tuple(ctl)
return (ctl,)app.routes.clear()v = Value('Hello, world!')
with bridge_cfg(auto_show=False):
display(v.__ft__())
v<div id="Value_Value_1-1764705844" class="control value">
<input name="value" value="Hello, world!" hx-post="/Value/Value_1-1764705844/value" hx-trigger="input changed" hx-swap="none">
</div>show_routes(app)[Mount(path='/Value', name='Value', app=<fasthtml.core.FastHTML object at 0x14b3ed1f0>)]
[Mount(path='/Value_1-1764705844', name='Value_1-1764705844', app=<fasthtml.core.FastHTML object at 0x14b283740>)]
[Route(path='/value', name='value', methods=['POST'])]
v.value'Hello, world!'
v.value = 'Bye!'String
Label
# W.Label('Hello, World!')<div class="lm-Widget jupyter-widgets widget-inline-hbox widget-label">Hello, World!</div>@dataclass
class Label(Control):
value: str = ''
def controls(self): return (self.value, )v = Label('Hello, world!')
with bridge_cfg(auto_show=False):
display(v.__ft__())
v<div class="control label">Hello, world!</div>Hello, world!
v.value'Hello, world!'
Text
# W.Text('Hello, World!')<div class="lm-Widget jupyter-widgets widget-inline-hbox widget-text">
<label class="widget-label" for="8ffde047-0ce3-4324-97a8-8f9bb6bb5a22" style="display: none;"></label>
<input type="text" id="8ffde047-0ce3-4324-97a8-8f9bb6bb5a22" class="widget-input" placeholder="">
</div>@dataclass
class Text(Value):
value: str = ''
@ar('/value', methods=['POST'], name='value')
def changed(self, value:str):
with self._updating(): self.value = value
def value_control(self):
return H.Input(name='value', value=self.value,
hx_post=self.ar.value, hx_trigger='input changed', hx_swap='none')v = Text('Hello, world!')
with bridge_cfg(auto_show=False):
display(v.__ft__())
v<div id="Text_Text_1-1764705844" class="control value text">
<input name="value" value="Hello, world!" hx-post="/Text/Text_1-1764705844/value" hx-trigger="input changed" hx-swap="none">
</div>v.value'Hello, world!'
v.value = 'Good-Bye to All That'show_routes(app)[ Mount(path='/Value', name='Value', app=<fasthtml.core.FastHTML object at 0x14b3ed1f0>), Mount(path='/Text', name='Text', app=<fasthtml.core.FastHTML object at 0x14b41db80>) ]
[Mount(path='/Text_1-1764705844', name='Text_1-1764705844', app=<fasthtml.core.FastHTML object at 0x14b41e1e0>)]
[Route(path='/value', name='value', methods=['POST'])]
TextArea
# W.Textarea('Hello, World!')<div class="lm-Widget jupyter-widgets widget-inline-hbox widget-textarea">
<label class="widget-label" for="ea87d53a-15a1-474f-a14d-2f84302a10d5" style="display: none;"></label>
<textarea rows="" id="ea87d53a-15a1-474f-a14d-2f84302a10d5" class="widget-input" placeholder=""></textarea>
</div>@dataclass
class TextArea(Value):
value: str = ''
@ar('/value', methods=['POST'], name='value')
def changed(self, value:str):
with self._updating(): self.value = value
def value_control(self):
return H.Textarea(name='value',
hx_post=self.ar.value, hx_trigger='keyup changed throttle:100ms', hx_swap='none')(self.value)app.routes.clear()v = TextArea('Hello, world!')
with bridge_cfg(auto_show=False):
display(v.__ft__())
v<div id="TextArea_TextArea_1-1764705844" class="control value textarea">
<textarea name="value" hx-post="/TextArea/TextArea_1-1764705844/value" hx-trigger="keyup changed throttle:100ms" hx-swap="none">Hello, world!</textarea></div>v.value'Hello, world!'
v.value = 'Adieu!'show_routes(app)[Mount(path='/TextArea', name='TextArea', app=<fasthtml.core.FastHTML object at 0x12f21e5d0>)]
[Mount(path='/TextArea_1-1764705844', name='TextArea_1-1764705844', app=<fasthtml.core.FastHTML object at 0x14b41eae0>)]
[Route(path='/value', name='value', methods=['POST'])]
Combobox
<div class="lm-Widget jupyter-widgets widget-inline-hbox widget-text">
<label class="widget-label" for="81e63e64-d351-4dd4-860a-6982c4d65bee" style="">Combobox:</label>
<input type="text" id="81e63e64-d351-4dd4-860a-6982c4d65bee" class="widget-input" placeholder="Choose Someone" list="62fd0f57-8eef-4b16-97d8-2648d373e452">
<datalist id="62fd0f57-8eef-4b16-97d8-2648d373e452">
<option value="Paul"></option>
<option value="John"></option>
<option value="George"></option>
<option value="Ringo"></option>
</datalist>
</div>@dataclass
class Combobox(Value):
value: str = ''
options: list[str] = field(default_factory=list)
def value_control(self):
return (
H.Input(type='text', name='value', list='abc-456', placeholder='Choose Someone',
hx_post=self.ar.value, hx_swap='none',
),
H.Datalist(id='abc-456')(*map(lambda x:H.Option(value=x), self.options)),
)v = Combobox('Ringo', ['Paul', 'John', 'George', 'Ringo'])
with bridge_cfg(auto_show=False):
display(v.__ft__())
v<div id="Combobox_Combobox_1-1764705844" class="control value combobox">
<input type="text" name="value" list="abc-456" placeholder="Choose Someone" hx-post="/Combobox/Combobox_1-1764705844/value" hx-swap="none">
<datalist id="abc-456"><option value="Paul"></option><option value="John"></option><option value="George"></option><option value="Ringo"></option></datalist></div>v.value'Ringo'
v.value = 'George'show_routes(app)[ Mount(path='/TextArea', name='TextArea', app=<fasthtml.core.FastHTML object at 0x12f21e5d0>), Mount(path='/Combobox', name='Combobox', app=<fasthtml.core.FastHTML object at 0x14b54cb00>) ]
[Mount(path='/Combobox_1-1764705844', name='Combobox_1-1764705844', app=<fasthtml.core.FastHTML object at 0x14b54cb30>)]
[Route(path='/value', name='value', methods=['POST'])]
Containers
Box
primes = list(map(int, """
2 3 5 7 11 13 17 19 23 29 31 37 41 43 47 53 59 61 67 71
73 79 83 89 97 101 103 107 109 113 127 131 137 139 149 151 157 163 167 173
179 181 191 193 197 199 211 223 227 229 233 239 241 251 257 263 269 271 277 281
283 293 307 311 313 317 331 337 347 349 353 359 367 373 379 383 389 397 401 409
419 421 431 433 439 443 449 457 461 463 467 479 487 491 499 503 509 521 523 541
547 557 563 569 571 577 587 593 599 601 607 613 617 619 631 641 643 647 653 659
661 673 677 683 691 701 709 719 727 733 739 743 751 757 761 769 773 787 797 809
811 821 823 827 829 839 853 857 859 863 877 881 883 887 907 911 919 929 937 941
947 953 967 971 977 983 991 997""".split()))
len(primes)168
@dataclass
class Box(Control):
children: list[Bridgeable] = field(default_factory=list)
_css = 'display: flex; margin: 0;'
def controls(self): return self.children
def on_children(self, nw: Any, old: Any):
dh = self.bridget(self, display_id=self.dh)
if self.dh is None: self.dh = dh
@dataclass
class HBox(Box):
_css = 'flex-direction: row; gap: 10px; column-gap: 10px;'
@dataclass
class VBox(Box):
_css = 'flex-direction: column; row-gap: 5px;'v = Box(['Hello', Text('world!')])
with bridge_cfg(auto_show=False):
display(v.__ft__())
v<div class="control box" style="display: flex; margin: 0;">
Hello <div id="Text_Text_2-1764705844" class="control value text">
<input name="value" value="world!" hx-post="/Text/Text_2-1764705844/value" hx-trigger="input changed" hx-swap="none">
</div>
</div>
Hello
w = HBox(['Hello', Text('world!')])
with bridge_cfg(auto_show=False):
display(w.__ft__())
w<div class="control box hbox" style="display: flex; margin: 0; flex-direction: row; gap: 10px; column-gap: 10px;">
Hello <div id="Text_Text_3-1764705844" class="control value text">
<input name="value" value="world!" hx-post="/Text/Text_3-1764705844/value" hx-trigger="input changed" hx-swap="none">
</div>
</div>
Hello
w = VBox(['Hello', Text('world!')])
with bridge_cfg(auto_show=False):
display(w.__ft__())
w<div class="control box vbox" style="display: flex; margin: 0; flex-direction: column; row-gap: 5px;">
Hello <div id="Text_Text_4-1764705844" class="control value text">
<input name="value" value="world!" hx-post="/Text/Text_4-1764705844/value" hx-trigger="input changed" hx-swap="none">
</div>
</div>
Hello
w = HBox()
with bridge_cfg(auto_show=False):
display(w.__ft__())
w<div class="control box hbox" style="display: flex; margin: 0; flex-direction: row; gap: 10px; column-gap: 10px;"></div>w.cell#nb.cell_by_did(w.dh.display_id)NBCell@59
- idx: 59
- source: w = HBox() with bridge_cfg(auto_show=False): display(w.__ft__()) w
- id: Y164sZmlsZQ==
- cell_type: code
- output_type: display_data
- text/plain: div((),{'id': '', 'class': 'control box hbox', 'style': 'display: flex; margin: 0; flex-direction: row; gap: 10px; column-gap: 10px;'})
- text/markdown: ```html <div class="control box hbox" style="display: flex; margin: 0; flex-direction: row; gap: 10px; column-gap: 10px;"></div> ```
- metadata: {}
- output_type: display_data
- text/plain: <IPython.core.display.HTML object>
- text/html: <div class="control box hbox" style="display: flex; margin: 0; flex-direction: row; gap: 10px; column-gap: 10px;"></div> <brd-mark id="aa2f…
- metadata: {'brd_did': 'aa2f95d6aaf436220ed671faf4f65e20'}
- execution_count: 75
outputs
0
data
1
data
test_eq(w.children, [])
w.children = [vt1 := H.Text('asdf')]
test_eq(w.children, [vt1])
# test_eq(w.dhdl.contents(), f'<div id="{w.ar.name()}" class="widget">\n<text>asdf</text></div>\n') # type: ignore
test_is('asdf' in w.cell.outputs[1]['data']['text/html'], True)w.children = [*w.children, vb1 :=H.Button('asdf')]
test_eq(w.children, [vt1, vb1])
test_is('button' in w.cell.outputs[1]['data']['text/html'], True)w.children = [*w.children, vb2 := Button('check', description='Click me')]
test_eq(w.children, [vt1, vb1, vb2])
test_is(vb2.ar.name() in w.cell.outputs[1]['data']['text/html'], True)# w = Widget(contents=[Button('asdf'), Text('asdf')])# help(Widget)show_routes(app)[ Mount(path='/TextArea', name='TextArea', app=<fasthtml.core.FastHTML object at 0x12f21e5d0>), Mount(path='/Combobox', name='Combobox', app=<fasthtml.core.FastHTML object at 0x14b54cb00>), Mount(path='/Text', name='Text', app=<fasthtml.core.FastHTML object at 0x14b3ed760>) ]
[ Mount(path='/Text_2-1764705844', name='Text_2-1764705844', app=<fasthtml.core.FastHTML object at 0x14b54e690>), Mount(path='/Text_3-1764705844', name='Text_3-1764705844', app=<fasthtml.core.FastHTML object at 0x14b734b90>), Mount(path='/Text_4-1764705844', name='Text_4-1764705844', app=<fasthtml.core.FastHTML object at 0x14b735a00>) ]
[Route(path='/value', name='value', methods=['POST'])]
cprint(app.routes[1].app.routes) # type: ignore[Mount(path='/Combobox_1-1764705844', name='Combobox_1-1764705844', app=<fasthtml.core.FastHTML object at 0x14b54cb30>)]
Numeric
IntText
@dataclass
class IntText(Value):
value: int = 0
@ar('/value', methods=['POST'], name='value')
def changed(self, value:int):
with self._updating(): self.value = value
# return the update to allow feedback
return self.value or str(self.value) # fasthtml inconditionally handles 0s from route handlers as falsy, not bona fide values
def value_control(self):
return H.Input(name='value', value=str(self.value), type='number',
# hx_post=f"{self.ar.to('changed')}", hx_trigger='input changed throttle:50ms', hx_swap='none')
hx_post=self.ar.value, hx_trigger='input changed', hx_swap='none')app.routes.clear()v = IntText(5)
with bridge_cfg(auto_show=False):
display(v.__ft__())
v<div id="IntText_IntText_1-1764705844" class="control value inttext">
<input name="value" value="5" type="number" hx-post="/IntText/IntText_1-1764705844/value" hx-trigger="input changed" hx-swap="none">
</div>v.value5
v.value = 10show_routes(app)[Mount(path='/IntText', name='IntText', app=<fasthtml.core.FastHTML object at 0x12f21e5d0>)]
[Mount(path='/IntText_1-1764705844', name='IntText_1-1764705844', app=<fasthtml.core.FastHTML object at 0x14b7357f0>)]
[Route(path='/value', name='value', methods=['POST'])]
BoundedInt
@dataclass
class BoundedInt(IntText):
min: int = 0
max: int = 100
step: int = 1
def setup_values(self):
if (val := self.value) < self.min: self.value = self.min
elif val > self.max: self.value = self.max
def value_control(self):
return super().value_control()(min=str(self.min), max=str(self.max), step=str(self.step))v = BoundedInt(10, 5, 15)
with bridge_cfg(auto_show=False):
display(v.__ft__())
v<div id="BoundedInt_BoundedInt_1-1764705844" class="control value inttext boundedint">
<input name="value" value="10" type="number" hx-post="/BoundedInt/BoundedInt_1-1764705844/value" hx-trigger="input changed" hx-swap="none" min="5" max="15" step="1">
</div>v.value10
v.max = 10
test_eq(v.value, 10)IntSlider
@dataclass
class IntSlider(BoundedInt):
readout: bool = True
readout_format: str = 'd'
_wrapper_class = 'slider'
def value_control(self):
return super().value_control()(type='range', hx_target='next text', hx_swap='textContent')
def controls(self):
return (H.Label(_for='value')('Scale'), _n,
self.value_control(),
H.Text(id='spanscale', style='inline')(self.value), _n)# brt.bridge.logger.show(clear=True)iv = IntSlider()
with bridge_cfg(auto_show=False):
display(iv.__ft__())
iv<div id="IntSlider_IntSlider_1-1764705844" class="control value inttext boundedint slider">
<label for="value">Scale</label>
<input name="value" value="0" type="range" hx-post="/IntSlider/IntSlider_1-1764705844/value" hx-trigger="input changed" hx-swap="textContent" min="0" max="100" step="1" hx-target="next text">
<text id="spanscale" style="inline">0</text>
</div># response = brt(_debug_req('value', iv.ar.value.to(), 'POST', 'value=50'))iv.value0
iv.value = 89iv.min = 20
test_eq(iv.value, 89)iv.max = 80
test_eq(iv.value, 80)iv2 = IntSlider(33, step=2)
iv2iv3 = IntSlider()
iv4 = IntSlider()
link((iv3, 'value'), (iv4, 'value'))
box = Div(style='display: flex; gap: 1em;')(iv3, iv4)
boxiv3.value = 22
test_eq((iv3.value, iv4.value), (22, 22))iv5 = IntSlider(57)
iv6 = IntSlider()
link((iv5, 'value'), (iv6, 'value'))
box = HBox([iv5, iv6])
boxBool
class _Bool(Value):
value: bool = False
@ar('/value', methods=['POST'], name='value')
def changed(self, value:str='', abc:bool=False):
with self._updating(): self.value = not self.value
return ''<div class="lm-Widget jupyter-widgets widget-inline-hbox widget-checkbox">
<label class="widget-label" style=""></label>
<label class="widget-label-basic">
<input type="checkbox">
<span title=""></span>
</label>
</div># w = W.Checkbox(True)
# wclass Checkbox(_Bool):
def value_control(self):
return H.Div(cls='inline-hbox')(
H.Label('Check')(
Input(type='checkbox', name='value', checked=self.value,
hx_post=self.ar.value, hx_trigger='click',
hx_vals="js:{'abc':event.target.checked}", hx_swap='none')
),
)# brt.bridge.logger.show(clear=True)# app.routes.clear()v = Checkbox(False)
with bridge_cfg(auto_show=False):
display(v.__ft__())
v<div id="Checkbox_Checkbox_1-1764705844" class="control value checkbox">
<div class="inline-hbox">
<label>Check <input type="checkbox" name="value" hx-post="/Checkbox/Checkbox_1-1764705844/value" hx-trigger="click" hx-swap="none" hx-vals="js:{'abc':event.target.checked}">
</label> </div>
</div># response = brt(_debug_req('value', f"{v.ar.to('changed')}", 'POST', 'value=on&abc=true'))test_is(v.value, False)v.value = Truetest_is(v.value, True)<div class="lm-Widget p-Widget jupyter-widgets widget-valid widget-inline-hbox mod-invalid"><label class="widget-label" title="null" style="display: none;"></label><i class="fa fa-fw fa-times"></i><span class="widget-valid-readout widget-readout">Invalid</span></div>
<div class="lm-Widget jupyter-widgets widget-valid widget-inline-hbox mod-valid"><label class="widget-label" title="null" style="display: none;"></label><i class="fa fa-fw fa-check"></i><span class="widget-valid-readout widget-readout">Invalid</span></div># w = W.Valid(True)
# wclass Valid(_Bool):
def value_control(self):
return H.Div(cls='inline-hbox mod-invalid')(
H.I(cls=f"fa fa-fw fa-{'check' if self.value else 'times'}"),
H.Text(cls='readout')('valid' if self.value else 'invalid')
)v = Valid(True)
with bridge_cfg(auto_show=False):
display(v.__ft__())
v<div id="Valid_Valid_1-1764705844" class="control value valid">
<div class="inline-hbox mod-invalid">
<i class="fa fa-fw fa-check"></i><text class="readout">valid</text> </div>
</div>v.valueTrue
v.value = False<button class="lm-Widget jupyter-widgets jupyter-button widget-toggle-button mod-active" tabbable="null" title="null">Click me</button><button class="lm-Widget jupyter-widgets jupyter-button widget-toggle-button" tabbable="null" title="null">Click me</button>class ToggleButton(_Bool):
@ar('/value', methods=['POST'], name='value')
def changed(self, value:bool):
with self._updating(): self.value = not value
return self.value_control()
def value_control(self):
return H.Button('Click me')(
cls=f"button", name='value',
value=f"{'on' if self.value else 'off'}", style=f"{'background-color:darkgray' if self.value else ''}",
hx_post=self.ar.value, hx_swap='outerHTML')v = ToggleButton(False)
with bridge_cfg(auto_show=False):
display(v.__ft__())
v<div id="ToggleButton_ToggleButton_1-1764705844" class="control value togglebutton">
<button class="button" name="value" value="off" hx-post="/ToggleButton/ToggleButton_1-1764705844/value" hx-swap="outerHTML">Click me</button></div># response = brt(_debug_req('value', f"{v.ar.to('changed')}", 'POST', 'value=on'))
# print(v.value)Selection
Dropdown
<div class="lm-Widget jupyter-widgets widget-inline-hbox widget-dropdown">
<label class="widget-label" for="3f6a6063-c1fa-4f3d-966b-185eb49eb67f" style="">Number:</label>
<select id="3f6a6063-c1fa-4f3d-966b-185eb49eb67f">
<option data-value="1" value="1">1</option>
<option data-value="2" value="2">2</option>
<option data-value="3" value="3">3</option>
</select>
</div>@dataclass
class _Select(Value):
options: list[tuple[str, Any]|Any] = field(default_factory=list)
def __post_init__(self):
self.options = [t if isinstance(t, tuple) else (str(t), t) for t in self.options]
super().__post_init__()
@cached_property
def values(self) -> list[Any]: return [v for _,v in self.options]
@property
def index(self) -> int|None: return self.values.index(self.value) if self.value in self.values else None
@index.setter
def index(self, i:int): self.value = self.values[i] if i>=0 and i<len(self.options) else None
def on_options(self, nw: Any):
try: del self.values
except Exception: pass
if self.value not in self.values: self.value = None
@ar('/value', methods=['POST'], name='value')
def changed(self, value:str, index:int):
with self._updating():
# self.index = index
self.value = self.options[index][1]
self.setup_values()@dataclass
class Dropdown(_Select):
def value_control(self):
return H.Div(cls='inline-hbox')(
H.Label(self.description)(
H.Select(name='value',
hx_post=self.ar.value,
hx_vals="js:{'index':event.target.selectedIndex}", hx_swap='none')(
*[Option(value=opt[1], selected=i==self.index)(opt[0]) for i,opt in enumerate(self.options)]
)
)
)v = Dropdown(
value=2,
options=[('One', 1), ('Two', 2), ('Three', 3)]
)
with bridge_cfg(auto_show=False):
display(v.__ft__())
v<div id="Dropdown_Dropdown_1-1764705844" class="control value dropdown">
<div class="inline-hbox">
<label><select name="value" hx-post="/Dropdown/Dropdown_1-1764705844/value" hx-swap="none" hx-vals="js:{'index':event.target.selectedIndex}"><option value="1">One</option><option value="2" selected>Two</option><option value="3">Three</option></select></label> </div>
</div>v.value, v.index(2, 1)
v.value = 1Select
<div class="lm-Widget jupyter-widgets widget-inline-hbox widget-select">
<label class="widget-label" for="a3c9d534-7c03-4948-9cf2-87dc8f353e1f" style="">OS:</label>
<select id="a3c9d534-7c03-4948-9cf2-87dc8f353e1f" size="5">
<option data-value="Linux" value="Linux">Linux</option>
<option data-value="Windows" value="Windows">Windows</option>
<option data-value="macOS" value="macOS">macOS</option>
</select>
</div>@dataclass
class SelectArea(_Select):
rows: int = 5
def value_control(self):
return H.Div(cls='inline-hbox')(
H.Label(self.description)(
H.Select(name='value', size=self.rows,
hx_post=self.ar.value,
hx_vals="js:{'index':event.target.selectedIndex}", hx_swap='none')(
*[H.Option(value=opt[1], selected=i==self.index)(opt[0]) for i,opt in enumerate(self.options)]
)
)
)v = SelectArea(
options=['Linux', 'Windows', 'macOS'],
value='macOS',
# rows=10,
description='OS:',
)
with bridge_cfg(auto_show=False):
display(v.__ft__())
v<div id="SelectArea_SelectArea_1-1764705844" class="control value selectarea">
<div class="inline-hbox">
<label>OS:<select name="value" size="5" hx-post="/SelectArea/SelectArea_1-1764705844/value" hx-swap="none" hx-vals="js:{'index':event.target.selectedIndex}"><option value="Linux">Linux</option><option value="Windows">Windows</option><option value="macOS" selected>macOS</option></select></label> </div>
</div>v.value, v.index('macOS', 2)
v.value = 'Windows'SelectMultiple
<div class="lm-Widget jupyter-widgets widget-inline-hbox widget-select widget-select-multiple">
<label class="widget-label" for="c3af7603-7856-431f-ad6a-15ede273e09f" style="">Fruits</label>
<select multiple="" id="c3af7603-7856-431f-ad6a-15ede273e09f" size="5">
<option data-value="Apples" value="Apples">Apples</option>
<option data-value="Oranges" value="Oranges">Oranges</option>
<option data-value="Pears" value="Pears">Pears</option>
</select>
</div>@dataclass
class SelectMultiple(_Select):
value: list[Any] = field(default_factory=list)
# index: list[int] = field(default_factory=list)
rows: int = 5
@property
def index(self) -> list[int]: return [self.values.index(_) for _ in self.value]
@index.setter
def index(self, ii:list[int]): self.value = [self.values[_] for _ in ii if _ in range(len(self.options))]
@ar('/value', methods=['POST'], name='value')
def changed(self, value:list[str]=[], index:list[int]=[]):
with self._updating():
# self.index = index
self.value = [self.values[_] for _ in index if _ in range(len(self.options))]
self.setup_values()
def value_control(self):
return H.Div(cls='inline-hbox')(
H.Label(self.description)(
H.Select(name='value', size=self.rows, multiple=True,
hx_post=self.ar.value,
hx_vals="js:{'index':[...event.target.selectedOptions].map(i=>i.index)}", hx_swap='none')(
*[H.Option(value=opt[1], selected=i in self.index)(opt[0]) for i,opt in enumerate(self.options)]
)
)
)v = SelectMultiple(
options=['Apples', 'Oranges', 'Pears'],
value=['Pears'],
description='Fruits',
)
with bridge_cfg(auto_show=False):
display(v.__ft__())
v<div id="SelectMultiple_SelectMultiple_1-1764705844" class="control value selectmultiple">
<div class="inline-hbox">
<label>Fruits<select name="value" size="5" multiple hx-post="/SelectMultiple/SelectMultiple_1-1764705844/value" hx-swap="none" hx-vals="js:{'index':[...event.target.selectedOptions].map(i=>i.index)}"><option value="Apples">Apples</option><option value="Oranges">Oranges</option><option value="Pears" selected>Pears</option></select></label> </div>
</div>v.value, v.index(['Pears'], [2])
Simple widgets (Traitlets)
Value
# class Value(T.HasTraits, Control):
# value = T.Any()
# @T.observe('value')
# def on_value(self, _):
# if self.bridget and not self._updating_:
# self.bridget.bridge.commander.swap(f"#{self.ar.name()}", to_xml(self.__ft__()), swapStyle='innerHTML')# app.routes.clear()# v = Value(value='Hello, world!')
# with bridge_cfg(auto_show=False):
# display(v.__ft__())
# v# v.value# v.value = 'bye!'Numeric
# class Numeric(Value):
# value: int
# min, max, step = None, None, None
# input_type = 'number'
# hx_target, hx_swap = None, 'none'
# @ar('/value', methods=['POST'])
# def changed(self, value:int):
# with self._updating(): self.value = value
# return self.value # fasthtml handles 0s from route handlers as falsy, not bona fide values
# def __ft__(self):
# return Input(type=self.input_type, name='value',
# value=self.value, min=self.min, max=self.max, step=self.step,
# hx_post=f"/{self.ar.to()}/value", hx_trigger='input changed throttle:100ms',
# hx_target=self.hx_target, hx_swap=self.hx_swap)# class _Int(_Value):
# value: int
# min, max, step = None, None, None
# input_type = 'number'
# hx_target, hx_swap = None, 'none'
# @ar('/value', methods=['POST'])
# def changed(self, value:int):
# with self._updating(): self.value = value
# return self.value # fasthtml handles 0s from route handlers as falsy, not bona fide values
# def value_control(self):
# return Input(type=self.input_type, name='value', value=self.value, min=self.min, max=self.max, step=self.step,
# hx_post=f"/{self.ar.to()}/value", hx_trigger='input changed throttle:100ms',
# hx_target=self.hx_target, hx_swap=self.hx_swap)IntText
# class IntText(Value, _Int):
# value = T.CInt()# v = IntText(value=5)
# with bridge_cfg(auto_show=False):
# display(v.__ft__())
# v# v.value# v.value = 17BoundedInt
# class BoundedInt(IntText):
# min = T.CInt(0)
# max = T.CInt(100)
# step = T.Int(1)
# def value_control(self): return super().value_control()(min=self.min, max=self.max, step=self.step)# v = BoundedInt(value=10, min=5, max=15)
# with bridge_cfg(auto_show=False):
# display(v.__ft__())
# v# v.value# v.max = 17IntSlider
# class IntSlider(BoundedInt):
# readout:bool=True; readout_format:str='d'
# _wrapper_class = 'slider'
# def value_control(self):
# return Input(type='range', name='value', min=self.min, max=self.max, step=self.step, value=self.value,
# hx_post=f"/{self.ar.to()}/value", hx_trigger='input changed throttle:100ms',
# hx_target='next text', hx_swap='textContent')
# def setup_controls(self):
# return (Label(_for='value')('Scale'), _n,
# self.value_control(),
# Text(id='spanscale', style='inline')(self.value), _n)# iv = IntSlider()
# with bridge_cfg(auto_show=False):
# display(iv.__ft__())
# iv# iv.value# iv.value = 77# iv2 = IntSlider(step=2)
# iv2# iv3 = IntSlider()
# iv4 = IntSlider()
# # brt.mount(iv3, show=False)
# # brt.mount(iv4, show=False)
# T.link((iv3, 'value'), (iv4, 'value'))
# box = Div(style='display: flex; gap: 1em;')(iv3, iv4)
# box# iv3.value = 22
# test_eq(iv4.value, 22)Simple Widget (psignal) - Not working
Output
out = W.Output()
outmsg_id = None
with out:
msg_id = out.msg_id
out.clear_output()
display(Text('Hello, world!'))
msg_idout.append_display_data(HTML('<div id="asdf">asdf</div>'))
out.append_display_data(Div(style='color: red;')(Text('Hi!')))
out.append_display_data(Markdown('Hi!'))# o = HTML('<div id="asdf">asdf</div>')
# display(o)from IPython.utils.capture import RichOutput, CapturedIO, capture_outputwith capture_output() as io:
display(Div(style='color: red;')(Text('Hi!')), HTML('<div id="asdf">asdf</div>'), 'Hi!', Markdown('Hi!'))
# io.outputs
# io.show()io()def _capture(*objs, **kwargs):
if not InteractiveShell.initialized():
raise RuntimeError('IPython not initialized')
outs = []
format = InteractiveShell.instance().display_formatter.format
for obj in objs:
format_dict, md_dict = format(obj)
if not format_dict:
# nothing to display (e.g. _ipython_display_ took over)
outs.append((None, None))
continue
outs.append((format_dict, md_dict))
return outs
_capture(Div(style='color: red;')(Text('Hi!')), HTML('<div id="asdf">asdf</div>'), 'Hi!', Markdown('Hi!'))def _capture_output(*objs, **kwargs):
with capture_output() as io:
display(*objs, **kwargs)
return io
capt = _capture_output(Div(style='color: red;')(Text('Hi!')), HTML('<div id="asdf">asdf</div>'), 'Hi!', Markdown('Hi!'))
capt()
for c in capt.outputs:
print(c._repr_mimebundle_())class Output(Widget):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.contents = []
Bridget().mount(self, show=False)
# def __ft__(self):
# return super().__ft__()(id=self.idx)
# def _ipython_display_(self):
# Bridget()(self, display_id=self.idx)
def clear_output(self, wait=False):
self.contents = []
if not wait and self.dhdl: self.dhdl.update()
# def capture(self, clear_output=False, *clear_args, **clear_kwargs):
# pass
# @contextmanager
# def managed_resource(*args, **kwds):
# # Code to acquire resource, e.g.:
# resource = acquire_resource(*args, **kwds)
# try:
# yield resource
# finally:
# # Code to release resource, e.g.:
# release_resource(resource)
def __enter__(self):
self._capt = capture_output()
self._cio = self._capt.__enter__()
def __exit__(self, etype, evalue, tb):
bridge_cfg.current_did = None
self._capt.__exit__(etype, evalue, tb)
outputs, self._cio, self._capt = self._cio.outputs, None, None
if not self.dhdl:
dhdl = display(display_id=True)
self.dhdl = DisplayId(dhdl.display_id)
dhdl = self.dhdl.display_id
for c in outputs:
mime = {'data': c.data, 'metadata': c.metadata}
self.contents.append(mime)
display(mime['data'], metadata=mime['metadata'], raw=True, update=True, display_id=dhdl)
# def _flush(self):
# pass
def append_stdout(self, text):
brt = self.bridget
brt.swap(f"#{self.ar.name}", text+'<br>', swapStyle='beforeend')
self.contents.append(text)
def append_stderr(self, text):
brt = self.bridget
brt.swap(f"#{self.ar.name}", text+'<br>', swapStyle='beforeend')
self.contents.append(text)
def append_display_data(self, display_object):
with capture_output() as io:
display(display_object)
mimebundle = io.outputs[0]._repr_mimebundle_()
html = mimebundle[0]['text/html']
brt = self.bridget
brt.swap(f"#{self.ar.name}", html, swapStyle='beforeend')
self.contents.append(display_object)out = Output()
with bridge_cfg(auto_show=False):
display(out.__ft__())
outout(Text('Hello, world!'))out.append_stdout('Hello, universe!')out.append_display_data(Div(style='color: red; padding: 0px;')(Text('Hello, red!')))
out.append_display_data(Button('adfrt'))out.contentsout = Output()
with out:
display('adfrt')
# out.contentsout.append_display_data(Div(style='color: red; padding: 0px;')(Text('Hello, red!')))out.contentsimport matplotlib.pyplot as pltwith out:
fig = plt.figure(figsize=(4, 2.67))
plt.plot([1,2,3,4])
# plt.show()
out.contents# with out:
# plt.plot([1,2,3,4])
# plt.show()out = Output(layout={'border': '1px solid black'})
outwith out:
for i in range(10):
print(i, 'Hello world!')from IPython.display import YouTubeVideowith out:
display(YouTubeVideo('eWzY2nGfkXk'))with out:
display(widgets.IntSlider())out = widgets.Output(layout={'border': '1px solid black'})
out.append_stdout('Output appended with append_stdout')
out.append_display_data(YouTubeVideo('eWzY2nGfkXk'))
out@out.capture()
def function_with_captured_output():
print('This goes into the output widget')
raise Exception('As does this')
function_with_captured_output()a = widgets.IntSlider(description='a')
b = widgets.IntSlider(description='b')
c = widgets.IntSlider(description='c')
def f(a, b, c):
print('{}*{}*{}={}'.format(a, b, c, a*b*c))
out = widgets.interactive_output(f, {'a': a, 'b': b, 'c': c})
widgets.HBox([widgets.VBox([a, b, c]), out])interactive_output
interactive
def f(a, b):
display(a + b)
return a+bw = W.interactive(f, a=10, b=20)
wfrom ipywidgets import interactive
import matplotlib.pyplot as plt
import numpy as np
def f(m, b):
plt.figure(2)
x = np.linspace(-10, 10, num=1000)
plt.plot(x, m * x + b)
plt.ylim(-5, 5)
plt.show()
interactive_plot = interactive(f, m=(-2.0, 2.0), b=(-3, 3, 0.5))
output = interactive_plot.out#children[-1]
output.layout.height = '420px'
interactive_plotinteract
def f(x):
return xinteract(f, x=10);interact(f, x=True);interact(f, x='Hi there!');@interact(x=True, y=1.0)
def g(x, y):
return (x, y)IntSlider(min=-10, max=30, step=1, value=10)interact(f, x=widgets.IntSlider(min=-10, max=30, step=1, value=10));