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"))

Every anylumino widget 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.

What counts as an activation depends on the widget kind:

  • Event widgets such as buttons, cards, menus, toolbars, and command palettes activate on a click, because a click is not a persistent value.
  • Value widgets such as checkboxes, switches, inputs, sliders, and the native date and time controls activate on every value change. on_action receives the new value. They also fire when Python assigns value, so use observe when a handler needs the old value or must ignore programmatic updates.
  • Tables and tab panels keep selection, sort, and tab state in synced traits. Observe those traits, or use on_selection and on_sort on tables.
status = ax.Text("Ready")
button = ax.Button(
    "Submit",
    variant="primary",
    callbacks=[lambda _button: setattr(status, "text", "Submitted")],
)

live = ax.Checkbox(False, label="Live")
live.on_action(lambda _checkbox, checked: setattr(status, "text", f"Live: {checked}"))

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.1

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.1
File Size Uploaded Provenance
anylumino-0.1.1.tar.gz 1.6 MB Without provenance Details

Built distribution (wheel)

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

Total release size: 2.3 MB

Release files / anylumino-0.1.1.tar.gz

Download URL anylumino-0.1.1.tar.gz
Size 1.6 MB
Tags Source
SHA-256 checksum
How to use checksums
5b4e24d21cb4bb94d74f58944caa79313665ddd57bb6285caedf78d43cc7aae0
BLAKE2b-256 checksum
How to use checksums
d26579129ff3b0875750b769e98223839ff1d367e65d253f0c4d4ee63c348be1
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.1-py3-none-any.whl

Download URL anylumino-0.1.1-py3-none-any.whl
Size 683.3 kB
Tags Python 3
SHA-256 checksum
How to use checksums
c7b0839af819655749c0ef0abc0a6098515cafaf4a48341e89642c2e564e72ae
BLAKE2b-256 checksum
How to use checksums
68380e70e6608e471a9769897b75b63ef4c0bb1d9e3d3c5b227be8ca9b8336dc
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.1

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