New features
-
Added test mode, enabled via the
SHINY_TESTMODE=1environment variable or theApp(test_mode=)argument (which defaults to that env var). The pytest app-launch fixtures (local_app,create_app_fixture) now run apps in test mode. (#2269) -
When test mode is enabled, each session serves a JSON snapshot of its
input,output, andexportvalues at/session/{id}/dataobj/shinytest(URL available viasession.get_test_snapshot_url()). App authors can surface internal reactive values withshiny.testmode.export_test_values(). The endpoint honors query paramsinput/output/exportto select specific blocks (=1for a whole block, or a comma-separated list of keys); unlike R, requesting no blocks returns all three rather than a400. (#2269) -
input.set_snapshot_preprocess(id, fn)(orshiny.testmode.snapshot_preprocess_input()) andmy_output.snapshot_preprocess(fn)preprocess test-mode snapshot values before they are written, e.g. to scrub timestamps or temp paths. Handlers may be synchronous or asynchronous. File inputs automatically scrub each file'sdatapathto its basename, matching Shiny for R.export_test_values()moved fromshiny.sessionto the newshiny.testmodemodule. (#2282) -
shiny.playwright.controller.AppTestValuesreads a session's test-mode snapshot (input/output/export) in end-to-end tests. Its expect methods accept predicates: pass any callable (e.g.app_values.expect_input("n", is_integer)) in place of an expected value, and the expectation retries until the predicate returns a truthy value. (#2269, #2357) -
offcanvas()creates sliding Bootstrap Offcanvas panels that appear from a viewport edge. Panels can be triggered by a UI element, revealed programmatically withshow_offcanvas(), or controlled by id withhide_offcanvas()andtoggle_offcanvas(). (#2279) -
@render.download_buttonand@render.download_linkpair 1:1 withui.download_button()andui.download_link(). (#2364) -
The shiny package now ships bundled Agent Skills under
shiny/.agents/skills/(the library-skills convention): a singleshiny-for-pythonskill whoseSKILL.mdis a grouped index that routes coding agents to a per-topic reference file for each of shiny's public APIs. (#2339, #2344, #2345, #2356, #2365, #2366)- Foundations:
reactivity(the reactive graph —reactive.value/calc/effect/event,req,isolate,poll),express(Express mode),modules-core/modules-express(reusable namespaced components), andsession-lifecycle(on_ended/on_flush, request info, dynamic routes). - Layout and navigation:
layouts(pages, cards, sidebars, columns, value boxes),navigation(navsets andpage_navbar),dynamic-ui(@render.ui,update_*,insert_ui,panel_conditional), andtheming(ui.Theme, presets, dark mode). - Outputs and rendering:
plots(render.plot/render.imageand interactions),data-frames(render.data_frame), andfiles(upload/download). - Feedback and interactivity:
feedback(notifications, modals, progress, busy indicators),extended-tasks(non-blocking long-running work), andbookmarking(save/restore app state withbookmark_store=andsession.bookmarkhooks). - Generative AI:
chat(ui.Chat) andmarkdown-streaming(ui.MarkdownStream). - Extending shiny:
custom-renderers(authoring aRenderersubclass) andcustom-components(custom JavaScript input/output bindings). - Testing and observability:
testing(end-to-end tests with pytest and Playwright),debugging(inspect running apps via test mode), andotel(OpenTelemetry observability).
- Foundations:
-
shiny skills listshows each bundled skill's name and description, andshiny skills path <name>prints the skill's directory, so itsSKILL.mdand supporting files (references/,scripts/) can be read from the installed package. To install the skills into a coding agent, the README and theshiny skillsCLI help now point atlibrary-skills(uvx library-skills --claude). (#2340, #2367)
Improvements
-
The
shiny[otel]optional dependency group now includesopentelemetry-distro[otlp], so OpenTelemetry zero-code auto-instrumentation works out of the box:opentelemetry-instrument shiny run app.py. This is now the documented standard way to enable OpenTelemetry — the docs and theexamples/open-telemetry/example no longer configure providers inside the app (in-codeset_tracer_provider()setup is silently ignored when a provider is already installed, e.g. underopentelemetry-instrument). The OTLP exporters are also included, making it easy to switch between gRPC and HTTP export via standardOTEL_*environment variables. Note thatopentelemetry-distropinsopentelemetry-sdkto a matching minor version, so if you combineshiny[otel]with other packages that pin the OpenTelemetry SDK (e.g.logfire), the resolver may need matching versions. (#2349) -
The Playwright controllers
DownloadButtonandDownloadLinknow share a common base class and gainexpect_label(), andDownloadLinkgains the width expectations previously available only onDownloadButton. (#2093) -
OutputDataFrame.set_filter()now supports multi-column filters. (#2093)
Deprecations
@render.downloadis deprecated; use@render.download_button(or@render.download_link) instead. (#2364)
Bug fixes
-
Fixed rare tabset ID collisions in pages with many navsets. Randomly generated
data-tabsetidvalues were drawn from a pool of ~1 million, so a page with dozens of tabsets could occasionally produce two navsets with the same ID (a birthday collision), resulting in duplicate DOM ids and broken tab switching. IDs are now drawn from a pool of 9 trillion. (#2296) -
shiny run --launch-browsernow opens the browser even when Uvicorn's INFO-level startup logs are unavailable, e.g. when logs are disabled or--log-level=warningis used. (#569) -
InputSlider.set()andInputSliderRange.set()now compute the target value's position from the slider's step configuration and drag the handle directly to it, verifying the landing position against the widget's state (finishing with arrow-key presses when the slider has more steps than the track has pixels). Previously the handle was swept one pixel at a time while polling the label, which was slow and could intermittently miss the target when the browser coalesced or dropped mouse-move events (a recurring webkit CI flake).InputSliderRange.set()orders the two handle moves so neither is clamped by its sibling, instead of parking both handles at their extremes first. Setting a value the slider cannot produce now raises an error listing the slider's actual values. (#2311, #2326) -
ui.output_data_frame()now consistently orders the filtered columns in ascending column order (#2093), and resetting a numeric range filter resets both values (#2093). -
value_box()'siddocstring now documentsinput.<id>_full_screen()for observing the value box's full screen state, matchingcard(). It previously documented the wrong reactive-value syntax,input.<id>()["full_screen"]. (#2324)
Other changes
-
CI now runs the unit tests on Ubuntu with the oldest supported Python (3.10) and every runtime dependency resolved to its declared minimum version (via
uv pip compile --resolution lowest-direct), so stale lower bounds inpyproject.tomlare caught. As part of this,starletteandprompt-toolkitgained explicit lower bounds (>=0.17.1and>=3.0.0), and theopentelemetry-api/opentelemetry-sdkminimums were raised from 1.20.0 to 1.24.0 — older versions could leak unsanitized error messages in span exception stack traces. Shiny's OpenTelemetry log emission (which relied on theLogger.emit()keyword form added in opentelemetry 1.38.0 and was silently dropped on older versions) now falls back to constructing theLogRecordmanually, so it works across the whole supported range. (#2335) -
Optimized the test suite by avoiding network-based dataset downloads and heavy library imports during Playwright test app startup, reducing setup time significantly. (#2314)
-
Packaging metadata now uses a PEP 639 SPDX license expression instead of the deprecated
licensetable and license classifier, and staleMANIFEST.inrules were removed. Building the package (e.g. withuv build) no longer emits setuptools deprecation or manifest warnings. Wheel and sdist contents are unchanged. (#2304) -
Raised the minimum supported uvicorn version from 0.16.0 to 0.23.0 (July 2023). The old floor no longer worked in practice: running on Posit Workbench passes uvicorn the
ws_per_message_deflateoption, which requires uvicorn >= 0.17. (#2317) -
@add_example()gained anexample_name=parameter that looks up the example in the nearestapi-examples/directory, and allex_dir=call sites that pointed inside anapi-examples/tree were migrated to it;ex_dir=remains only for examples outside the nearestapi-examples/tree. This also fixed nine call sites that passed the example name positionally, where it was silently treated asapp_file=and the example was missing from the generated docs (#2328). Docs builds now fail when an@add_example()API reference is missing its example app, instead of emitting a warning and silently omitting the example; several APIs whose examples had gone missing this way were fixed, and missing Shiny Express examples were added. (Thanks, @EltonChang1!) (#2278) -
Added api-examples for
shiny.testmode.export_test_values(),shiny.testmode.snapshot_preprocess_input(), andRenderer.snapshot_preprocess(), demonstrating how to surface reactive values in the test-mode snapshot and how to scrub sensitive or nondeterministic input/output values from it (#2284), and forui.output_code()(#2093). -
shiny.run.run_shiny_app()(and thereforeshiny.pytest.create_app_fixture()) now picks random app ports from a disjoint per-worker port range when running under pytest-xdist, instead of the full 1024-49151 range shared by all workers. This prevents parallel test workers from racing to bind the same port and from reusing each other's recycled ports. When not running under pytest-xdist, the behavior is unchanged: ports are picked fromrandom_port()'s full default range (1024-49151). (#2297)
Full Changelog: v1.6.4...v1.7.0