Synchronous API¶
The sync API lives in pgwidgets.sync. Widget constructors and method
calls block until the browser responds.
from pgwidgets.sync import Application
Application¶
app = Application(
ws_port=9500, # WebSocket port
http_port=9501, # HTTP server port
host="127.0.0.1", # bind address
http_server=True, # serve JS/CSS assets
concurrency_handling="per_session", # callback threading model
max_sessions=1, # max concurrent sessions (None=unlimited)
logger=None, # logging.Logger or None
)
Properties:
app.url– URL to open in a browser (e.g.http://127.0.0.1:9501/).app.sessions– dict of active sessions (session_id -> Session).app.static_path– path to the pgwidgets static files directory.app.remote_html– path to theremote.htmlconnector page.
Methods:
app.create_session(session_id=None)– create a session without a browser. Returns aSessionthat can have its widget tree built before any browser connects.
on_connect / on_disconnect¶
Register handlers as decorators or by calling directly.
on_connect fires when a new session is created (not on reconnection).
on_disconnect fires each time a browser disconnects.
@app.on_connect
def setup(session):
Widgets = session.get_widgets()
top = Widgets.TopLevel(title="Hello")
top.show()
@app.on_disconnect
def teardown(session):
print(f"Session {session.id}: browser disconnected "
f"({len(session.connections)} remaining)")
Running¶
# Option 1: run() calls start() automatically and blocks forever
app.run()
# Option 2: start() + your own main loop
app.start()
# ... do other things ...
app.close() shuts down all sessions and stops the servers.
Custom fonts¶
Two methods on Application push custom font files to the
browser and apply a document-wide default font. Both broadcast
to every connected session immediately, and replay the entire
registry + default font to every new / reconnecting session
before on_connect / reconstruct runs – so any widget
that takes set_font(family, ...) always finds the face
already declared on the JS side.
# Register one or more faces for a family
app.register_font('Roboto', '/path/to/Roboto-Regular.ttf')
app.register_font('Roboto', '/path/to/Roboto-Bold.ttf',
weight='bold')
app.register_font('Roboto', '/path/to/Roboto-Italic.ttf',
style='italic')
# Apply a document-wide default
app.set_default_font('Roboto', size=13)
# Later use it on a single widget (or anywhere CSS sees it)
label.set_font('Roboto', 14)
sourceis a path (str/os.PathLike) or rawbytes. Accepted formats:.ttf,.otf,.woff,.woff2– the Content-Type is set from the extension.weightaccepts CSS keywords, numeric strings, and common TTF metadata names (thin,light,medium,semibold,extrabold,black,heavy, etc.) – the JS handler normalises descriptive names to numeric CSS values per the CSS Fonts spec.styleaccepts'normal','italic', or'oblique'; synonyms ('roman','slanted') are normalised too.set_default_font(family=None)clears the override.
Bytes are served from /_pgwidgets/font/<id> by the built-in
HTTP server with Cache-Control: immutable; behind Flask /
nginx the same route is served from the Python process.
See also the JS-side write-up at custom-fonts in the pgwidgets-js documentation.
Opening a URL in the browser¶
app.open_url('https://example.com/')
open_url(url) asks the connected browser(s) to open url in a
new tab/window (window.open) – i.e. in the user’s browser, not
on the host running Python. It is a fire-and-forget, per-session
broadcast. (A browser popup blocker may suppress it if the
user-activation from the originating click has already lapsed.)
Session¶
Sessions persist independently of browser connections. A session can exist with no browser connected and can have multiple browsers connected simultaneously.
Key methods:
Widgets = session.get_widgets() # widget factory namespace
session.close() # close browser connections (session survives)
session.destroy() # destroy session completely
session.make_timer(duration=0) # create a Timer widget
# Schedule work on the callback thread
session.gui_do(func, *args) # fire-and-forget
result = session.gui_call(func, *args) # block for result
# Widget tree inspection
for widget in session.walk_widget_tree():
print(widget)
Properties:
session.id– unique session identifier (int).session.app– the owningApplication.session.token– security token for reconnection.session.is_connected–Trueif at least one browser is connected.session.connections– list of active WebSocket connections.
Creating Sessions Without a Browser¶
Use app.create_session() to create a session before any browser connects.
Build the widget tree, then connect a browser to see the pre-built UI:
app.start()
session = app.create_session()
Widgets = session.get_widgets()
top = Widgets.TopLevel(title="Pre-built")
top.show()
# Navigate a browser to the session URL to see the UI.
Multi-Browser Support¶
Multiple browsers can connect to the same session by navigating to the session URL (which includes the session ID and security token). All browsers show the same UI and stay synchronized:
State changes in one browser are pushed to all others in real time.
Widget positions, slider values, tab selections, tree expand/collapse state, and more are all synchronized.
Callbacks fire only in response to the originating browser’s action; other browsers receive silent updates that don’t trigger callbacks.
Widget Factory¶
session.get_widgets() returns a namespace with factory methods for all
widget types:
Widgets = session.get_widgets()
top = Widgets.TopLevel(title="Demo", resizable=True)
vbox = Widgets.VBox(spacing=8, padding=10)
btn = Widgets.Button("Click me")
label = Widgets.Label("Status", halign="center")
slider = Widgets.Slider(min=0, max=100, value=50, track=True)
Constructor arguments match the widget definitions: positional args first,
then options as keyword arguments. Extra keyword arguments that do not match
a defined option are applied as set_<name>() calls.
Concurrency Modes¶
The concurrency_handling parameter controls how widget callbacks are
dispatched:
- per_session (default)
Each session gets its own thread. Callbacks within a session are dispatched sequentially. Different sessions run concurrently. No locks needed within a session.
- serialized
All callbacks from all sessions run on the main thread inside
run(). Simple single-threaded model, but one slow callback blocks everything.- concurrent
Each callback fires on its own daemon thread. Maximum concurrency, but you must manage thread safety for shared state.
gui_do and gui_call¶
In per_session and serialized modes, use gui_do and gui_call
to schedule work on the callback thread from other threads:
import threading
def background_work(session):
# This runs on a background thread
result = expensive_computation()
# Schedule UI update on the callback thread
session.gui_do(lambda: label.set_text(str(result)))
threading.Thread(target=background_work, args=(session,)).start()
gui_call blocks until the scheduled function completes and returns its
return value. gui_do returns immediately.
Full Example¶
import logging
from pgwidgets.sync import Application
logging.basicConfig(level=logging.INFO)
logger = logging.getLogger("pgwidgets")
app = Application(max_sessions=4, logger=logger)
@app.on_connect
def on_session(session):
Widgets = session.get_widgets()
top = Widgets.TopLevel(title="Sync Demo", resizable=True)
top.resize(400, 300)
vbox = Widgets.VBox(spacing=8, padding=10)
status = Widgets.Label("Click a button!")
hbox = Widgets.HBox(spacing=6)
btn_hello = Widgets.Button("Hello")
btn_world = Widgets.Button("World")
hbox.add_widget(btn_hello, 0)
hbox.add_widget(btn_world, 0)
entry = Widgets.TextEntry(text="Type here", linehistory=5)
slider = Widgets.Slider(min=0, max=100, value=50, track=True)
vbox.add_widget(hbox, 0)
vbox.add_widget(entry, 0)
vbox.add_widget(slider, 0)
vbox.add_widget(status, 1)
top.set_widget(vbox)
top.show()
btn_hello.on("activated", lambda: status.set_text("Hello!"))
btn_world.on("activated", lambda: status.set_text("World!"))
entry.on("activated", lambda text: status.set_text(f"Entered: {text}"))
slider.on("activated", lambda val: status.set_text(f"Slider: {val}"))
app.run()