pythondashboardpy-shinydynamic-ui

Problems with dynamically added python shiny server modules


The short question is, How does one instantiate an arbitrary set of server modules? My UI modules render as intended, but I'm seeing signs that the buttons are triggering multiple server module responses per click (without incrementing the button value) or not triggering the server module at all.

I'm looking for suggestions on how to do it better or on what I may be doing wrong.

The goal is to create a UI that updates based on the contents of an external file (essentially a new accordion panel per line). The server module can also purge lines from that file (and therefore elements from the UI), making it circular. I read the contents of the file (as monitored by a reactive.file_reader()), and loop through the lines to instantiate a new server and UI module. The loop is run every time the external file is updated.

I may be creating multiple identical instances of the module every time the file updates, though I can't tell for sure. The UI renders and navigates as desired. See the multiple accordion elements and modal dialog after clicking the "Stop Run" button. The first time an experiment is ended in a session, the external file and UI update as expected. The second time creates two dialog boxes: the first only registers the "Keep Experiment Running" button, the second box accepts either button with the appropriate response in the UI. All subsequent attempts to "End the Experiment" fail any response in the server. More details on the symptoms below.

normal looking shiny for python app with a modal dialog box that will behave correctly just long enough to gain the trust of an unsuspecting user before it starts assulting them with unending errors

The code looks like this:

@reactive.file_reader(watched_file):
def data():
   do_stuff
   return list_of_desired_modules

@reactive.effect
@reactive.event(data)
@def _():
   server_list=[]
   ui_list = []
   for item in data():
      server_list.append(server_module_instance(item, args....) #server logic to populate graph,etc in UI
      ui_list.append(ui_module_instance(item, args....) #returns accordion_panel object
   @output
   @render.ui
   def dynamic_ui():
      return ui.accordion(*ui_list, id ="dynamic_accordion")
   return server_list

I've inspected elements in the browser to confirm that all the buttons have unique names inherited from their module instance.

I've strategically placed print commands to report what's happening when the buttons are clicked.

@reactive.Effect
@reactive.event(input.stop_run)
def _():
  print("stop_run registered", input.stop_run() )
  confirm_stop = ui.modal("Are you sure you want to stop this run?",
    title = "Stop Run?",
    footer= ui.row(ui.column(6,ui.modal_button("Keep Experiment Running")), 
      ui.column(6,ui.input_action_button("commit_stop", "End the Experiment"))),
      easy_close= False,
      )
  ui.modal_show(confirm_stop)
@reactive.Effect
@reactive.event(input.commit_stop)
def _():
  print(input.commit_stop(), "commit stop value")
  ... a bunch of other stuff, including removing a record from the watched_file...

For a new session where I try to stop several runs, I get the following print out:

#click stop_run on a module/accordion
stop_run registered 1             #one click, one increment, one print
1 commit stop value               #modal dialog and everything works

...
#click stop on another module/accordion
stop_run registered 1             #one click, one increment, two prints
stop_run registered 1
1 commit stop value               #two modal dialogs show up, 
                                  #first only accepts the modal_button, 
                                  #second accepts action button
...
#click stop on another module/accordion
stop_run registered 1             #once click, one increment, three prints
stop_run registered 1
stop_run registered 1 
                                  #one modal shows, action button is unresponsive, 
                                  #modal button closes modal dialog
...
#click again on same module/accordion
stop_run registered 2             #one click, one increment, three prints, same behavior as previous
stop_run registered 2
stop_run registered 2

I've tried using ui.remove_ui() to remove suspected pre-existing modules (within the loop, before returing fresh copies in the UI and server lists), but this yielded an empty space without any UI.

I've looked into ui.insert_accordion() and ui.remove_accordion(), but those don't have the logic for the server that I need for the graph, modal dialog, and buttons as shown in the linked image.

All examples of dynamic UI or modules on the shiny for python webpage have hard coded numbers of modules which are either shown or hidden. Not examples where calls to modules are created dynamically.


Solution

  • TLDR: add a counter when creating the new server/ui module IDs. Increment every time the block is executed. Hopefully, this is generalizable.

    What started off as a troubleshooting attempt ended up fixing the behavior. Since the problem seems to grow with each time the block with the server/ui list loop runs, I decided to add a counter to the module ID. That way, when I inspect the element in the browser, I can tell if I'm clicking the 0th instance or an N'th instance of the module. The elements are usually an N'th element, but behavior is as desired: One click, one print, one successful completion of downstream tasks, no non-responsive buttons.

    The code is as follows:

    counter = reactive.value(0) #NEW CHANGE: added a counter, counter = 0 at start of new session
    
    @reactive.file_reader(watched_file):
    def data():
       do_stuff
       return list_of_desired_modules
    
    @reactive.effect
    @reactive.event(data)
    @def _():
       server_list=[]
       ui_list = []
       for item in data():
          server_list.append(server_module_instance(f"{item}_{counter()}", args....) # NEW CHANGE: changed ID to include counter, new ID for same item
          ui_list.append(ui_module_instance(f"{item}_{counter()}", args....) #NEW CHANGE: changed ID to include counter, new ID for same item
     
       counter.set(counter() + 1) #NEW CHANGE:counter increments every time this reactive effect is executed.
     
       @output
       @render.ui
       def dynamic_ui():
          return ui.accordion(*ui_list, id ="dynamic_accordion")
       return server_list
    

    Memory usage doesn't seem to be ballooning with tons of previous iterations sitting around in the background, so this solution seems satisfactory. Hopefully they are being cleaned up.