Bridge helpers



Helpers

Internal utilities for JS/Python interop. Most users won’t need these directly.


source

notdebug

 notdebug (jsstr:str)

source

debug

 debug (jsstr:str)

source

to_js

 to_js (*fts:fastcore.xml.FT|str)
a = Div(P('a'), P('aa'), id='a11')
b = P('b', Span('bb'))

test_eq(_to_js(a), ['div', [['p', ['a'], {}], ['p', ['aa'], {}]], {'id': 'a11'}])
test_eq(_to_js(b), ['p', ['b', ['span', ['bb'], {}]], {}])

test_eq(to_js(a, 'const a=`${"a"}`; console.log(a)', b), """[
  ['div', [['p', ['a'], {}], ['p', ['aa'], {}]], {'id': 'a11'}],
  'const a=`${"a"}`; console.log(a)',
  ['p', ['b', ['span', ['bb'], {}]], {}]
]""")
a = Script('const a=`${"a"}`; console.log(a)')
b = Script(src='https://unpkg.com/htmx.org@next/dist/htmx.js', type='module')

test_eq(repr(_to_js(a)), "['script', ['const a=`${\"a\"}`; console.log(a)'], {}]")

test_eq(to_js(a, b), """[
  ['script', ['const a=`${"a"}`; console.log(a)'], {}],
  ['script', [''], {'src': 'https://unpkg.com/htmx.org@next/dist/htmx.js', 'type': 'module'}]
]""")

Quick & dirty way to convert FT to HTML elements int the front-end using fasthtml-js $E.
Intended only for “linking”/head elements (with void or text content): script, style, link, meta, etc. It’ll surely fail with other elements.


source

ScriptsDetails

 ScriptsDetails (scs, title='Loaded scripts', open=True)

Initialize self. See help(type(self)) for accurate signature.

Bridge scripts

HTMX and other useful JS libraries.


source

show_scripts

 show_scripts (**scs:fastcore.xml.FT)

source

bridge_scripts

 bridge_scripts (htmx=True)
bridge_scripts()
{'htmx': script(('',),{'src': 'https://unpkg.com/htmx.org@next/dist/htmx.js'}),
 'fasthtml_js': script((),{'src': 'https://cdn.jsdelivr.net/gh/answerdotai/fasthtml-js@1.0.12/fasthtml.js'}),
 'surreal': script((),{'src': 'https://cdn.jsdelivr.net/gh/answerdotai/surreal@main/surreal.js'}),
 'css_scope_inline': script((),{'src': 'https://cdn.jsdelivr.net/gh/gnat/css-scope-inline@main/script.js'})}
show_scripts(**bridge_scripts())
Loaded scripts
<script src="https://unpkg.com/htmx.org@next/dist/htmx.js"></script>
<script src="https://cdn.jsdelivr.net/gh/answerdotai/fasthtml-js@1.0.12/fasthtml.js"></script>
<script src="https://cdn.jsdelivr.net/gh/answerdotai/surreal@main/surreal.js"></script>
<script src="https://cdn.jsdelivr.net/gh/gnat/css-scope-inline@main/script.js"></script>

BCanvas

FCanvas associated with a bridge.

Configure the bridge logging system to display log traces here in the notebook, a convenience that alleviates the need to open the browser console.

NOTE: BCanvas could be a Bridge plugin, but it’s a separate widget instead in order to use another comm channels. (must check if we gain something by having different Comm channels, in the end ZMQD uses one websocket under the hood, I think)


source

get_bcanvas

 get_bcanvas (height:int=200, elid:str='', **kwargs)
cleanupwidgets('cnv')
__bcanvas__ = None

cnv = get_bcanvas(timeout=DEBUG(2, 2))                
# test_eq(cnv.loaded(), True)  # in Lab something weird is happening here
cnv.show()
cnv.add('Why Universe, why?<br>')

BLogger

FLogger subclass used by Bridge.


source

BLogger

 BLogger (msg=None, canvas:bridget.logger.Canvas|None=None,
          height:int=200, show:bool=True, history:bool=True, **kwargs)

Simple logger that displays messages in a scrollable div

lgr = BLogger('BLogger initialized')
lgr.log('test')
lgr.error('test')
lgr.warn('test')
lgr.log('bbbb')
lgr.log('cccccc', True)
lgr.show()
lgr.log('ddddddddd')

Log directly.

Bridge bootstrap

Progressive bridge construction: Boot → Messenger → Bridge (with plugins). Each layer adds functionality. Use get_bridge() to get the full-featured bridge.

BridgeBoot

Simply setup the bridge and connect logging.

cleanupwidgets('brd')

brd = BridgeBoot.create(show_logger=True, timeout=DEBUG(2, 2), sleep=0.2)
test_eq(brd.loaded(), True)

Note create blocks, it waits for the widget to be loaded (or the default timeout, see BlockingMixin).

brd.error('Erred!')
brd.update_logger_config(color='darkgoldenrod')  # type: ignore
brd.log('my trea~~sssure')

lgr now that the bridge is opened reflects both frontend and kernel log messages and uses the JS bridge logging functionality.

scr = Script('''
const { bridge } = await brdimport('./bridge.js');
bridge.logger.log('Hi from JS land');
''', type='module')
display(HTML(scr))
time.sleep(0.1)
clear_output()

Log from JS land.

@FC.delegates(BridgeBoot, keep=True)  # type: ignore
def get_bridge(logger:NBLogger|None=None, show_logger:bool=False, **kwargs) -> BridgeBoot:
    if not __brd__:
        timeout, sleep = kwargs.pop('timeout', 2), kwargs.pop('sleep', 0.2)
        brd = BridgeBoot.create(logger=logger, show_logger=show_logger, timeout=timeout, sleep=sleep, **kwargs)
        return brd
    assert __brd__ is not None
    brd:BridgeBoot = __brd__
    if logger: brd.logger = logger
    if show_logger: brd.logger.show()
    return brd
brd = get_bridge(show_logger=True)
test_is(get_bridge(), brd)
brd.close()
test_is(__brd__ is None, True)

close() disconnect the widget and remove the widget model from the JS bridge, so it can no longer communicate with the Python side. Besides that, it has no effect on bcanvas, or the modules imported with brdimport. Create a new bridge to reconnect.

We need to use AnyWidget to setup the JS bridge, a class. The python side of the bridge should be created once per notebook, no sense to have several bridges around.

Given the nearly impossibility of creating class singletons in Python even with metaclasses, in previous refactorizations, I’ve used SingletonConfigurable a-la-getipython. It works very well but it uglify the code.

Anyway, given we’re-all-adults-here, you should always use get_bridge to access the bridge instance.

brd = get_bridge(show_logger=True)

Messages from the beyond

handle_message

class _A:
    ctx_names={'A'}
    def __init__(self): self.forward = _B()
    def on_info(self, *args, **kwargs): 
        print('A', 'info', f"{args=}", f"{kwargs=}")
    def _msg_fwrdr(self, *args, ctx:str, kind:str, **kwargs):
        if ctx == 'B':
            handle_message(self.forward, *args, ctx=ctx, kind=kind, **kwargs)
        else:
            print(f"{ctx=} Not forwarded")

class _B:
    ctx_names={'B'}
    def on_info(self, *args, **kwargs):
        print('B', 'info', f"{args=}", f"{kwargs=}")

a = _A()
test_stdout(
    lambda: handle_message(a, 'hello', ctx='A', kind='info', info='initialized'), 
    "A info args=('hello',) kwargs={'info': 'initialized'}\nctx='A' Not forwarded")
test_stdout(
    lambda: handle_message(a, 'other', ctx='B', kind='info', info='forwarded'), 
    "B info args=('other',) kwargs={'info': 'forwarded'}")

BridgeMessenger

A bridge that can receive messages from JS land.

BridgeBoot just loads the JS bridge and setup logging. Here we set the basis for more powerful messaging to and fro JS.

@FC.delegates(BridgeMessenger, keep=True)  # type: ignore
def get_bridge(logger:NBLogger|None=None, show_logger:bool=False, **kwargs):
    if not __brd__: 
        timeout, sleep = kwargs.pop('timeout', 3), kwargs.pop('sleep', 0.2)
        brd = BridgeMessenger.create(logger=logger, show_logger=show_logger, timeout=timeout, sleep=sleep, **kwargs)
        return brd
    assert __brd__ is not None
    brd = __brd__
    if logger: brd.logger = logger
    if not logger and show_logger: brd.logger.show()
    return brd
cleanupwidgets('brd')

brd = get_bridge(show_logger=True, timeout=DEBUG(2, 2))
brd.debug_enabled('loader', enabled=True)
brd.send(brd.msg(ctx='brd', cmd='echo', args='test'))
msg = dict(ctx='loader', cmd='load', args={'confetti': 'https://esm.sh/canvas-confetti@1.6'}, timeout=DEBUG(1))
brd.send(msg)
observer_loader = anysource('''
const { getObserverManager } = await brdimport('./observer.js');
console.log(getObserverManager)
''')

brd.send(dict(ctx='loader', cmd='load', args={'get_observer': observer_loader}, reload=True), timeout=DEBUG(1))
({'ctx': 'loader',
  'kind': 'load',
  'success': ['get_observer'],
  'failed': [],
  'msg_id': 'msg-8'},
 [])
src = '''
const res = await brdimport('https://a.com/b/c.js');
console.log(res)
'''

brd.send(dict(ctx='loader', cmd='load', args={'willfail': src}, reload=True), timeout=DEBUG(1))
({'ctx': 'loader',
  'kind': 'load',
  'success': [],
  'failed': [{'name': 'willfail',
    'error': 'Failed to fetch dynamically imported module: https://a.com/b/c.js'}],
  'msg_id': 'msg-9'},
 [])

anysource doesn’t transform its arguments. Use bundled (i.e., Bundle.__call__(..., transform=True), the default), instead.

You can disabled JS transform completely with bridge.cfg.bundle_cfg.rewrite_imports.

src = bundled('''
import confetti from 'https://esm.sh/canvas-confetti@1.6';
console.log(confetti)
''')(transform=False)

brd.send(dict(ctx='loader', cmd='load', args={'load_confetti': src}), timeout=DEBUG(1))
({'ctx': 'loader',
  'kind': 'load',
  'success': ['load_confetti'],
  'failed': [],
  'msg_id': 'msg-10'},
 [])

Check the JS console network tab, we imported the confetti module previously.

src = anysource('''
const { confetti } = await brdimport('https://esm.sh/canvas-confetti@1.6');
console.log(confetti)
''')

brd.send(dict(ctx='loader', cmd='load', args={'load_confetti': src}, reload=True), timeout=DEBUG(1))
({'ctx': 'loader',
  'kind': 'load',
  'success': ['load_confetti'],
  'failed': [],
  'msg_id': 'msg-11'},
 [])

brdimport doesn’t re-import confetti too.

Note the reload argument of the send method. bridge caches the modules by name and URL, and we’re reusing load_confetti name.

Use cache=False if you don’t want bridge caching the module.

Linking elements loader

script (and other links) loader for notebooks.

FastHTML way of loading head elements is fine with standard web apps, head links are evaluated in order (unless they have async or defer attributes) if present in the HTML source.

In notebooks, we need to load those head elements dinamically in order (in head, body or anywhere of the front-end page). And we want to load fasthtml.js (and htmx.js because why not) as soon as possible, so we can use them in the same cell to define our JS extensions.

Assuming we’ve already loaded fasthtml.js, this is a possible solution:


source

BridgePlugin


source

BridgePlugin

 BridgePlugin (ctx:str='', src:str|pathlib.Path='', bridge=None)

Inherit from this to have all attr accesses in self._xtra passed down to self.default

plg = BridgePlugin('test', 'a=10')
test_eq(plg.ctx_name, 'test')
test_eq(plg.ctx_names, {'test'})
test_eq(plg.src, 'a=10')

Bridge

BridgeWidget + plugins to extend Bridge functionality in Python and/or JavaScript.

BridgeWidget contains the core functionality, logging and JS loading. All other stuff in this project will be developed with plugins.

def get_bridge(*plugins:BridgePlugin, kwplugins:dict[str, str]|None=None, 
    logger:NBLogger|None=None, show_logger:bool=False, **kwargs):
    "Get the bridge, creating it if not found."
    if not __brd__: 
        timeout, sleep = kwargs.pop('timeout', 2), kwargs.pop('sleep', 0.2)
        brd = Bridge.create(*plugins, kwplugins=kwplugins, logger=logger, show_logger=show_logger, 
                            timeout=timeout, sleep=sleep, **kwargs)
        return brd
    assert __brd__ is not None
    brd = __brd__
    if logger: brd.logger = logger
    if not logger and show_logger: brd.logger.show()
    return brd
cleanupwidgets('brd')

brd = get_bridge(show_logger=True, timeout=DEBUG(2, 2))
test_eq(brd.plugins.keys(), set(('loader', 'htmx', 'fasthtmljs')))
brd.add_plugins(badp := BridgePlugin('badp', '''export default function badp(bridge) { a = 1/0 }'''))
blocks(lambda: badp.is_initialized != None, 1)
test_is(brd.badp.is_initialized, False)

InspectPlugin

class InspectPlugin(BridgePlugin):
    src = '''
function inspect(msg) {
    const {ctx, kind} = msg;
    if (kind === 'echo') {
        bridge.logger.log('echo', msg);
        setTimeout(() => {
            bridge.model.send({ ctx: ctx, kind: 'echo', msg: msg, msg_id: msg?.msg_id })
        }, 100);
        return;
    }
    bridge.model.send({ ctx: 'inspect', kind: 'inspect', msg: msg, msg_id: msg?.msg_id })
}
export default function initializeInspect(bridge) {
    bridge.on('inspect', inspect);
    return () => bridge.off('inspect');
}
'''
    ctx_name = 'inspect'

    def on_inspect(self, *args, msg:Any, tracker:Any, **kwargs):
        self.log(f"{self.__class__.__name__} inspect: {msg=} {tracker=}")
brd.logger.show(clear=True)
brd.add_plugins(insp := InspectPlugin())
blocks(lambda: insp.is_initialized is not None, 3)
test_is('inspect' in brd.plugins, True)

add_plugin does not blocks. If needed, use blocks or blocking to ensure the plugin is loaded.

# content, _ = await insp.asend(msg := insp.msg(ctx='inspect', kind='echo', data={'a': 1}))  # asend not working in Lab
content, _ = insp.send(msg := insp.msg(ctx='inspect', kind='echo', data={'a': 1}), timeout=DEBUG(1))
test_eq(content, {'ctx': 'inspect', 'kind': 'echo', 'msg': msg, 'msg_id': msg['msg_id']})
content, _ = insp.send(msg := insp.msg({'tracker': 'test'}, ctx='inspect', kind='echo', data={'b': 2}), timeout=DEBUG(1))
test_eq(content, {'ctx': 'inspect', 'kind': 'echo', 'msg': msg, 'msg_id': msg['msg_id']})
insp.src = '''
const logger = bridge.logger.create({ ns: 'inspect', color: 'green' });
function inspect(msg) {
    const {ctx, kind} = msg;
    if (kind === 'echo') {
        logger.log('echo', msg);
        setTimeout(() => {
            bridge.model.send({ ctx: ctx, kind: 'echo', msg: msg, msg_id: msg?.msg_id })
        }, 100);
        return;
    }
    bridge.model.send({ ctx: 'inspect', kind: 'inspect', msg: msg, msg_id: msg?.msg_id })
}
export default async function initializeInspect(bridge) {
    bridge.on('inspect', inspect);
    return () => { bridge.off('inspect'); logger.close(); }
}
'''
brd.logger.show(clear=True)
brd.add_plugins(insp)
insp.log("Look Ma', now greeny!")
content, _ = insp.send(msg := insp.msg(ctx='inspect', kind='echo', data={'a': 1}), timeout=2)
test_eq(content, {'ctx': 'inspect', 'kind': 'echo', 'msg': msg, 'msg_id': msg['msg_id']})

We can re-load any plugin by calling addPlugins again. See above insp now uses a new logger.

In the examples folder, there’s an inspect plugin more fully developed.

Loader

Convenience python-side plugin for loading scripts and ESMs.

brd.logger.show(clear=True)
loader = Loader(dict(
    test=Script('// debugger;\nconsole.log("test")', id='test-script'),
    test2=Script('// debugger;\nconsole.log(a)', id='test-script2')
))
brd.add_plugins(loader)
test_is(brd.loader.loading, True)
test_is(brd.loader, loader)
loader.load_links({
    'test3': Script('// debugger;\nbridge.logger.log("test3")', id='test-script3'),
    'test4': Script('// debugger;\nbridge.logger.log(a)', id='test-script4')
})
loader.load({'htmx1': '''
// debugger;
import htmx from "https://unpkg.com/htmx.org@next/dist/htmx.esm.js";
console.log(htmx);
''',
    'htmx2': '''
// debugger;
import htmx from "https://unpkg.com/htmx.org@next/dist/XXXX.esm.js";
console.log(htmx);
'''})
blocks(lambda: loader.loaded('htmx1'), 2, show=_show)  # needed when running all above/below cells

test_eq(loader.loaded().keys(), set(('test', 'test3', 'htmx1')))
._.
test_eq(brd.loader.loaded('htmx1'), True)
test_eq(brd.loader.loaded('nah'), False)

Loader can be used to execute any JS code (as an EcmaScript module). Note that unlike IPython Javacript, there’s no output, the code won’t run on opening the notebook until explicitly running the cell.

brd.logger.show()
display(HTML(
    '<button type="button" onclick="const canvas=document.getElementById(\'my-canvas\');\ncanvas.confetti({spread:70, particleCount:100, origin: { y: 1 }})">Fire!</button><br>'
    '<canvas id="my-canvas" width="1000px" height="200px"></canvas>'
))
time.sleep(0.5)
loader.load({'confetti': '''
import confetti from "https://esm.sh/canvas-confetti@1.6";

function randomInRange(min, max) {
  return Math.random() * (max - min) + min;
}

const canvas = document.getElementById('my-canvas');
canvas.confetti = canvas.confetti || confetti.create(canvas, { resize: true });
'''}, reload=True)

loader.load({'beep': '''
// debugger;
export function beep() {
    var snd = new Audio("data:audio/wav;base64,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qJrx4KOQjahZxWbcZgztj2c49nKmkId44S71j0c8eV9yDK6uPRzx5X18eDvjvQ6yKo9ZSS6l//8elePK/Lf//IInrOF/FvDoADYAGBMGb7FtErm5MXMlmPAJQVgWta7Zx2go+8xJ0UiCb8LHHdftWyLJE0QIAIsI+UbXu67dZMjmgDGCGl1H+vpF4NSDckSIkk7Vd+sxEhBQMRU8j/12UIRhzSaUdQ+rQU5kGeFxm+hb1oh6pWWmv3uvmReDl0UnvtapVaIzo1jZbf/pD6ElLqSX+rUmOQNpJFa/r+sa4e/pBlAABoAAAAA3CUgShLdGIxsY7AUABPRrgCABdDuQ5GC7DqPQCgbbJUAoRSUj+NIEig0YfyWUho1VBBBA//uQZB4ABZx5zfMakeAAAAmwAAAAF5F3P0w9GtAAACfAAAAAwLhMDmAYWMgVEG1U0FIGCBgXBXAtfMH10000EEEEEECUBYln03TTTdNBDZopopYvrTTdNa325mImNg3TTPV9q3pmY0xoO6bv3r00y+IDGid/9aaaZTGMuj9mpu9Mpio1dXrr5HERTZSmqU36A3CumzN/9Robv/Xx4v9ijkSRSNLQhAWumap82WRSBUqXStV/YcS+XVLnSS+WLDroqArFkMEsAS+eWmrUzrO0oEmE40RlMZ5+ODIkAyKAGUwZ3mVKmcamcJnMW26MRPgUw6j+LkhyHGVGYjSUUKNpuJUQoOIAyDvEyG8S5yfK6dhZc0Tx1KI/gviKL6qvvFs1+bWtaz58uUNnryq6kt5RzOCkPWlVqVX2a/EEBUdU1KrXLf40GoiiFXK///qpoiDXrOgqDR38JB0bw7SoL+ZB9o1RCkQjQ2CBYZKd/+VJxZRRZlqSkKiws0WFxUyCwsKiMy7hUVFhIaCrNQsKkTIsLivwKKigsj8XYlwt/WKi2N4d//uQRCSAAjURNIHpMZBGYiaQPSYyAAABLAAAAAAAACWAAAAApUF/Mg+0aohSIRobBAsMlO//Kk4soosy1JSFRYWaLC4qZBYWFRGZdwqKiwkNBVmoWFSJkWFxX4FFRQWR+LsS4W/rFRb/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////VEFHAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAU291bmRib3kuZGUAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAMjAwNGh0dHA6Ly93d3cuc291bmRib3kuZGUAAAAAAAAAACU=");  
    snd.play();
}
window.$beep = beep;
'''})
display(HTML('<button type="button" onclick="window.$beep()">Beep!</button>'))

HTMX plugin


source

HTMXPlugin

 HTMXPlugin (ctx:str='', src:str|pathlib.Path='', bridge=None)

Inherit from this to have all attr accesses in self._xtra passed down to self.default

brd.logger.show(clear=True)
brd.add_plugins(htmxp := HTMXPlugin())
blocks(lambda: htmxp.is_initialized is not None, 2, show=_show)  # needed when running all above/below cells
._.
True
htmxp.setup()

ObserverManager

MutationObserver manager for bridget

bundled(observer_js)(debugger=DEBUG(), ts=True);
brd.logger.show(clear=True)
observer_scr = Script(bundled('''
import { getObserverManager } from './observer.js';
getObserverManager();
''')(), 
    type='module', id='brd-get-observer-manager')
loader.load_links({'get_observer': observer_scr})  # load_links is async

ObserverManager is a normal ES module, just import it. But for local development in notebooks, or until we register it, we can use the loader or whatever of the many methods we now have to load JS code..

loader.loaded()
{'test': script(('// debugger;\nconsole.log("test")',),{'id': 'test-script'}),
 'test3': script(('// debugger;\nbridge.logger.log("test3")',),{'id': 'test-script3'}),
 'htmx1': None,
 'confetti': None,
 'beep': None,
 'get_observer': script(('\nconst {getObserverManager} = await brdimport("./observer.js");\ngetObserverManager();\n',),{'type': 'module', 'id': 'brd-get-observer-manager'})}
brd.logger.show()
brd.loader.brdimport('./observer.js')
blocks(lambda: loader.loaded('./observer.js'), 2, show=_show)  # needed when running all above/below cells
._.
True
test_eq(loader.loaded('./observer.js'), True)

We can also use with Python the equivalent to ES5 relative import declaration (don’t forget to bundled your source or use brdimport directly).

Front-end:

import { getObserverManager } from './observer.js';
const observer= getObserverManager();

Kernel:

get_bridge().loader.brdimport('./observer.js')
observer_plugin = anysource('''
export default async function initializeObserverPlugin(bridge) {
    const { getObserverManager } = await brdimport('./observer.js');
    return [null, { getObserverManager }]
}
''')

or:

brd.logger.show(clear=True)
brd.add_plugins(kwplugins={'observer':observer_plugin})
[*brd.plugins.keys()]
['loader', 'htmx', 'fasthtmljs', 'badp', 'inspect', 'observer']

We could also add observerManager as a plugin.

brd-mark

Bridge plugin that defines a custom element that adds data- attributes to its parent and remove itself. It also processes the parent with htmx.

brdmark_plugin = bundled(brdmark_js)()#(debugger=DEBUG(), ts=True)

We could load brd_mark directly, but as it depends on the bridge (for logging), better to load it as a plugin.

brd.logger.show()
brd.add_plugins(kwplugins={'brd_mark':brdmark_plugin})
marker = 'aaaa<brd-mark id="marker-123">'
display(HTML(marker))
aaaa
print('asdf\nwerwqert')
asdf
werwqert
display(HTML('<div class="bridge">aaaa</div><brd-mark id="marker-1234"></brd-mark>'))
display(HTML('<div class="bridge">bbbb</div><brd-mark id="marker-12345"></brd-mark>'))
aaaa
bbbb
HTML(Brd_Mark(id=new_id()))

get_bridge


source

show_summary

 show_summary (brd:__main__.Bridge)
brd.logger.show()
brd.loader.load_links({
    'surreal':bridge_scripts()['surreal'],
    'css_scope_inline':bridge_scripts()['css_scope_inline'],
    })

source

get_bridge

 get_bridge (logger:NBLogger|None=None, show_logger:bool=False,
             lnks:dict[str,FT]|None=None,
             esms:dict[str,str|Path]|None=None,
             plugins:Sequence[BridgePlugin]|None=None,
             kwplugins:dict[str,str]|None=None, wait:int=0,
             summary:bool=False, factory:Callable[...,Any]|None=None,
             timeout:float=10, sleep:float=0.2, n:int=10,
             show:Callable[[bool],None]|None=None, **kwargs)
Type Default Details
logger NBLogger | None None
show_logger bool False
lnks dict[str, FT] | None None
esms dict[str, str | Path] | None None
plugins SequenceBridgePlugin | None None
kwplugins dict[str, str] | None None
wait int 0 seconds to wait for plugins/links/modules to load
summary bool False
factory Callable[…, Any] | None None
timeout float 10
sleep float 0.2
n int 10
show Callable[[bool], None] | None None
kwargs VAR_KEYWORD
nbdev.show_doc(get_bridge)

source

get_bridge

 get_bridge (logger:NBLogger|None=None, show_logger:bool=False,
             lnks:dict[str,FT]|None=None,
             esms:dict[str,str|Path]|None=None,
             plugins:Sequence[BridgePlugin]|None=None,
             kwplugins:dict[str,str]|None=None, wait:int=0,
             summary:bool=False, factory:Callable[...,Any]|None=None,
             timeout:float=10, sleep:float=0.2, n:int=10,
             show:Callable[[bool],None]|None=None, **kwargs)
Type Default Details
logger NBLogger | None None
show_logger bool False
lnks dict[str, FT] | None None
esms dict[str, str | Path] | None None
plugins SequenceBridgePlugin | None None
kwplugins dict[str, str] | None None
wait int 0 seconds to wait for plugins/links/modules to load
summary bool False
factory Callable[…, Any] | None None
timeout float 10
sleep float 0.2
n int 10
show Callable[[bool], None] | None None
kwargs VAR_KEYWORD
test_is(get_bridge(show_logger=True), brd)
brd.close()
test_is(__brd__, None)
brd.logger.clear_log()

with bridge_cfg(bootstrap=True):
    brd = get_bridge(show_logger=True, wait=5, summary=True)
Loaded scripts
loader
htmx
fasthtmljs
observer
brd_mark
<script src="https://cdn.jsdelivr.net/gh/answerdotai/surreal@main/surreal.js"></script>
<script src="https://cdn.jsdelivr.net/gh/gnat/css-scope-inline@main/script.js"></script>
html = '''
<div>💩 👻 No style.</div>

<div>
    <style> /* Simple example. */
        me { margin: 20px; }
        me div { font-size: 5rem; }
    </style>
    <div>👻</div>
</div>
'''

display(HTML(html))
💩 👻 No style.
👻

Bridge automatically loads some JavaScript libraries:

  1. HTMX
  2. FastHTML core scripts
  3. Awesome gnat’s Scope and Surreal scripts

When importing bridge, if bridge_cfg.bootstrap is True or there is an environment variable BRIDGET_BOOTSTRAP set to a true-ish value, it will automatically create a bridge.