posit-dev / py-shiny

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

Accessing an empty reactive Value raises a SilentException #404

Open bartverweire opened 1 year ago

bartverweire commented 1 year ago

Hi,

I was trying to implement a lazy loading mechanism. The data is defined as a reactive.Value, but is initially not initialized. The data is loaded in a reactive Effect, that is fired when some other event is occurring (e.g. a button click, a nav item becoming active). The reactive Effect needs to check if the reactive Value is empty, and if so, it loads the required data and sets the reactive Value. Otherwise, it does nothing. To get the current value of the reactive Value, I'm using a with isolate block. However, this block is raising a SilentException, and the next steps (the data load) never happens. This is not limited to an isolate block. When used in an output function, the function stops when the empty reactive value is encountered.

Here's a simple example :

from shiny import *

app_ui = ui.page_fluid(
    ui.h2("Lazy Load Demo"),
    ui.input_action_button("in_load", "Load Data"),
    ui.output_text_verbatim("out_data", placeholder=True),
    ui.input_text("in_text", "Some text"),
    ui.output_text_verbatim("out_text", placeholder=True),
)

def server(input, output, session):

    data = reactive.Value()
    # workaround:
    # data = reactive.Value(False) or reactive.Value(pd.DataFrame())

    @reactive.Effect
    def load_data():
        """
        Load data on demand. But only load it once.
        If the data has been loaded before, do not reload
        """
        req(input.in_load())

        with reactive.isolate():
            # check if data is loaded. Avoid dependency with the reactive value
            # if using an initialized reactive.Value, the check may be different, e.g.
            # loaded = data()
            loaded = data() is not None

        # This is never executed:
        if loaded:
            print("data already loaded, skipping")
        else:
            print("loading data, skipping")
            data.set("Simulate")

    @output
    @render.text
    def out_data():
        """
        Also in the output function, calling the non-initialized reactive Value results in a SilentException.

        """
        # req(data())

        print("Rendering output")
        # This is never executed:
        print(data())
        print("data printed")

        return data()

    @output
    @render.text
    def out_text():
        return input.in_text()

app = App(app_ui, server)

I would expect that an empty reactive Value would simply return None, instead of raising a SilentException. My workaround for the moment is to initialize the reactive Value with a value, such as False, or pd.DataFrame().

Shiny version : 0.2.9

Kind Regards,

Bart

wch commented 1 year ago

Hi, this is an issue that we've been thinking about recently -- the current behavior is intentional (and different from Shiny for R), but we haven't decided for sure if we want to keep it that way. We will continue to think about what behavior would be best here.

bartverweire commented 1 year ago

Hi,

in that case, because empty reactive Values are behaving differently than reactive Values initialized with whatever value, maybe it would be appropriate to make the value property mandatory ? A non-initialized read-only reactive.Value is allowed, for example, but cannot be checked if it's empty.

I'm looking forward to the next steps of Shiny for Python by the way. This really is a great framework.

Regards

Bart

wch commented 1 year ago

I believe you can call input.x.is_set() to check whether an input value exists and has a value.

Also, as of #402, you can also do:

if "x" in input:
    ...

However, this change hasn't been released yet.

bartverweire commented 1 year ago

thx. I'll give it a try!

wch commented 1 year ago

Sorry, I didn't read your issue closely enough. Another option is to initialize the reactive.Value with a value of None.

   data = reactive.Value(None)
jcheng5 commented 1 year ago

We could also consider adding a default= parameter to the get() method (similar to the get() method on dict)

wch commented 1 year ago

Note that this is the same issue as #400.

bartverweire commented 1 year ago

@wch

Sorry, I didn't read your issue closely enough. Another option is to initialize the reactive.Value with a value of None.

   data = reactive.Value(None)

Thanks for the tip.

I'm working a lot with pandas data frames. Using reactive.Value(), I used req like

req(~data().empty)

When I switch to

data = reactive.Value(None)

I have to change my req conditions to

req(data(), ~data().empty)

Because otherwise, I'm getting the error

AttributeError: 'NoneType' object has no attribute 'empty' 

req doesn't seem to silently ignore this error. Is this intentional ?

So there are multiple workarounds, but you have to be aware of the consequences.