github posit-dev/py-shiny v1.9.0
shiny 1.9.0

3 hours ago

Breaking changes

  • OpenTelemetry reactive_update spans now follow each session's cycle: one span per session, from when the session turns busy until all of its effects (including async ones) have finished, carrying the session's session.id. The session's calc, effect, and output spans are its children. Previously, there was one span per reactive flush, which can serve several sessions, so with concurrent effects it couldn't carry a single session.id and ended before async effects finished. Effects without a session no longer have a reactive_update parent (#2522).

  • Reactive effects now run concurrently. Shiny starts each invalidated effect without waiting for an async effect's awaited part, so a slow async effect, calc, or render function no longer delays other sessions. Within a session, input changes and reactive.invalidate_later() still wait until all of that session's effects have finished, and outputs are sent together once they have. What app authors may notice:

    • async effects interleave at each await instead of running one after another. priority orders when effects start, not when they finish. Runs of the same effect still don't overlap: a re-run waits for the previous run.

    • reactive.lock() is now defunct and no longer locks the reactive graph. Please remove it from your code.

    • await reactive.flush() returns right away, without waiting for dependents, when called from within an effect, a reactive.on_flushed() or session.on_flush()/session.on_flushed() callback, or a task one of these started while it is still running, since waiting there could deadlock. A task that outlives the effect that started it, such as an extended task's body, still waits. The wait covers the whole process, including other sessions' effects and output.

    • reactive.on_flushed() callbacks run after each round of effects has started, without waiting for async effects, and can run several times per reactive.flush(). Previously, they ran once all effects had finished. An error in one of these callbacks is now printed, and the other callbacks still run, instead of closing a session.

    • Message handlers (session.set_message_handler()) run in their own task, so a slow handler no longer holds up other messages from the client.

    • Each session sends its outputs in a task of its own, once all of its effects have finished. A slow client, or a slow session.on_flush() callback, delays only its own session's output, not other sessions' output or other reactive work.

    • Setting a reactive.value now re-runs its dependents automatically, without a call to reactive.flush(). The reactive graph itself still isn't thread-safe, so from another thread use loop.call_soon_threadsafe(value.set, new_value).

    • File uploads and session.dynamic_route() requests no longer mark the session busy.

    • Destroying an effect (effect.destroy()) cancels its in-progress run, raising asyncio.CancelledError at its current await. So ending a session, or session.destroy(id), cancels its in-progress async effects and render functions instead of letting them run on after the session or module is gone.

    (#2508, #2515)

  • near_points(add_dist=True), imported from shiny.plotutils, now names its distance column dist_ instead of dist. Code reading df["dist"] from the result must now read df["dist_"] (#2510).

Lifecycle changes

  • reactive.lock() is now defunct: it no longer locks the reactive graph and emits a ShinyDeprecationWarning. Please remove it from your code (#2520).

  • shiny add test is deprecated and now prints a deprecation warning. It will be removed in a future release. Please use the shiny-for-python Agent Skill with a coding agent to generate tests instead (#2566).

New features

  • New session.run_once_when_idle() runs a function once, after the session's effects have finished and its outputs have been sent. Use it to change reactive state from a background task or a custom timer without changing a value that a running effect is still reading (#2546).

  • ui.sidebar() gains a role argument ("form", "search", "complementary", or "region") for opt-in ARIA landmark markup (rstudio/bslib#1359). "complementary" renders an <aside>, other roles render a <div> with the corresponding role attribute, and landmark roles require an accessible name (from title, aria_label, or aria_labelledby). Note that the default (role=None) now renders a neutral <div> instead of an <aside>. In addition, ui.page_sidebar() now places the whole sidebar layout inside the page's <main> landmark (#2526).

Bug fixes and minor improvements

  • On Windows, an output's busy indicator now shows while the output renders. Previously, when a synchronous render function blocked the event loop, the message that turns on the indicator reached the browser together with the output's new value, so most outputs never showed it (#1381).

  • UI updates and custom messages sent from outside the reactive graph (e.g. from a background asyncio task that calls ui.update_text()) are now sent to the client right away instead of waiting for the next input change (#21, #1889).

  • Jsonifiable now uses Sequence and Mapping for its container arms instead of the invariant List and Dict, so values like dict[str, int] and list[str] type-check as Jsonifiable, including as the return type of a function passed to a Renderer[Jsonifiable]. As a side effect, type checkers also accept other sequences and mappings, such as range, deque, and bytes, even though only list, tuple, and dict serialize. Runtime behavior is unchanged (@wolfgang-aura, #2511).

  • near_points(add_dist=True), imported from shiny.plotutils, now adds the dist_ column its documentation describes, instead of a column named dist. The trailing underscore keeps it from colliding with a dist column in your own data. Code reading df["dist"] from the result must now read df["dist_"] (#2510). near_points() and brushed_points() also now map a non-string categorical column (e.g. one of integers) to its category codes, instead of raising ValueError: to_float: unsupported dtype for x (#2505).

  • @output_args() in Express now accepts every argument of the output's UI function, with matching types. @render.ui now takes inline, container, fill, fillable, and HTML attributes, instead of raising a TypeError. @render.text now takes container. @render.plot and @render.image now declare inline, click, dblclick, hover, brush, and fill explicitly (and width and height for @render.image), instead of accepting any keyword. @render.download_button and @render.download_link now take label (#2539).

  • Progress.set(0) now places the bar at 0 relative to min and max, instead of sending 0 unnormalized. Previously, with a negative min, such as Progress(min=-10, max=10), a value of 0 showed an empty bar rather than a half-full one (#2518).

  • @render.data_frame now serializes Polars Categorical and Enum columns with Polars >= 2.0, instead of raising AttributeRemovedError: get_categories was removed in version 2.0 (#2529).

  • @render.data_frame now labels key-value Map columns (e.g. a pyarrow map<string, int64> column) as object, like struct, list, and array columns. This requires a narwhals version that has the Map dtype (#2569).

  • @render.data_frame now sends values of pandas nullable dtypes (Int64, Float64, boolean) to the browser as numbers and booleans, instead of strings such as "1" and "True". It also serializes data frames faster and with less memory: a 100,000-row frame with mixed column types serializes 2-3.5x faster, with a 15-40% lower memory peak. The largest gains are for pandas datetime and timedelta columns, duration columns, and Decimal or missing (pd.NA) values. Editing a cell no longer copies the whole data frame (@FBruzzesi, #2572).

  • @render.data_frame's selection_mode now accepts a list (e.g. ["rows"]), as its type hint says, instead of raising TypeError: unhashable type: 'list', and a set_patches_fn() that returns badly shaped patches now raises the intended ValueError describing the expected shape (#2560).

  • @render.download_button and @render.download_link gain an icon argument, and pass any other keyword arguments (e.g. class_="btn-success") to the control they auto-place in Express, so an Express app no longer needs ui.hold() and a hand-placed ui.download_button() just to add an icon. The same arguments can also be given with @output_args() (@raashish1601, #2533). Setting a reactive value from a @render.download_button download handler now updates the outputs and effects that read it right away, including while a streamed download is still sending, instead of only after the next message from the client (#1785).

  • req(False, cancel_output=True) inside a @reactive.effect now stops the effect silently, instead of being treated as an unhandled error that closes the session. SilentCancelOutputException now subclasses SilentException (#2560).

  • session.bookmark.on_bookmark() callbacks in a module now have their saved files found on restore. Restore now looks in <dir>/<ns>, where the files are saved, instead of <dir>/<ns>- (#2560).

  • session.on_flush() and session.on_flushed() callbacks registered while the session's flush callbacks run (by one of them, or by a task that ran while an async one was waiting) now run with one more output flush right after, instead of waiting for the next input change. The same goes for a message one of them queues, such as a ui.update_*() in an on_flushed callback. That output flush starts 10 ms later, so a callback that causes one every time it runs (e.g. a once=False callback that calls ui.update_*()) repeats at most 100 times a second (#2580).

  • shiny create now offers the "Stream markdown with enterprise LLM" templates, which were previously never found because of a misspelled path (#2560).

  • In ui.input_date(), datesdisabled now works when format is not the default yyyy-mm-dd. The dates are now converted on the client the same way min/max are, instead of being parsed by bootstrap-datepicker with the display format. The data-date-dates-disabled attribute is replaced by data-dates-disabled (and omitted when datesdisabled is None), and controller.InputDate.expect_datesdisabled() checks the new attribute. This relies on the updated vendored shiny.js (rstudio/shiny#4434, #2523).

  • ui.navset_tab() and the other navsets now add aria-controls, pointing at the panel's id, to nav links that target a tab panel, alongside the existing href (rstudio/bslib#1355, #2526).

Don't miss a new py-shiny release

NewReleases is sending notifications on new releases.