Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

input task button component entry #127

Open
wants to merge 6 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
37 changes: 37 additions & 0 deletions components/inputs/input-task-button/app-core.py
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This app is a good technical demo -- see, it works! for people who already deeply understand reactivity.

I don't think it's a good example for actually demonstrating the value of the extended task. Most people will want to use extended tasks to kick off a long-running computation without preventing users from interacting with the rest of the app. In other words, there are fast tasks and slow tasks.

This example suffers from two problems:

  1. The fast task isn't driven by the user. In this example, the time updates, but I don't think the connection to "your app still runs" will be clear to learners.
  2. The slow task isn't (pretending) to be something the learner would actually do in their app.

I have three recommendations:

  1. Make sure the fast task involves the user directly interacting with the app.
  2. Make the slow task longer to give the user more time to see the overlapping work.
  3. Make the slow task at least appear to be something people would do, like build a model, read from a database, crunch some stats, hit an API, etc. (It doesn't have to actually do that, but it should pretend to.)

Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
import asyncio # <<
import datetime

from shiny import App, Inputs, Outputs, Session, reactive, render, ui

app_ui = ui.page_fluid(
ui.p("The time is ", ui.output_text("current_time", inline=True)),
ui.hr(),
ui.input_task_button("btn", "Square 5 slowly"), # <<
ui.output_text("sq"),
)


def server(input: Inputs, output: Outputs, session: Session):

@render.text
def current_time():
reactive.invalidate_later(1)
return datetime.datetime.now().strftime("%H:%M:%S %p")

@ui.bind_task_button(button_id="btn") # <<
@reactive.extended_task # <<
async def sq_value(x): # <<
await asyncio.sleep(2) # <<
return x**2 # <<

@reactive.effect # <<
@reactive.event(input.btn) # <<
def btn_click():
sq_value(5) # <<

@render.text
def sq():
return f"5 squared is: {str(sq_value.result())}"


app = App(app_ui, server)
37 changes: 37 additions & 0 deletions components/inputs/input-task-button/app-detail-preview.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
import asyncio
import datetime

from shiny import App, Inputs, Outputs, Session, reactive, render, ui

app_ui = ui.page_fluid(
ui.p("The time is ", ui.output_text("current_time", inline=True)),
ui.hr(),
ui.input_task_button("btn", "Square 5 slowly"),
ui.output_text("sq"),
)


def server(input: Inputs, output: Outputs, session: Session):

@render.text
def current_time():
reactive.invalidate_later(1)
return datetime.datetime.now().strftime("%H:%M:%S %p")

@ui.bind_task_button(button_id="btn")
@reactive.extended_task
async def sq_value(x):
await asyncio.sleep(2)
return x**2

@reactive.effect
@reactive.event(input.btn)
def btn_click():
sq_value(5)

@render.text
def sq():
return f"5 squared is: {str(sq_value.result())}"


app = App(app_ui, server)
35 changes: 35 additions & 0 deletions components/inputs/input-task-button/app-express.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
import asyncio # <<
import datetime

from shiny import reactive
from shiny.express import render, input, ui


@render.text
def current_time():
reactive.invalidate_later(1)
time_str = datetime.datetime.now().strftime("%H:%M:%S %p")
return f"The time is, {time_str}"


ui.hr()

ui.input_task_button("btn", "Square 5 slowly") # <<


@ui.bind_task_button(button_id="btn") # <<
@reactive.extended_task # <<
async def sq_values(x): # <<
await asyncio.sleep(2) # <<
return x**2 # <<


@reactive.effect # <<
@reactive.event(input.btn) # <<
def btn_click(): # <<
sq_values(5) # <<


@render.text
def sq():
return str(sq_values.result())
22 changes: 22 additions & 0 deletions components/inputs/input-task-button/app-preview.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import asyncio
from shiny import App, Inputs, Outputs, Session, reactive, ui

app_ui = ui.page_fluid(
ui.input_task_button("btn", "Add numbers slowly"),
)


def server(input: Inputs, output: Outputs, session: Session):

@ui.bind_task_button(button_id="btn")
@reactive.extended_task
async def long_calculation():
await asyncio.sleep(1)

@reactive.effect
@reactive.event(input.btn)
def btn_click():
long_calculation()


app = App(app_ui, server)
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
import asyncio
import datetime

import matplotlib.pyplot as plt
import numpy as np

from shiny import App, Inputs, Outputs, Session, reactive, render, ui

app_ui = ui.page_fluid(
ui.input_numeric("x", "x", value=5),
ui.input_task_button("btn", "Square number slowly"),
ui.output_text("sq"),
ui.hr(),
ui.p(
"While computing, the time updates and you can still interact with the histogram."
),
ui.p("The time is ", ui.output_text("current_time", inline=True)),
ui.input_slider("n", "Number of observations", min=0, max=1000, value=500),
ui.output_plot("plot"),
)


def server(input: Inputs, output: Outputs, session: Session):
@render.plot(alt="A histogram")
def plot():
np.random.seed(19680801)
x = 100 + 15 * np.random.randn(input.n())
fig, ax = plt.subplots()
ax.hist(x, bins=30, density=True)
return fig

@render.text
def current_time():
reactive.invalidate_later(1)
return datetime.datetime.now().strftime("%H:%M:%S %p")

@ui.bind_task_button(button_id="btn")
@reactive.extended_task
async def sq_values(x):
await asyncio.sleep(5)
return x**2

@reactive.effect
@reactive.event(input.btn)
def btn_click():
sq_values(input.x())

@render.text
def sq():
return str(sq_values.result())


app = App(app_ui, server)
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
import asyncio
import datetime

import matplotlib.pyplot as plt
import numpy as np

from shiny import reactive
from shiny.express import render, input, ui

ui.input_numeric("x", "x", value=5),
ui.input_task_button("btn", "Square number slowly")


@ui.bind_task_button(button_id="btn")
@reactive.extended_task
async def sq_values(x):
await asyncio.sleep(5)
return x**2


@reactive.effect
@reactive.event(input.btn)
def btn_click():
sq_values(input.x())


@render.text
def sq():
return str(sq_values.result())


ui.hr()

ui.p("While computing, the time updates and you can still interact with the histogram.")


@render.text
def current_time():
reactive.invalidate_later(1)
dt_str = datetime.datetime.now().strftime("%H:%M:%S %p")
return f"The time is {dt_str}"


ui.input_slider("n", "Number of observations", min=0, max=1000, value=500),


@render.plot(alt="A histogram")
def plot():
np.random.seed(19680801)
x = 100 + 15 * np.random.randn(input.n())
fig, ax = plt.subplots()
ax.hist(x, bins=30, density=True)
return fig
103 changes: 103 additions & 0 deletions components/inputs/input-task-button/index.qmd
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
---
title: Input Task Button
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
title: Input Task Button
title: Task Button

sidebar: components
appPreview:
file: components/inputs/input-task-button/app-preview.py
listing:
- id: example
template: ../../_partials/components-detail-example.ejs
template-params:
dir: components/inputs/input-task-button/
contents:
- title: Preview
file: app-detail-preview.py
height: 200
- title: Express
file: app-express.py
shinylive: https://shinylive.io/py/editor/#code=NobwRAdghgtgpmAXGKAHVA6VBPMAaMAYwHsIAXOcpMASxlWICcyACKAZ2wkJuJZYDELADzCAOhDoNmLACZQKZOnAkSAZo2IwW7ABY0I2FlKatGcKISUA3FRA1ad+wxjgAPVOfbtj9Uy3MIWThGPGMIVABXMjDImlUICQABQODGDAo3MglgtRZCSMZAsgB9JXgACgBKRAl+fnNLGzgMA2soABsaeQoSjoUQioBGKrr68rgS9jJGFgBeOQGJjB64ZYhiAHdqjGnGNQmKsTAAUgAJRBOAWUuAZRYT1GPRiHqAtcLXtWOAFV04FgTYzsMIgCZTGYAX2OCQkcQwukY1QS8IMUVKZA4AGsSgAjaJkUhHMC4siJfAsY63ACOkSg5hYAFYdB0th1sM9+EJRLCIEl4biDLIyti8QSifiyISICVunNjqTyVUuSJxHzGlYaLZXFlKMFhZj2FiVTzoJxuHI4Hl2NSSu0OpE4OwKm4aia1W8oJsoDRWBwuDxiLsOnA4KgKgAmZWCVVjBofRivNwAKmTEfdvJSFk12qtajgVgz6uzzVctnIFTR0Qwiuj3LVuRYipKhC6hCx1UQRbeNrtnUdzsZddjiVHWaCIQy7myE+t1M7cfeZE+OhmFV79oHGC8kQ6ZGqLzAkIAukA
- title: Core
file: app-core.py
shinylive: https://shinylive.io/py/editor/#code=NobwRAdghgtgpmAXGKAHVA6VBPMAaMAYwHsIAXOcpMASxlWICcyACKAZ2wkJuJZYDELADzCAOhDoNmLACZQKZOnAkSAZo2IwW7ABY0I2FlKasAgujwsAkhFQBXMuysB5Rw6dWAynHbteEFaMcFCESgBucEGUsnCMVvY0qtDoAPqJLAC8LIlYUADmcKlqADaJsgAUEvz8uahVYAAqunAsSvDG7Cxi+Dk0GMTujqkUAB5kDYT2jMHkI8o9VgYlBnCZjYz2cACU23jVNbm6jBV7B7X9Bh4jHADWqQBGjmSkDQ9kEIvdYF4AjvZQYIsACsOhKxAA7iVsD09vwhKJzn0BkMyCM4OMGuxfrD9hBtskJLE1Do4pETldHIgbHZHM4WIMyB5qW4mXSrOxfP5SNSfH4AttEMkaiwAAKzWKMDBjMhI4ksKYzSho9pwU5CiAikXBUIROAYAzhKAreQUVIlBRxCoARgJmq1-GCZGmmtNcFVGDdHogkNOGHYZEYalVDQApAAJRChgCyUa8LFDqFhwpqotyDwMshu7HuTzILwgFTzBdSNFkmR670+YG28JE4nt-HFITCNEiGAxFAgsSzZDudcRjbYnG4cjgJOxqSNZTVo0FA4bDv4UAhUBorA4XB4xH9JTgcHqACZa4J60jte6XSxRgAqG+Hhcpps61vt8dqOBhR9D5u6tv6uBInICpKTIDAqxPBFFxqeUq1SQgVkIW51XPGpJ2nLYKmBSCz0+H8JTiaVOzlccdF+FChwvZ1GE1NQelBbEAWCWROmpEAAxOdDjS2DBgnYewSgmXYAF8ekJFJUCyFgLHqNBUHSGgOTJOI7TAYSAF0gA
- id: relevant-functions
template: ../../_partials/components-detail-relevant-functions.ejs
contents:
- title: ui.input_task_button
href: https://shiny.posit.co/py/api/ui.input_task_button.html
signature: ui.input_task_button(id, label, *args, icon=None, label_busy='Processing...', icon_busy=MISSING, width=None, type='primary', auto_reset=True, **kwargs)
- title: reactive.extended_task
href: https://shiny.posit.co/py/api/reactive.extended_task.html
signature: reactive.extended_task(func=None)
- title: reactive.event
href: https://shiny.posit.co/py/api/reactive.event.html
signature: reactive.event(*args, ignore_none=True, ignore_init=False)
- title: reactive.effect
href: https://shiny.posit.co/py/api/reactive.effect.html
signature: reactive.effect(fn=None, *, suspended=False, priority=0, session=MISSING)
- id: variations
template: ../../_partials/components-variations.ejs
template-params:
dir: components/inputs/input-task-button/
contents:
- title: Input task button with inputs and interactivity
description: Here's an application that shows how the input task button can perform non-blocking computations.
It will take in 2 inputs to add together in a slow computation and you will will be able to move the slider
to make histogram modifications and see the time increment during the computation time.
apps:
- title: Preview
file: app-variation-input-task-button-multi-core.py
- title: Express
file: app-variation-input-task-button-multi-express.py
shinylive: https://shinylive.io/py/editor/#code=NobwRAdghgtgpmAXGKAHVA6VBPMAaMAYwHsIAXOcpMASxlWICcyACKAZ2wkJuIB0IdBsxYATKBTJ04AgUKasYE1ABtiZFTQBGWbKvVt2LVWTn0FLCAFd62Q5dSyIAM0bEYLdgAsaEO-JFGOChCKQA3GRc3D29fbAw4AA9UIPYjANYgiFE4RjwWX1QrMnyrGicyjELigH1reEYaQgAKPjBEtvy2jvwWMKgVKzgAXgBWAEo8AUrqshqyDgBrGq1islJWsC0yCE6WNoBlAEcrKCDLGy1czzUAdxVsNvGnAQABSq1fUXmllbWN1ZkdYQGo0UTDNrbXZgZ4QV5BELhOAJRIUbJwb4LdiLAQcLiEMRwZyeI41fqDODsZqJcaIAQsBlsW5QGisPHcXgYdgqOBwVDNCb0xlBMhWRgQFiJABUUoATC84QjQjQIglnM44KE3kqkQkIuRmrMMFDYTliVCaoRNIRFs1aUKGexSeShlSjYk7bCnPDKDlGBgKKiBGaSXa6RLhXBReLPGRGM0nWSBq6MKkrCoyJ6FZUvPGvdYaFhNgB1Hw8lgkejFXwAc3yZC8cBYUngLCsqHEFCMUGyLGwxCsFZ7sZoKhUBXIuURLFurK8zcbLB87HWNcYsAwTwVPvR-sDpnRxMIYqycxbcDDDpYOpVyN85LBEjgNRUT-jAEZYYyxHMV4wWMMYhPueGCdlG0gYBAxC3HaXJxs456bAApAAEogSEALLoQcLBIY4MJXiKYoSs4bQACqLueBRGCAoi-nGAC+bTZoWsw1NyYK5Js0JdGAABylzXMQxLEFo7C5P0UikOwewwL4wwAAz5EoiTDO+CkafkLojKMGmTNuWR+lgaiZgMZAQmAACCS40CuxBrrAW6HsYJmXhGDIQJg67ZO4XK8qIzTvgAnAAbAAHApEWfleiQASw6kKSwADU8WjCwUoOKmPaiL53miBAhqecUkFZu5LDODQdZsLFgEmFyVhaPoZBUl+jJQIkGDLpmiT5J8EDsMMADMSmEv1rLYMMZGMEMrUMkRMYVTWAhgAxAC6QA
- title: Core
file: app-variation-input-task-button-multi-core.py
shinylive: https://shinylive.io/py/app/#code=NobwRAdghgtgpmAXGKAHVA6VBPMAaMAYwHsIAXOcpMASxlWICcyACKAZ2wkJuIB0IdBsxYATKBTJ04AgUKasYE1ABtiZFTQBGWbKvVt2LVWTn0FLCAFd62Q5dSyIAM0bEYLdgAsaEO-JEAQXQ8FgBJCFQrMnZQgHloqJjQgGU4dnZeCFDGOChCKQA3OBzKUThGUKsaJzRUAH1qlgBeFmqsKABzOHrnFWrRAAoBFlG2mgxfJPrreEYaQmGwAA8+fBY11fXCqH64ZoBWAEo8EbH2qej6sg4Aa3qtaLJSJa0yCDXQtZSARysoXKWGxaCqeNQAdxU2DWJzOo3axESVwoyzIS3YPxhpwgY3GGC8jEGsJx5wmqGGJNxozWAHUfCo4CwSPRor5OqEyF5GVJ4G1UOIKEYoBBRCxsIimcLPFIVCoWL4KIx8qxwTROSxOYyfOxnp0lTAMGs4Sxibj2uS1gAVLka6TyoyfPGIsjTFFotaEKyMXLka7SR2+TQQfaWxhWOBHU2kyaRK7sTTlQlrD7rNYAOWBoOIzhYxC07AqOykpHYjpgvmaAAZQkpls0AIyVpuhHZ7Q5NqPwibO6b6d1gPtYgRHJwCco5guMYqEy5kRDhWPJXNIucsBIu6KxTzpTKkedpDJZI6IY0AAR9iawajRuzIzTWgRY2t1+phxvHxmvRJPlKpkQwSoiu4GAFnAQz1gAnAAbAAHJWcH1iOv64ssLQsI2lYsAA1OhBwsAAVA4AHCqIwGAaIECDLOGCUZGxq4s4NDsmwqGtCYIFWFofbsES9FjFAyz4jQOqDMsoRaL47DNAAzNWYiUJkZDYM0obhkhVJjLkZBejijGdE4uLnmUFQYG675wDmnrepQZB+vA358aMuTKjQxQxq2NACj0KgSBUgyIY5LBaTpYi+TycAYF54U0cQ4JEiBZCMM44VLAApAAEogqUALJZSkLCpY4YBIWe7QSSK1x3A8TwvI8ZDPBA9SefeYBvCm6ljEZLluXAqLGaIlXsLcxocFwhDyROPz1K24Y8csx6BVA4JQGqhhjbwIEMnA5LHIFwWMDiyz4fhABMBmdc5BSuRFFnOHABRnpdRQ3cU5BUYuGBtR1owfm19SEJohC3A5yFjBi027LN71JBgyxEiVv5GSKJlmb+H4YiDGlOXA2kHdKhLgzN6QAekVgqGidEfFTdRocE5J1I0NChJO05IWAAC+AC6QA

---

:::{#example}
:::

:::{#relevant-functions}
:::

## Details

The input task button is a special kind of button that allows long computations to not block
the behaviour of the rest of the application.
It looks very similar to the `ui.input_action_button()`, but uses the `ui.input_task_button()` function instead.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'd like to massage the framing here a little bit, in particular to set up the UI and server steps, which are well differentiated below.

I think it's worth mentioning in the intro that input_task_button() is a drop-in replacement for input_action_button(). If used on its own, it will update when clicking on it starts a long-running computation to let users know that something is happening on the server.

But input_task_button() is even more effective when paired with an extended task, which is something that happens on the server. Extended tasks run asynchronously and are truly non-blocking. The rest of your app can keep working while the extended task runs. An async function becomes an extended task with @reactive.extended_task decorator and it can drive the button state with @ui.bind_task_button("btn_id"). (You still have to react to the button click event, using something like @reactive.event(input.btn_id).)

(This is just a brainstorm/outline, feel free to modify/expand/etc.)


An input task button appears as a button and makes a computation asynchronously.

Follow these steps to add an action button to your app, perform a task, capture the result, and display the result:

1. Add `ui.input_task_button()` to the UI of your app to create an input task button.
Where you call this function will determine where the button will appear within the app's layout.

2. Specify the `id` and `label` parameters of `ui.input_task_button()` to define the button's identifier and label.

3. Use one of the `ui_output_*` functions to capture the computed result, here we are using `ui.output_text()`.

The input task button component can trigger a computation within the `server()` function.

There are 2 main parts of the input task button in the `server()` function,
a function that is called when the button is clicked (this is similar to the `ui.input_task_button()` component),
a separate function that defines any long computation you wish to perform asynchronously
(this is specific to making `ui.input_task_button()` making an asynchronous calculation).

To create the function that is called when the button is clicked:

1. Create a function that will be run when the `ui.input_task_button` is clicked.
2. Decorate this function with the `id` used in your ui.input_task_button(), i.e., `@reactive.event(input.<input_task_button_id>)`, this will make sure the function will only run when the button is clicked.
3. Add a second decorator, on top, `@reactive.effect`
4. In the body of the button click function, make a call to another function that you will use to make the long computation.

To create the separate function that makes the asynchronous computation:

1. Import the built-in Python `asyncio` module
2. Define a function that will be called in the button click step using the `async` and `await` syntax
See the [coroutines and tasks](https://docs.python.org/3/library/asyncio-task.html) official Python documentation
for details.
3. Return the calculated result in the function.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think it might be better to put this first. If I were teaching this interactively, I'd start by adding ui.input_task_button() and then adding the @reactive.event(input.btn_id) that reacts to the button click, and then finally showing the extended task.

But here, in static prose, I think that framing loses the priority of @reactive.extended_task. It'd be better to talk about setting up the extended task and then secondarily to discuss that you still need to react to the button click event.

Also make sure that you mention the @reactive.extended_task and @ui.bind_task_button() decorators -- they're definitely more important than the standard @reactive.event() and @reactive.effect decorators.


To learn more, you can read this article about
[non-blocking operations](https://shiny.posit.co/py/docs/nonblocking.html).
Loading