posit-dev / py-shiny

Shiny for Python
https://shiny.posit.co/py/
MIT License
1.32k stars 80 forks source link

Progress message assertions #1586

Open jcheng5 opened 3 months ago

jcheng5 commented 3 months ago

I'm getting these errors in the Shiny error dialog:

Shiny server has sent a progress message for result,
            but the output is in an unexpected state of: running
Shiny server sent a message that the output 'result' is recalculating,
            but the output is in an unexpected state of: 'idle'.
Shiny server sent a message that the output 'result' has been recalculated,
            but the output is in an unexpected state of: 'idle'.

Using this app (shinylive); to repro, enter a number and hit the button:

from shiny import App, ui, render, reactive
import matplotlib.pyplot as plt
import numpy as np
import random

# Function to create a simple Ishihara-like plate
def create_ishihara_plate(number, color, background):
    fig, ax = plt.subplots(figsize=(4, 4))
    ax.set_aspect('equal')

    # Create background dots
    x = np.random.rand(1000)
    y = np.random.rand(1000)
    ax.scatter(x, y, c=background, s=50)

    # Create number dots
    t = np.linspace(0, 2*np.pi, 100)
    x = 0.3 * np.cos(t) + 0.5
    y = 0.3 * np.sin(t) + 0.5
    ax.text(0.5, 0.5, str(number), fontsize=100, ha='center', va='center', color=color)
    ax.scatter(x, y, c=color, s=50)

    ax.axis('off')
    return fig

# Define the test plates
plates = [
    {"number": 12, "color": "red", "background": "green", "type": "normal"},
    {"number": 8, "color": "green", "background": "red", "type": "normal"},
    {"number": 6, "color": "purple", "background": "orange", "type": "protanopia"},
    {"number": 5, "color": "blue", "background": "yellow", "type": "deuteranopia"},
]

app_ui = ui.page_fluid(
    ui.h1("Color Blindness Test"),
    ui.p("Identify the number you see in each image:"),
    ui.output_plot("ishihara_plate"),
    ui.input_numeric("user_answer", "Enter the number you see:", value=0),
    ui.input_action_button("next_plate", "Next Plate"),
    ui.output_text("result"),
    ui.output_text("final_result"),
)

def server(input, output, session):
    current_plate = reactive.Value(0)
    correct_answers = reactive.Value(0)

    @output
    @render.plot
    def ishihara_plate():
        if current_plate.get() < len(plates):
            plate = plates[current_plate.get()]
            return create_ishihara_plate(plate["number"], plate["color"], plate["background"])

    @output
    @render.text
    def result():
        if input.next_plate() > 0 and current_plate.get() < len(plates):
            plate = plates[current_plate.get()]
            if input.user_answer() == plate["number"]:
                correct_answers.set(correct_answers.get() + 1)
                return f"Correct! The number was {plate['number']}."
            else:
                return f"Incorrect. The number was {plate['number']}."

    @output
    @render.text
    def final_result():
        if current_plate.get() >= len(plates):
            score = correct_answers.get()
            if score == len(plates):
                return "Your color vision appears to be normal."
            elif score >= len(plates) - 1:
                return "You may have mild color vision deficiency."
            else:
                return "You may have significant color vision deficiency. Please consult an eye care professional for a thorough evaluation."

    @reactive.Effect
    @reactive.event(input.next_plate)
    def update_plate():
        if current_plate.get() < len(plates):
            current_plate.set(current_plate.get() + 1)

app = App(app_ui, server)
jcheng5 commented 3 months ago

I think this is due to the result output having this line:

                    correct_answers.set(correct_answers.get() + 1)

This should actually start an infinite loop, as there's a circular dependency on correct_answers.get(). If you tuck that line under a with reactive.isolate(), the error goes away (but, BTW, the app's logic is incorrect--this is just a test of Claude 3.5 Sonnet and it didn't quite get the result() correct).

jcheng5 commented 3 months ago

So interesting... I told it:

It's extremely bad programming practice in Shiny to have side effects like correct_answers.set() in an output. Can you make sure side effects only appear in @reactive.Effect?

and its next version appeared to work perfectly. Although it is depending on the outputs running before the update_plate effect, which I would really prefer it make explicit by setting a priority.

shinylive

```python from shiny import App, ui, render, reactive import matplotlib.pyplot as plt import numpy as np import random # Function to create a simple Ishihara-like plate def create_ishihara_plate(number, color, background): fig, ax = plt.subplots(figsize=(4, 4)) ax.set_aspect('equal') # Create background dots x = np.random.rand(1000) y = np.random.rand(1000) ax.scatter(x, y, c=background, s=50) # Create number dots t = np.linspace(0, 2*np.pi, 100) x = 0.3 * np.cos(t) + 0.5 y = 0.3 * np.sin(t) + 0.5 ax.text(0.5, 0.5, str(number), fontsize=100, ha='center', va='center', color=color) ax.scatter(x, y, c=color, s=50) ax.axis('off') return fig # Define the test plates plates = [ {"number": 12, "color": "red", "background": "green", "type": "normal"}, {"number": 8, "color": "green", "background": "red", "type": "normal"}, {"number": 6, "color": "purple", "background": "orange", "type": "protanopia"}, {"number": 5, "color": "blue", "background": "yellow", "type": "deuteranopia"}, ] app_ui = ui.page_fluid( ui.h1("Color Blindness Test"), ui.p("Identify the number you see in each image:"), ui.output_plot("ishihara_plate"), ui.input_numeric("user_answer", "Enter the number you see:", value=0), ui.input_action_button("next_plate", "Next Plate"), ui.output_text("result"), ui.output_text("final_result"), ) def server(input, output, session): current_plate = reactive.Value(0) correct_answers = reactive.Value(0) last_answer = reactive.Value(None) @output @render.plot def ishihara_plate(): if current_plate.get() < len(plates): plate = plates[current_plate.get()] return create_ishihara_plate(plate["number"], plate["color"], plate["background"]) @output @render.text def result(): if current_plate.get() > 0 and current_plate.get() <= len(plates): plate = plates[current_plate.get() - 1] if last_answer.get() == plate["number"]: return f"Correct! The number was {plate['number']}." else: return f"Incorrect. The number was {plate['number']}." @output @render.text def final_result(): if current_plate.get() >= len(plates): score = correct_answers.get() if score == len(plates): return "Your color vision appears to be normal." elif score >= len(plates) - 1: return "You may have mild color vision deficiency." else: return "You may have significant color vision deficiency. Please consult an eye care professional for a thorough evaluation." @reactive.Effect @reactive.event(input.next_plate) def update_plate(): if current_plate.get() < len(plates): plate = plates[current_plate.get()] if input.user_answer() == plate["number"]: correct_answers.set(correct_answers.get() + 1) last_answer.set(input.user_answer()) current_plate.set(current_plate.get() + 1) app = App(app_ui, server) ```