Deprecations
-
ui.output_text_verbatim()is deprecated and now emits aShinyDeprecationWarning. Please useui.output_code()/@render.codeto create an output container for code (monospaced text), orui.output_text()/@render.textto create an output container for plain text. (#2097) -
playwright.controller.OutputTextVerbatimis deprecated alongsideui.output_text_verbatim()and now emits aShinyDeprecationWarningwhen constructed. Please useplaywright.controller.OutputCodeinstead. (#2097)
New features
-
Added
local_server, a pytest fixture that is the in-memory counterpart oflocal_app: a function-scopedtest_server()session for theapp.pynext to the test file. (#2495) -
Added
session.allow_reconnect(), the Python counterpart to Shiny for R'ssession$allowReconnect(). Call it withTrueto let the browser reconnect to its session (showing a countdown dialog instead of the "Disconnected from server" overlay) when the hosting environment keeps sessions alive after a client disconnects, or with"force"to attempt the reconnect anywhere. (#2441) -
Added
shiny.testserver.test_server(), an in-memory server testing API for testing reactive server functions and Shiny apps headlessly, without a browser. (#2470) -
Added
ui.page_html(), for apps whose UI is a complete HTML document they own (e.g. theindex.htmla JS bundler emits) rather than one built fromui.page_*()components. It takes the document as a string or aPath, plus optionalextra_deps=. Pass the result asApp(ui=), or return it from a UI function (App(ui=lambda request: ...), which is what bookmarking requires): the document is served as-is, with Shiny's own HTML dependencies -- plus any inextra_deps=-- inserted at<meta name="shiny-dependency-placeholder" content="">(or a customdeps_replace_pattern=), and their files served by the app. In Express, useui.page_opts(html=), which routes the whole app throughui.page_html(): top-level UI markup is dropped (the document already is the page) but its HTML dependencies are kept. This is the Python counterpart to Shiny for R'sshinyApp(ui = htmlTemplate("index.html", document_ = TRUE))withattachDependencies(). (#2462)
Improvements
-
Stub files for
folium,uvicorn, andseabornare now generated with Pyrefly instead of Pyright. (Thanks, @ChidiebereNjoku!) (#2478) -
playwright.controller.Offcanvasnow supportsopen(),loc_trigger,loc_title,loc_footer, and the expectation methodsexpect_title(),expect_footer(), andexpect_placement(). (#2451) -
shiny skillsCLI help and the README now explain thatlibrary-skillsmust be run from your own project directory, since it installs the bundled Agent Skills of the packages that project has installed. The previous wording left that precondition implicit, so running the command from an empty directory or from a clone of py-shiny silently installed nothing. (#2447) -
shiny.testserver.test_server()is now documented in the bundledshiny-for-pythonAgent Skill, in a newtest-servertopic, so coding agents reach for in-memory server tests instead of hand-built sessions or a browser when only server logic needs checking. (#2491) -
ui.navset_tab()and the other navsets created with anidnow use thatidas theirdata-tabsetid, so their tab panes get stabletab-tabs-0style DOM ids instead of ones built from a random integer. This makes the rendered markup reproducible across renders and easier to target from custom CSS and JavaScript. Navsets without anid, andui.nav_menu()dropdowns, keep the random ID. (Thanks, @pevolution-ahmed!) (#2410)
Bug fixes
-
Closing a session no longer destroys the reactive values and calcs created in it, so async work that outlives the connection does not error. Since v1.6.1, refreshing the page while an
@reactive.extended_task(or anyasynciotask) was in flight could raiseDestroyedReactiveError: Reactive value '<name>' has been destroyed.once it settled, leaving the task in neither"success"nor"error". Values and calcs are now left readable at their last value on close and reclaimed by garbage collection, while an explicitsession.destroy(id)on a live session still tears them down. Effects are still destroyed on close. (#2428) -
@expressifyand@render.expressno longer fail withRuntimeError: Failed to find function '...' in ASTwhen another decorator has changed the function's__name__. The AST lookup matched on__name__, which a decorator can rewrite; it now matches on the function's code object name, which always reflects the name at thedefsite. This pattern is commonly used to give each@render.expressfunction in a loop a unique output id. When@expressifycannot locate a function's definition, the error now names the function as it appears in the source (rather than a__name__a decorator may have rewritten), points at the file and line it looked at, and lists the likely causes — anasync def, a decorator belowexpressify()that returns a wrapper instead of the original function, or a source file modified after import. (#2016) -
playwright.controller.InputSelectizeno longer clicks the page body to close the selectize dropdown.expect_choices(),expect_choice_labels(), andexpect_choice_groups()open the dropdown, because selectize renders its choices into the DOM only after the first open. The click that closed the dropdown again landed on app content and fired the app's own click handlers, so a test could record an interaction that it never made. The controller now callsclose()on the selectize instance instead, which touches no app content. (#2426) -
@render.data_framenow renders data frames whose column names are empty ("") or are not strings: column ids are positional and are never derived from the column name, and a column named0no longer renders a blank header. Its.update_sort()also now honors its documented default for bare column indices, sodescfollows the column dtype: number-like columns sort descending and everything else sorts ascending. (#2421) -
@render.download_button,@render.download_link, and the deprecated@render.downloadnow honor@output(id=). The download handler was registered under the decorated function's name, but the URL rendered by the control used the@output(id=)value, so clicking the control returned a 404. (#2415) -
@render.uiand other dynamically-rendered outputs inside aui.popover()orui.tooltip()without atitle=no longer get stuck showing "recalculating". The container collapsed to 0 width, so the output'sResizeObservernever fired; vendored bslib CSS now gives it a non-zero minimum width. (#2446) -
shiny run --app-dir <dir> <app>now honors--app-dirfor Shiny Express apps. Express detection looked for the app file relative to--app-dir, but the entrypoint that gets handed to uvicorn was then built by resolving the app path against the current working directory instead, so running an Express app from outside its directory failed with aFileNotFoundErrorfor a path that never existed. (#2419) -
ui.input_bookmark_button()is now included in the Express API reference. (#2418) -
ui.input_selectize()'soptionsdocstring now correctly points atui.js_eval()for marking a string as a JavaScript function. (#2416) -
ui.input_slider()andui.update_slider()now encodedatetime.datevalues as UTC midnight, matching Shiny for R, so they no longer shift by a day when the server runs in a timezone ahead of UTC. Dates were encoded as local midnight, but the client formats and reads slider dates back in UTC, so on e.g.Europe/Amsterdaman update to2025-01-01landed on2024-12-31. Naive (timezone-less)datetime.datetimevalues are likewise anchored to UTC and round-trip unchanged, instead of shifting by the server's UTC offset: such a value was encoded as if it named a local time, but the client sends it back to be decoded as UTC, so a slider set to12:00reported11:00on a server inEurope/Amsterdam. Timezone-aware datetimes name an absolute instant and are unaffected. (#2398) -
ui.input_task_button(type=None)no longer drops thebslib-task-buttonclass. Operator precedence made thetype is not Nonecheck apply to the whole class string rather than just the Bootstrap classes, so the button rendered withclass=""; since that class is the selector Shiny's input binding uses, the button was never bound as an input and clicking it did nothing. (#2388) -
ui.show_offcanvas()now accepts theid(a string) of aui.offcanvas()panel already in the UI and reveals it, matching its sibling functionsui.hide_offcanvas()/ui.toggle_offcanvas(). Previously, passing a string raised an unhandledAttributeErrorfrom deep inside the implementation.ui.show_offcanvas()also now accepts bare tag content (wrapping it into a new anonymous panel), in addition to aui.offcanvas()object; a string that looks like body text instead of an id (empty, or containing whitespace) raises an actionableValueError. (#2445) -
ui.Theme's API reference examples now run in Shinylive. Compiling a customized theme requireslibsass, but Shinylive only auto-loads packages it finds in an app's top-level imports andTheme.to_css()importssasslazily, so the examples died with anImportError; the example directory now declareslibsassin arequirements.txt. The error raised when a package required for theme compilation is missing is also fixed: it interpolated the package name into the first sentence but printed a literalpip install {pkg}in the second. (#2387)