Skip to main content

anylumino

anylumino is a small proof-of-concept library for creating anywidget widgets that use Lumino for layout and Astryx for notebook UI components.

It targets the anywidget 0.11 AFM composition API: Python passes child anywidgets as synced widget references, and the frontend resolves each child with host.getWidget(...) before mounting it into a Lumino layout widget or an Astryx component slot.

The main benefit is composition. anylumino lets a notebook author build larger reusable widgets from existing anywidgets, Astryx controls, charts, tables, and outputs. A composed widget can own its layout, state, callbacks, and domain methods while still being an anywidget that can be nested into another layout.

Documentation: https://faysou.github.io/anylumino/docs/

Quick start

import anylumino as al
import anylumino.astryx as ax

brand = ax.Brand(
    "desk",
    **{
        "color-accent": ("#0057b8", "#79b8ff"),
        "color-background-card": ("#ffffff", "#111827"),
        "color-text-primary": ("#111827", "#f9fafb"),
        "radius-container": "8px",
    },
)

status = ax.Text("Ready")

orders = ax.Table(
    rows=[
        {"order_id": "O-1", "symbol": "AAPL", "qty": 10, "status": "NEW"},
        {"order_id": "O-2", "symbol": "MSFT", "qty": 5, "status": "PARTIAL"},
    ],
    columns={"order_id": "Order", "symbol": "Symbol", "qty": "Qty", "status": "Status"},
    row_key="order_id",
    selects="multiple",
    sortable=True,
    sort_key="symbol",
    width="100%",
)

controls = ax.Theme(
    {
        "heading": ax.Stack(
            {
                "title": ax.Heading("Orders", level=3),
                "badge": ax.Badge("Live", variant="success"),
            },
            direction="horizontal",
            gap=2,
            align="center",
        ),
        "symbol": ax.Typeahead(
            ["AAPL", "MSFT", "NVDA"],
            value="AAPL",
            label="Symbol",
        ),
        "fields": ax.Tokenizer(
            ["Bid", "Ask", "Last", "Size"],
            value=["Bid", "Ask"],
            label="Fields",
        ),
        "refresh": ax.Button(
            "Refresh",
            variant="primary",
            callbacks=[lambda _button: setattr(status, "text", "Refreshed")],
        ),
        "orders": orders,
    },
    brand=brand,
    mode="system",
    gap=3,
)

al.SplitPanel(
    {"controls": controls, "status": status},
    orientation="vertical",
    sizes=[0.8, 0.2],
    height=560,
)

What it provides

  • Layout widgets: public LayoutWidget base plus TabPanel, BoxPanel, HBox, VBox, ScrollBox, SplitPanel, DockPanel, AccordionPanel, StackedPanel, GridPanel, and ResponsivePanel.
  • Astryx layout panels with the same keyed child API and Astryx styling: TabPanel, StackedPanel, AccordionPanel, ScrollBox, SplitPanel, and ResponsivePanel in anylumino.astryx.
  • Astryx component wrappers for notebook UI: buttons, text, headings, badges, cards, stacks, grids, forms, fields, tabs, segmented controls, selectors, checkboxes, radio lists, sliders, date/time inputs, banners, status dots, progress bars, code, Markdown, tooltips, popovers, inline dialogs, static search inputs, command palettes, and tables.
  • Astryx table helpers: normalized rows and columns, optional checkbox selection, sorting, row append/prepend/update/remove helpers, and Python callbacks for selection and sort changes.
  • Reusable Astryx theming with Brand and Theme, so notebooks can define shared colors, radii, and component treatment once.
  • Action widgets: Toolbar, DropdownMenu, MoreMenu, and CommandPalette for notebook-safe commands.

All layout containers inherit from LayoutWidget, which provides keyed child composition, owner access, dynamic mutation, and method forwarding.

This turns layout into an application structure, not only visual placement. Children remain addressable by key, callbacks can interact with sibling widgets, and larger components such as OrderDashboard, FigurePanel, or SampleReview can be reused across notebooks without relying on global variables.

Layout composition

Layout widgets are resizable by drag and drop by default. Pass resizable=False to hide the bottom-right resize handle. SplitPanel also syncs Lumino split-handle changes back to its sizes trait.

Layout children can be declared with semantic keys. If titles is omitted for a mapping, the titles default to those keys.

tabs = al.TabPanel({"orders": orders, "logs": ax.Text("No events")})
tabs.select_key("orders")
tabs.selected_key
tabs.get_widget("logs")

If a child is a Python wrapper with a .widget anywidget view, anylumino renders the view and keeps the wrapper as the keyed owner.

tabs = al.TabPanel({"trend": trend_panel})
tabs.get_widget("trend")  # trend_panel.widget
tabs["trend"].refresh()
tabs.call_owner("trend", "refresh")

To add methods to a composed UI object, subclass a layout widget. The subclass is still an anywidget and can be nested inside another layout:

class OrderDashboard(al.VBox):
    def __init__(self, rows):
        self.table = ax.Table(
            rows,
            row_key="order_id",
            selects="multiple",
            sortable=True,
        )
        super().__init__({"table": self.table})

    def replace_orders(self, rows):
        self.table.set_rows(rows)


tabs = al.TabPanel({"orders": OrderDashboard(order_rows)})
tabs["orders"].replace_orders(next_order_rows)

Astryx widgets

Astryx widgets are the UI family for notebook-facing controls. They use the anylumino.astryx namespace so notebook component code stays explicit. Useful appearance and behavior options are keyword-only constructor arguments with Python snake-case names. Unmodeled JSON-safe upstream options can still be passed through **props. See the Astryx component documentation for the upstream component reference.

form = ax.FormLayout(
    {
        "symbol": ax.TextInput(value="AAPL", label="Symbol"),
        "quantity": ax.NumberInput(value=100, label="Quantity", min=1),
        "side": ax.SegmentedControl(
            ["Buy", "Sell"],
            value="Buy",
            label="Side",
        ),
        "routing": ax.Selector(
            ["Smart", "Primary", "Dark"],
            value="Smart",
            label="Routing",
        ),
        "urgent": ax.Checkbox(value=False, label="Urgent"),
    }
)

Read an input widget's current value with .value. Use traitlets observers when another widget should react to edits:

symbol = ax.TextInput(value="AAPL", label="Symbol")
quantity = ax.NumberInput(value=100, label="Quantity")

symbol.value
quantity.value

symbol.observe(lambda change: print(change["new"]), names="value")

Text inputs, number inputs, and sliders send every keystroke and drag step to Python. Pass continuous_update=False when a value should arrive only once the edit ends, which for inputs means blur or Enter and for sliders means the end of the drag.

notes = ax.TextArea(value="", label="Notes", continuous_update=False)
risk = ax.Slider(value=42, label="Risk budget", continuous_update=False)

Astryx Slider steps linearly, so two wrappers adapt it for scales it does not model directly. LogSlider drives it in exponent space and reports base ** exponent; SelectionSlider drives it over option indices and reports the option, taking a two-item value for a range.

rate = ax.LogSlider(1e-3, label="Learning rate", base=10, min_exponent=-4, max_exponent=0)
size = ax.SelectionSlider(["XS", "S", "M", "L"], value="M", label="Size", marks=True)
window = ax.SelectionSlider(["Mon", "Tue", "Wed", "Thu", "Fri"], value=["Tue", "Thu"], label="Window")

To mirror one widget's state onto another, prefer traitlets.link over a callback:

import traitlets

mirror = ax.Text("AAPL")
traitlets.link((symbol, "value"), (mirror, "text"))

Button-like widgets use callbacks because a click is an event, not a persistent value. Every anylumino widget family registers activations the same way: on_click(callback) runs for each activation and receives the widget, while on_action(callback) runs only for activations that carry a value and receives the widget and that value. Pass remove=True to unregister the same callable. The callbacks and action_callbacks constructor arguments register the same two lists.

status = ax.Text("Ready")
button = ax.Button(
    "Submit",
    variant="primary",
    callbacks=[lambda _button: setattr(status, "text", "Submitted")],
)

menu = ax.DropdownMenu(
    [{"label": "Export", "value": "export"}],
    label="Actions",
)
menu.on_action(lambda _menu, action: setattr(status, "text", f"Chose {action}"))

Choosing a menu item is both an activation and a value, so it runs on_click and on_action. Toolbar, MenuBar, and CommandPalette also keep their id-keyed callbacks mapping, which stays mutable after construction.

Astryx tables

Table renders structured row data with optional checkbox selection and sorting. Pass mappings, sequences, or scalar rows; use row_key when mappings contain a stable row id such as an order id or symbol.

orders = ax.Table(
    rows=[
        {"order_id": "O-1", "symbol": "AAPL", "qty": 10, "status": "NEW"},
        {"order_id": "O-2", "symbol": "MSFT", "qty": 5, "status": "PARTIAL"},
    ],
    columns={"order_id": "Order", "symbol": "Symbol", "qty": "Qty", "status": "Status"},
    row_key="order_id",
    selects="multiple",
    sortable=True,
    sort_key="symbol",
)

orders.prepend_row({"order_id": "O-0", "symbol": "AMD", "qty": 2, "status": "NEW"})
orders.append_row({"order_id": "O-3", "symbol": "NVDA", "qty": 1, "status": "NEW"})
orders.update_row("O-2", {"qty": 7, "status": "FILLED"})
orders.remove_row("O-1")
orders.set_rows(next_order_snapshot)

Each row helper assigns a new rows list, so displayed notebooks receive the update through traitlets. prepend_row inserts before existing rows and append_row inserts after existing rows. Leave selects="" for no checkbox column, use selects="single" for one selected row, or use selects="multiple" for multi-row selection.

Theming

Use Brand and Theme for small reusable notebook theme overrides:

brand = ax.Brand(
    "desk",
    **{
        "color-accent": ("#0057b8", "#79b8ff"),
        "color-background-card": ("#ffffff", "#111827"),
        "radius-container": "8px",
    },
)

panel = ax.Theme(
    {"button": ax.Button("Run"), "status": ax.Badge("Ready")},
    brand=brand,
    mode="system",
    gap=2,
)

mode accepts "light", "dark", "system" to follow the OS prefers-color-scheme, and "jupyterlab" to follow the JupyterLab theme. Astryx syncs the active mode to the page's html[data-theme]; anylumino restores what the page started with once the last Astryx widget unmounts, so clearing one output does not leave the notebook with no mode.

Theme propagates the brand to nested Astryx children because every anywidget child is mounted in its own frontend root. See docs/astryx_brand_theme.md for runtime defineTheme() themes, precompiled BuiltTheme CSS themes, and the workflow for defining new Astryx themes. That document also describes the frontend style injection, deduplication, and JupyterLab testing workflow for theme changes.

Assets

Frontend assets are bundled into the Python package. Runtime notebooks do not need CDN access for Lumino, Astryx, or the bundled workflow icons.

Saving a notebook with widget state stores those assets once per widget model, which makes such files large. Rerun the examples with a kernel instead, or execute them with --ExecutePreprocessor.store_widget_state=False.

Development

uv sync --group dev
npm ci
npm run build
uv run pytest
uv run jupyter lab

Build and browse the documentation site:

make docs-preview

This generates the Python API reference from source and NumPy docstrings, then opens http://127.0.0.1:4200/docs with the Fumadocs development server. Use make docs for a static export into out/, make docs-serve to serve that export locally, and make docs-check to run API generation, link tests, linting, type checks, and the production build. The Griffe-backed fumadocs-python generator owns content/docs/api; update public signatures and docstrings in src/anylumino instead of editing generated API pages.

The Docs workflow publishes the same export to GitHub Pages at https://faysou.github.io/anylumino/docs/ on every push to main.

Open notebooks/astryx_components_smoke.py for the broad Astryx component smoke test. Open notebooks/astryx_layout_panels.py for the Astryx layout panels driven from Python. Open notebooks/astryx_table.py for focused table selection, sorting, and row-helper examples. Open notebooks/plotly_tabs.py and notebooks/plotly_dashboard.py for Lumino layout patterns with Plotly figures. Open notebooks/itables_tabs.py to see an itables.widget.ITable inside a tab with controls that append rows to a pandas DataFrame and refresh the displayed table.

License

anylumino is MIT licensed and requires Python 3.14 or later. The frontend bundles compile Lumino, Astryx, StyleX, and React into the wheel; THIRD_PARTY_LICENSES.md lists those packages with their notices, and npm run licenses regenerates it from the bundle inputs.

Metadata

Release files for anylumino 0.1.0

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for anylumino 0.1.0
File Size Uploaded Provenance
anylumino-0.1.0.tar.gz 970.6 kB Without provenance Details

Built distribution (wheel)

Table of built distributions (wheels) for anylumino 0.1.0
File Interpreter ABI Platform
anylumino-0.1.0-py3-none-any.whl Python 3 none any Details

Total release size: 1.7 MB

Release files / anylumino-0.1.0.tar.gz

Download URL anylumino-0.1.0.tar.gz
Size 970.6 kB
Tags Source
SHA-256 checksum
How to use checksums
034fc5fdd1a8444c5e20e3606bb202ab8810fe00026dcdbe536d304c45176a46
BLAKE2b-256 checksum
How to use checksums
cd7a4028ad13b5881063991dfc4c90c9c5204dbb3e018162a652ae701562815a
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via uv/0.12.17 {"installer":{"name":"uv","version":"0.12.17","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Ubuntu","version":"24.04","id":"noble","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":true}

Release files / anylumino-0.1.0-py3-none-any.whl

Download URL anylumino-0.1.0-py3-none-any.whl
Size 682.8 kB
Tags Python 3
SHA-256 checksum
How to use checksums
141da77b2a5fa626c1992226c4b91d0593d5c497b41f0f56ffae8f63071bc92c
BLAKE2b-256 checksum
How to use checksums
b9a2c039a4bb16dd866bb75759ea60dcaa86c0e1e3a3db3edaeea4c41bea177d
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via uv/0.12.17 {"installer":{"name":"uv","version":"0.12.17","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Ubuntu","version":"24.04","id":"noble","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":true}

Security for anylumino 0.1.0

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

No provenance found

None of the files in this release were uploaded with provenance metadata.

A note on package security

PyPI is a repository for distributing packages, not a service for auditing them. While third-party security researchers and our staff do their best to detect and remove malicious packages, PyPI cannot guarantee the quality, security or suitability of any given package.

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page