dt2 (Python)
June 26, 2026 · View on GitHub
Goal: feature parity with the R DT2 package, delivered via anywidget for Shiny for Python. The DataTables v2 runtime (2.3.4) and extensions are reused; the work is reimplementing the R binding layer in Python and re-wiring the Shiny transport onto the anywidget Comm.
Architecture decisions (locked)
- Repo:
StrategicProjects/dt2py(separate from the R repo). - PyPI / import name:
dt2. - Mechanism: anywidget (
shinywidgetsbridges it to Shiny for Python). - JS: bundled with esbuild from
js/index.js→src/dt2/static/index.{js,css}. - DataTables: pinned to
2.3.4/ jQuery3.7.1to match the R bundle.
Transport mapping (R → Python)
| R (htmlwidgets / Shiny) | Python (anywidget) |
|---|---|
Shiny.setInputValue(id+'_state', v) | model.set('state', v); model.save_changes() |
Shiny.setInputValue(id+'_row_check') | trait + model.save_changes() |
Shiny.addCustomMessageHandler(id+'_proxy') | model.on('msg:custom', fn) |
| SSP Ajax via custom message round-trip | model.send(req) + model.on('msg:custom') reply |
Phases
Phase 0 — scaffold ✅ (this commit)
- Repo, pyproject (hatchling), package.json (esbuild)
- anywidget adapter
js/index.js(core render + proxy/event skeleton) -
Dt2widget +dt2()constructor (pandas/polars/records) - Shiny example app
- Build bundle; widget constructs, traits populate, app serves (HTTP 200)
- Visual in-browser render + selection (needs Chrome extension online)
Phase 1 — config parity ✅
-
Optionsbuilder (chainable) covering the Rdt2_options/dt2_formatssurface:order,search_global,length_menu,language,use_buttons,cols_align/width/hide/escape,format_number/datetime/number_abbrev/time_relative,cols_render,cols_render_orthogonal,register/use_renderer. - Name→index resolution (
_name_to_idx, ported with warnings); seed names from a DataFrame/records viaOptions(df)— removes the R "forgot to set options$columns" footgun. -
JS()parity forhtmlwidgets::JS(): Python marks renderer source as{"__dt2_js__": code};index.jsreviveJs()recursively compiles markers into functions withDataTable/$/momentin scope. Proven in node. -
dt2(df, options=opts, **kw)merge. 22 unit tests + JS revive test green. - Quote-safe values via
json.dumps(_js_str, port of.dt2_js_str).
Phase 2 — extensions ✅
- All 15 extensions bundled via esbuild (
js/extensions.js): Buttons, Select, Responsive, FixedHeader, FixedColumns, KeyTable, Scroller, RowGroup, RowReorder, ColReorder, DateTime, SearchBuilder, SearchPanes, StateRestore, ColumnControl. Verified present in the built bundle. - jszip bundled (Excel/CSV/copy export via
window.JSZip). - Python activation helpers on
Options:select/responsive/fixed_header/ fixed_columns/key_table/col_reorder/row_reorder/row_group/scroller/ search_panes/search_builder/state_restore/column_control/buttons, plusextensions()registry (parity with Rdt2_extensions()). -
buttons(target=...)relocation (port of Rdt2_buttons) handled in JS. -
_momentLocaleapplied client-side. 15 extension tests green (46 total). - Deferred follow-ups: pdfmake (PDF export, ~1MB) as an optional extra;
moment-with-locales for
format_time_relative; modular/lazy loading so the base wheel ships only requested extensions (parity with R's per-ext loading).
Phase 3 — Shiny integration
- Server-side processing (de-risked first): DataTables
ajaxas a function routed over the Comm;dt2.server.process_sspports the R filter/order/paginate logic (no query-string parse needed — request arrives structured). Widget keeps full data Python-side;_on_msgreplies with a correlateddt2_ssp_response. Unit + handler tests green. Still to verify: the live Comm round-trip in a browser (needs frontend). - Proxy parity (
cmdprotocol mirroring R/dt2_proxy.R): widget methodsreplace_data,draw,reload,order,search,clear_search,page,select_rows. Order resolves header names → indices client-side. - Events parity: enriched
state({reason, order, search, page, selected}) +selected_rows; a monotonic_seqmakes event traits re-fire underreactive_read(anywidget equivalent of Shiny'spriority:"event"). - Inline inputs (port of R/dt2_inputs.R):
Options.col_checkbox/col_buttonrender delegated controls; clicks setrow_check/row_buttonevent traits. Verified in node (render compiles, seeds checked state) + 12 tests (58 total). Exampleapp_proxy_inputs.py. - Per-column search in SSP (R does global only — match, then optionally extend)
Phase 4 — polish & release
- Tests: 58 pytest cases (config, extensions, SSP, proxy/inputs) + node
smoke checks for the JS adapter.
[tool.pytest.ini_options]configured. - CI (
.github/workflows/ci.yml): builds the JS bundle, asserts the committedsrc/dt2/staticis fresh, installs and runs pytest on Python 3.9/3.11/3.13; syntax-checks the adapter sources. - Wheel verified: includes the built bundle; fresh
pip installworks with no Node toolchain (703kb bundle, 15 extensions, widget builds). - Release workflow (
release.yml): builds + publishes to PyPI via Trusted Publishing on a published GitHub Release (maintainer-triggered). - README (full API), CHANGELOG, examples gallery (5 runnable apps).
- Live in-browser verification (Shiny for Python): core render + selection, SSP (50k rows: render + search), config renderers (number/datetime/custom badge), extensions (RowGroup + Buttons), and inline inputs with row_check/row_button events — all confirmed. Two bugs found & fixed: moment.js now bundled (datetime), buttons placed in layout.
- PyPI publish — maintainer action: configure the PyPI trusted publisher,
then publish a GitHub Release to trigger
release.yml.
Open questions
- Bundle size: shipping all extensions vs. lazy/optional extras. Lean toward
optional
pip install dt2[buttons,searchpanes,...]extras mapping to JS chunks, to keep the base wheel small. - SSP transport latency over Comm vs. a Starlette route — benchmark in Phase 3.